fase 5
This commit is contained in:
@@ -0,0 +1,61 @@
|
||||
import apiClient from './client'
|
||||
import type {
|
||||
LoginRequest,
|
||||
TokenResponse,
|
||||
TOTPSetupResponse,
|
||||
TOTPStatusResponse,
|
||||
UserResponse,
|
||||
} from '@/types/api.types'
|
||||
|
||||
export const authApi = {
|
||||
/**
|
||||
* Login con email + password (+ opzionale codice TOTP).
|
||||
*/
|
||||
login: (data: LoginRequest) =>
|
||||
apiClient.post<TokenResponse>('/auth/login', data).then((r) => r.data),
|
||||
|
||||
/**
|
||||
* Rinnova access token con refresh token.
|
||||
*/
|
||||
refresh: (refreshToken: string) =>
|
||||
apiClient
|
||||
.post<TokenResponse>('/auth/refresh', { refresh_token: refreshToken })
|
||||
.then((r) => r.data),
|
||||
|
||||
/**
|
||||
* Revoca il refresh token (logout).
|
||||
*/
|
||||
logout: (refreshToken: string) =>
|
||||
apiClient.post('/auth/logout', { refresh_token: refreshToken }),
|
||||
|
||||
/**
|
||||
* Recupera l'utente corrente autenticato.
|
||||
*/
|
||||
me: () => apiClient.get<UserResponse>('/auth/me').then((r) => r.data),
|
||||
|
||||
/**
|
||||
* Setup 2FA TOTP: restituisce QR code e segreto.
|
||||
*/
|
||||
totpSetup: () =>
|
||||
apiClient.post<TOTPSetupResponse>('/auth/totp/setup').then((r) => r.data),
|
||||
|
||||
/**
|
||||
* Verifica e attiva il TOTP.
|
||||
*/
|
||||
totpVerify: (totp_code: string) =>
|
||||
apiClient
|
||||
.post<TOTPStatusResponse>('/auth/totp/verify', { totp_code })
|
||||
.then((r) => r.data),
|
||||
|
||||
/**
|
||||
* Disabilita il TOTP.
|
||||
*/
|
||||
totpDisable: () =>
|
||||
apiClient.post<TOTPStatusResponse>('/auth/totp/disable').then((r) => r.data),
|
||||
|
||||
/**
|
||||
* Cambio password utente corrente.
|
||||
*/
|
||||
changePassword: (current_password: string, new_password: string) =>
|
||||
apiClient.post('/auth/change-password', { current_password, new_password }),
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
/**
|
||||
* Client Axios base con interceptor per refresh token silenzioso.
|
||||
*
|
||||
* Flusso:
|
||||
* 1. Ogni richiesta include il Bearer token dall'auth store
|
||||
* 2. Se la risposta è 401, tenta il refresh silenzioso
|
||||
* 3. Se il refresh fallisce, fa logout e redirige a /login
|
||||
*/
|
||||
|
||||
import axios, { AxiosError, InternalAxiosRequestConfig } from 'axios'
|
||||
|
||||
const BASE_URL = '/api/v1'
|
||||
|
||||
// Flag per evitare loop infiniti di refresh
|
||||
let isRefreshing = false
|
||||
let pendingRequests: Array<(token: string) => void> = []
|
||||
|
||||
function processPendingRequests(token: string) {
|
||||
pendingRequests.forEach((cb) => cb(token))
|
||||
pendingRequests = []
|
||||
}
|
||||
|
||||
export const apiClient = axios.create({
|
||||
baseURL: BASE_URL,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
timeout: 30000,
|
||||
})
|
||||
|
||||
// ─── Request Interceptor: inietta access token ────────────────────────────────
|
||||
apiClient.interceptors.request.use(
|
||||
(config: InternalAxiosRequestConfig) => {
|
||||
// Legge il token dallo storage (evita dipendenza circolare con lo store)
|
||||
const token = localStorage.getItem('access_token')
|
||||
if (token && config.headers) {
|
||||
config.headers.Authorization = `Bearer ${token}`
|
||||
}
|
||||
return config
|
||||
},
|
||||
(error) => Promise.reject(error),
|
||||
)
|
||||
|
||||
// ─── Response Interceptor: gestisce 401 con refresh silenzioso ───────────────
|
||||
apiClient.interceptors.response.use(
|
||||
(response) => response,
|
||||
async (error: AxiosError) => {
|
||||
const originalRequest = error.config as InternalAxiosRequestConfig & {
|
||||
_retry?: boolean
|
||||
}
|
||||
|
||||
// Salta il refresh per l'endpoint di login e refresh stesso
|
||||
const skipRefresh = ['/auth/login', '/auth/refresh'].some((path) =>
|
||||
originalRequest.url?.includes(path),
|
||||
)
|
||||
|
||||
if (error.response?.status === 401 && !originalRequest._retry && !skipRefresh) {
|
||||
originalRequest._retry = true
|
||||
|
||||
if (isRefreshing) {
|
||||
// Accoda la richiesta in attesa del nuovo token
|
||||
return new Promise((resolve) => {
|
||||
pendingRequests.push((token: string) => {
|
||||
if (originalRequest.headers) {
|
||||
originalRequest.headers.Authorization = `Bearer ${token}`
|
||||
}
|
||||
resolve(apiClient(originalRequest))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
isRefreshing = true
|
||||
const refreshToken = localStorage.getItem('refresh_token')
|
||||
|
||||
if (!refreshToken) {
|
||||
// Nessun refresh token: logout
|
||||
handleLogout()
|
||||
return Promise.reject(error)
|
||||
}
|
||||
|
||||
try {
|
||||
const { data } = await axios.post(`${BASE_URL}/auth/refresh`, {
|
||||
refresh_token: refreshToken,
|
||||
})
|
||||
|
||||
const newAccessToken = data.access_token
|
||||
const newRefreshToken = data.refresh_token
|
||||
|
||||
localStorage.setItem('access_token', newAccessToken)
|
||||
localStorage.setItem('refresh_token', newRefreshToken)
|
||||
|
||||
processPendingRequests(newAccessToken)
|
||||
|
||||
if (originalRequest.headers) {
|
||||
originalRequest.headers.Authorization = `Bearer ${newAccessToken}`
|
||||
}
|
||||
return apiClient(originalRequest)
|
||||
} catch {
|
||||
handleLogout()
|
||||
return Promise.reject(error)
|
||||
} finally {
|
||||
isRefreshing = false
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.reject(error)
|
||||
},
|
||||
)
|
||||
|
||||
function handleLogout() {
|
||||
localStorage.removeItem('access_token')
|
||||
localStorage.removeItem('refresh_token')
|
||||
localStorage.removeItem('user')
|
||||
// Redirect alla pagina di login
|
||||
window.location.href = '/login'
|
||||
}
|
||||
|
||||
/**
|
||||
* Estrae il messaggio di errore leggibile da un AxiosError.
|
||||
*/
|
||||
export function getErrorMessage(error: unknown): string {
|
||||
if (axios.isAxiosError(error)) {
|
||||
const detail = error.response?.data?.detail
|
||||
if (typeof detail === 'string') return detail
|
||||
if (Array.isArray(detail)) {
|
||||
return detail.map((d) => d.msg || d).join(', ')
|
||||
}
|
||||
if (error.response?.status === 404) return 'Risorsa non trovata'
|
||||
if (error.response?.status === 403) return 'Accesso non autorizzato'
|
||||
if (error.response?.status === 500) return 'Errore interno del server'
|
||||
return error.message || 'Errore di rete'
|
||||
}
|
||||
if (error instanceof Error) return error.message
|
||||
return 'Errore sconosciuto'
|
||||
}
|
||||
|
||||
export default apiClient
|
||||
@@ -0,0 +1,31 @@
|
||||
import apiClient from './client'
|
||||
import type {
|
||||
ConnectionTestResult,
|
||||
MailboxCreateRequest,
|
||||
MailboxListResponse,
|
||||
MailboxResponse,
|
||||
MailboxUpdateRequest,
|
||||
} from '@/types/api.types'
|
||||
|
||||
export const mailboxesApi = {
|
||||
list: (page = 1, page_size = 50) =>
|
||||
apiClient
|
||||
.get<MailboxListResponse>('/mailboxes', { params: { page, page_size } })
|
||||
.then((r) => r.data),
|
||||
|
||||
get: (id: string) =>
|
||||
apiClient.get<MailboxResponse>(`/mailboxes/${id}`).then((r) => r.data),
|
||||
|
||||
create: (data: MailboxCreateRequest) =>
|
||||
apiClient.post<MailboxResponse>('/mailboxes', data).then((r) => r.data),
|
||||
|
||||
update: (id: string, data: MailboxUpdateRequest) =>
|
||||
apiClient.put<MailboxResponse>(`/mailboxes/${id}`, data).then((r) => r.data),
|
||||
|
||||
delete: (id: string) => apiClient.delete(`/mailboxes/${id}`),
|
||||
|
||||
testConnection: (id: string, protocol: 'imap' | 'smtp' = 'imap') =>
|
||||
apiClient
|
||||
.post<ConnectionTestResult>(`/mailboxes/${id}/test-connection`, { protocol })
|
||||
.then((r) => r.data),
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import apiClient from './client'
|
||||
import type {
|
||||
AttachmentResponse,
|
||||
MessageListResponse,
|
||||
MessageResponse,
|
||||
} from '@/types/api.types'
|
||||
|
||||
export interface MessageFilters {
|
||||
page?: number
|
||||
page_size?: number
|
||||
mailbox_id?: string
|
||||
direction?: 'inbound' | 'outbound'
|
||||
state?: string
|
||||
is_read?: boolean
|
||||
is_starred?: boolean
|
||||
is_archived?: boolean
|
||||
search?: string
|
||||
}
|
||||
|
||||
export const messagesApi = {
|
||||
list: (filters: MessageFilters = {}) =>
|
||||
apiClient
|
||||
.get<MessageListResponse>('/messages', { params: filters })
|
||||
.then((r) => r.data),
|
||||
|
||||
get: (id: string) =>
|
||||
apiClient.get<MessageResponse>(`/messages/${id}`).then((r) => r.data),
|
||||
|
||||
markRead: (id: string) =>
|
||||
apiClient.patch<MessageResponse>(`/messages/${id}`, { is_read: true }).then((r) => r.data),
|
||||
|
||||
markUnread: (id: string) =>
|
||||
apiClient.patch<MessageResponse>(`/messages/${id}`, { is_read: false }).then((r) => r.data),
|
||||
|
||||
toggleStar: (id: string, starred: boolean) =>
|
||||
apiClient
|
||||
.patch<MessageResponse>(`/messages/${id}`, { is_starred: starred })
|
||||
.then((r) => r.data),
|
||||
|
||||
archive: (id: string) =>
|
||||
apiClient
|
||||
.patch<MessageResponse>(`/messages/${id}`, { is_archived: true })
|
||||
.then((r) => r.data),
|
||||
|
||||
getAttachments: (id: string) =>
|
||||
apiClient.get<AttachmentResponse[]>(`/messages/${id}/attachments`).then((r) => r.data),
|
||||
|
||||
getAttachmentUrl: (messageId: string, attachmentId: string) =>
|
||||
`/api/v1/messages/${messageId}/attachments/${attachmentId}/download`,
|
||||
|
||||
getReceipts: (id: string) =>
|
||||
apiClient.get<MessageResponse[]>(`/messages/${id}/receipts`).then((r) => r.data),
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import apiClient from './client'
|
||||
import type {
|
||||
MailboxUserPermissionResponse,
|
||||
PermissionGrantRequest,
|
||||
PermissionResponse,
|
||||
UserMailboxPermissionResponse,
|
||||
} from '@/types/api.types'
|
||||
|
||||
export const permissionsApi = {
|
||||
/**
|
||||
* Assegna o aggiorna i permessi di un utente su una casella.
|
||||
*/
|
||||
grant: (mailboxId: string, userId: string, data: PermissionGrantRequest) =>
|
||||
apiClient
|
||||
.post<PermissionResponse>(
|
||||
`/permissions/mailboxes/${mailboxId}/users/${userId}`,
|
||||
data,
|
||||
)
|
||||
.then((r) => r.data),
|
||||
|
||||
/**
|
||||
* Revoca i permessi di un utente su una casella.
|
||||
*/
|
||||
revoke: (mailboxId: string, userId: string) =>
|
||||
apiClient.delete(`/permissions/mailboxes/${mailboxId}/users/${userId}`),
|
||||
|
||||
/**
|
||||
* Lista utenti con accesso a una casella.
|
||||
*/
|
||||
listMailboxUsers: (mailboxId: string) =>
|
||||
apiClient
|
||||
.get<MailboxUserPermissionResponse[]>(`/permissions/mailboxes/${mailboxId}/users`)
|
||||
.then((r) => r.data),
|
||||
|
||||
/**
|
||||
* Lista caselle accessibili a un utente.
|
||||
*/
|
||||
listUserMailboxes: (userId: string) =>
|
||||
apiClient
|
||||
.get<UserMailboxPermissionResponse[]>(`/permissions/users/${userId}/mailboxes`)
|
||||
.then((r) => r.data),
|
||||
|
||||
/**
|
||||
* Caselle accessibili all'utente corrente.
|
||||
*/
|
||||
myMailboxes: () =>
|
||||
apiClient
|
||||
.get<UserMailboxPermissionResponse[]>('/permissions/my/mailboxes')
|
||||
.then((r) => r.data),
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import apiClient from './client'
|
||||
import type {
|
||||
SendJobListResponse,
|
||||
SendJobResponse,
|
||||
SendPecRequest,
|
||||
} from '@/types/api.types'
|
||||
|
||||
export interface SendJobFilters {
|
||||
page?: number
|
||||
page_size?: number
|
||||
mailbox_id?: string
|
||||
status?: string
|
||||
}
|
||||
|
||||
export const sendApi = {
|
||||
send: (data: SendPecRequest) =>
|
||||
apiClient.post<SendJobResponse>('/send', data).then((r) => r.data),
|
||||
|
||||
listJobs: (filters: SendJobFilters = {}) =>
|
||||
apiClient.get<SendJobListResponse>('/send/jobs', { params: filters }).then((r) => r.data),
|
||||
|
||||
getJob: (id: string) =>
|
||||
apiClient.get<SendJobResponse>(`/send/jobs/${id}`).then((r) => r.data),
|
||||
|
||||
cancelJob: (id: string) => apiClient.delete(`/send/jobs/${id}`),
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import apiClient from './client'
|
||||
import type {
|
||||
UserCreateRequest,
|
||||
UserListResponse,
|
||||
UserResponse,
|
||||
UserUpdateRequest,
|
||||
} from '@/types/api.types'
|
||||
|
||||
export const usersApi = {
|
||||
list: (page = 1, page_size = 25) =>
|
||||
apiClient
|
||||
.get<UserListResponse>('/users', { params: { page, page_size } })
|
||||
.then((r) => r.data),
|
||||
|
||||
get: (id: string) =>
|
||||
apiClient.get<UserResponse>(`/users/${id}`).then((r) => r.data),
|
||||
|
||||
create: (data: UserCreateRequest) =>
|
||||
apiClient.post<UserResponse>('/users', data).then((r) => r.data),
|
||||
|
||||
update: (id: string, data: UserUpdateRequest) =>
|
||||
apiClient.patch<UserResponse>(`/users/${id}`, data).then((r) => r.data),
|
||||
|
||||
delete: (id: string) => apiClient.delete(`/users/${id}`),
|
||||
|
||||
resetPassword: (id: string, new_password: string) =>
|
||||
apiClient.post(`/users/${id}/reset-password`, { new_password }),
|
||||
}
|
||||
Reference in New Issue
Block a user