This commit is contained in:
2026-03-18 20:54:43 +01:00
parent b3c8b77f12
commit 9fe656b34c
8058 changed files with 912898 additions and 23 deletions
+61
View File
@@ -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 }),
}
+137
View File
@@ -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
+31
View File
@@ -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),
}
+53
View File
@@ -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),
}
+50
View File
@@ -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),
}
+26
View File
@@ -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}`),
}
+28
View File
@@ -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 }),
}