veza/apps/web/src/services/api/client.ts
2025-12-03 22:56:50 +01:00

134 lines
4.2 KiB
TypeScript

import axios, { AxiosError, InternalAxiosRequestConfig } from 'axios';
import { TokenStorage } from '../tokenStorage';
import { refreshToken } from '../tokenRefresh';
/**
* Client API avec interceptors pour refresh automatique des tokens
* T0177: Interceptor axios pour détecter 401 et refresh automatique
*/
// Configuration de base
const API_BASE_URL =
import.meta.env.VITE_API_BASE_URL || 'http://localhost:8080/api/v1';
// Client API réutilisable
export const apiClient = axios.create({
baseURL: API_BASE_URL,
timeout: 10000,
headers: {
'Content-Type': 'application/json',
},
});
// Flag pour éviter les refresh en boucle
let isRefreshing = false;
let failedQueue: Array<{
resolve: (value?: any) => void;
reject: (error?: any) => void;
}> = [];
// T0177: Fonction pour traiter la queue de requêtes en attente
const processQueue = (error: Error | null, token: string | null = null) => {
failedQueue.forEach(prom => {
if (error) {
prom.reject(error);
} else {
prom.resolve(token);
}
});
failedQueue = [];
};
// T0177: Interceptor de requête pour ajouter le token d'accès
apiClient.interceptors.request.use(
(config: InternalAxiosRequestConfig) => {
const token = TokenStorage.getAccessToken();
if (token && config.headers) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => {
return Promise.reject(error);
}
);
// T0177: Interceptor de réponse pour détecter 401 et refresh automatique
apiClient.interceptors.response.use(
(response) => {
return response;
},
async (error: AxiosError) => {
const originalRequest = error.config as InternalAxiosRequestConfig & {
_retry?: boolean;
};
// T0177: Détecter 401 et refresh automatiquement
if (error.response?.status === 401 && originalRequest && !originalRequest._retry) {
// Éviter les refresh multiples simultanés
if (isRefreshing) {
// Si un refresh est en cours, mettre la requête en queue
return new Promise((resolve, reject) => {
failedQueue.push({ resolve, reject });
})
.then((token) => {
if (originalRequest.headers) {
originalRequest.headers.Authorization = `Bearer ${token}`;
}
return apiClient(originalRequest);
})
.catch((err) => {
return Promise.reject(err);
});
}
originalRequest._retry = true;
isRefreshing = true;
try {
// T0177: Refresh automatique du token
await refreshToken();
const newToken = TokenStorage.getAccessToken();
if (newToken && originalRequest.headers) {
originalRequest.headers.Authorization = `Bearer ${newToken}`;
}
// T0177: Traiter la queue et retry la requête originale
processQueue(null, newToken);
return apiClient(originalRequest);
} catch (refreshError) {
// T0177: Gérer cas refresh échoué
// T0178: Rediriger vers login si refresh échoue et afficher message
processQueue(refreshError as Error, null);
// T0178: Nettoyer les tokens
TokenStorage.clearTokens();
// T0178: Stocker un message d'erreur pour l'afficher après redirection
sessionStorage.setItem('auth_error', 'Your session has expired. Please log in again.');
// T0178: Rediriger vers login si refresh échoue
// Utiliser window.location pour forcer un rechargement complet et nettoyer l'état
if (typeof window !== 'undefined') {
window.location.href = '/login';
}
return Promise.reject(refreshError);
} finally {
isRefreshing = false;
}
}
// T0178: Détecter les erreurs liées à l'expiration du token (header X-Token-Expired)
if (error.response?.status === 401 && error.response.headers?.['x-token-expired'] === 'true') {
// Token expiré détecté via header
// Tenter le refresh automatique sera géré par le bloc ci-dessus lors du prochain 401
// Pour l'instant, on laisse passer l'erreur pour que le refresh automatique se déclenche
}
return Promise.reject(error);
}
);