diff --git a/VEZA_INTEGRATION_PERFECTION_TODOLIST_TEMPLATE.json b/VEZA_INTEGRATION_PERFECTION_TODOLIST_TEMPLATE.json index 312ae0127..342a6269b 100644 --- a/VEZA_INTEGRATION_PERFECTION_TODOLIST_TEMPLATE.json +++ b/VEZA_INTEGRATION_PERFECTION_TODOLIST_TEMPLATE.json @@ -620,7 +620,8 @@ "description": "S'assurer que le refresh token gère: expiration, révocation, erreurs réseau.", "priority": "P1", "priority_rank": 18, - "status": "todo", + "status": "completed", + "completed_at": "2025-01-27T16:00:00Z", "estimated_hours": 2, "side": "frontend_only", "files_to_modify": [ @@ -1099,13 +1100,13 @@ }, "progress_tracking": { "total_tasks": 32, - "completed": 17, + "completed": 18, "in_progress": 0, - "todo": 15, + "todo": 14, "blocked": 0, - "completion_percentage": 53, - "last_updated": "2025-01-27T15:45:00Z", + "completion_percentage": 56, + "last_updated": "2025-01-27T16:00:00Z", "estimated_completion_date": null, - "estimated_hours_remaining": 23 + "estimated_hours_remaining": 21 } } diff --git a/apps/web/src/services/api/client.ts b/apps/web/src/services/api/client.ts index ba41f83b2..d8a82c12b 100644 --- a/apps/web/src/services/api/client.ts +++ b/apps/web/src/services/api/client.ts @@ -500,19 +500,61 @@ apiClient.interceptors.response.use( }); } - // Détecter 401 et refresh automatiquement + // INT-AUTH-003: Détecter 401 et refresh automatiquement // EXCLURE l'endpoint /auth/refresh pour éviter les boucles infinies const isRefreshEndpoint = originalRequest?.url?.includes('/auth/refresh'); + // INT-AUTH-003: Handle 401 on /auth/refresh endpoint - token expired/revoked, logout and redirect + if (error.response?.status === 401 && isRefreshEndpoint) { + logger.error('[API] 401 on /auth/refresh - refresh token expired or revoked, logging out', { + request_id: requestId, + url: originalRequest?.url, + }); + + // Clear tokens + TokenStorage.clearTokens(); + + // Clear CSRF token + csrfService.clearToken(); + + // Clear auth store state + if (typeof window !== 'undefined') { + // Import and use auth store to clear state + import('@/features/auth/store/authStore').then(({ useAuthStore }) => { + const store = useAuthStore.getState(); + store.logout().catch((err) => { + logger.error('[API] Failed to logout from store after refresh token 401', { error: err }); + }); + }).catch((err) => { + logger.error('[API] Failed to import auth store for logout', { error: err }); + }); + + // Store error message for display after redirect + sessionStorage.setItem( + 'auth_error', + 'Votre session a expiré. Veuillez vous reconnecter.', + ); + // Redirect to login + window.location.href = '/login'; + } + + return Promise.reject(parseApiError(error)); + } + if ( error.response?.status === 401 && originalRequest && !originalRequest._retry && !isRefreshEndpoint ) { - // Éviter les refresh multiples simultanés + // INT-AUTH-003: Éviter les refresh multiples simultanés if (isRefreshing) { // Si un refresh est en cours, mettre la requête en queue + logger.debug('[API] Refresh already in progress, queuing request', { + request_id: requestId, + url: originalRequest?.url, + queue_size: failedQueue.length, + }); return new Promise((resolve, reject) => { failedQueue.push({ resolve, reject }); }) @@ -520,9 +562,18 @@ apiClient.interceptors.response.use( if (originalRequest.headers && token) { originalRequest.headers.Authorization = `Bearer ${token} `; } + logger.debug('[API] Replaying queued request after successful refresh', { + request_id: requestId, + url: originalRequest?.url, + }); return apiClient(originalRequest); }) .catch((err) => { + logger.error('[API] Queued request failed after refresh', { + request_id: requestId, + url: originalRequest?.url, + error: err, + }); return Promise.reject(err); }); } @@ -530,8 +581,14 @@ apiClient.interceptors.response.use( originalRequest._retry = true; isRefreshing = true; + logger.info('[API] Starting token refresh due to 401', { + request_id: requestId, + url: originalRequest?.url, + method: originalRequest?.method, + }); + try { - // Refresh automatique du token + // INT-AUTH-003: Refresh automatique du token await refreshToken(); const newToken = TokenStorage.getAccessToken(); @@ -539,25 +596,53 @@ apiClient.interceptors.response.use( throw new Error('Failed to get new access token after refresh'); } + logger.info('[API] Token refresh successful, retrying original request', { + request_id: requestId, + url: originalRequest?.url, + queue_size: failedQueue.length, + }); + if (originalRequest.headers) { originalRequest.headers.Authorization = `Bearer ${newToken} `; } - // Traiter la queue et retry la requête originale + // INT-AUTH-003: Traiter la queue et retry la requête originale + // Toutes les requêtes en queue seront rejouées avec le nouveau token processQueue(null, newToken); return apiClient(originalRequest); } catch (refreshError) { - // Gérer cas refresh échoué + // INT-AUTH-003: Gérer cas refresh échoué (expiration, révocation, erreur réseau) + logger.error('[API] Token refresh failed, logging out', { + request_id: requestId, + error: refreshError, + queue_size: failedQueue.length, + }); + + // Rejeter toutes les requêtes en queue processQueue(refreshError as Error, null); // Nettoyer les tokens TokenStorage.clearTokens(); - // Stocker un message d'erreur pour l'afficher après redirection + // Clear CSRF token + csrfService.clearToken(); + + // INT-AUTH-003: Clear auth store state and redirect to login if (typeof window !== 'undefined') { + // Import and use auth store to clear state + import('@/features/auth/store/authStore').then(({ useAuthStore }) => { + const store = useAuthStore.getState(); + store.logout().catch((err) => { + logger.error('[API] Failed to logout from store after refresh failure', { error: err }); + }); + }).catch((err) => { + logger.error('[API] Failed to import auth store for logout', { error: err }); + }); + + // Stocker un message d'erreur pour l'afficher après redirection sessionStorage.setItem( 'auth_error', - 'Your session has expired. Please log in again.', + 'Votre session a expiré. Veuillez vous reconnecter.', ); // Rediriger vers login si refresh échoue (seulement dans le navigateur) window.location.href = '/login'; @@ -566,6 +651,10 @@ apiClient.interceptors.response.use( return Promise.reject(refreshError); } finally { isRefreshing = false; + logger.debug('[API] Token refresh process completed', { + request_id: requestId, + is_refreshing: false, + }); } }