134 lines
4.2 KiB
TypeScript
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);
|
|
}
|
|
);
|
|
|