[INT-AUTH-004] Add token expiration pre-check
This commit is contained in:
parent
25c38d10b7
commit
c171d66b0c
3 changed files with 71 additions and 12 deletions
|
|
@ -650,7 +650,8 @@
|
|||
"description": "Vérifier l'expiration du token AVANT d'envoyer une requête pour éviter 401 inutiles.",
|
||||
"priority": "P2",
|
||||
"priority_rank": 19,
|
||||
"status": "todo",
|
||||
"status": "completed",
|
||||
"completed_at": "2025-01-27T16:15:00Z",
|
||||
"estimated_hours": 1.5,
|
||||
"side": "frontend_only",
|
||||
"files_to_modify": [
|
||||
|
|
@ -1100,13 +1101,13 @@
|
|||
},
|
||||
"progress_tracking": {
|
||||
"total_tasks": 32,
|
||||
"completed": 18,
|
||||
"completed": 19,
|
||||
"in_progress": 0,
|
||||
"todo": 14,
|
||||
"todo": 13,
|
||||
"blocked": 0,
|
||||
"completion_percentage": 56,
|
||||
"last_updated": "2025-01-27T16:00:00Z",
|
||||
"completion_percentage": 59,
|
||||
"last_updated": "2025-01-27T16:15:00Z",
|
||||
"estimated_completion_date": null,
|
||||
"estimated_hours_remaining": 21
|
||||
"estimated_hours_remaining": 19.5
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import axios, { AxiosError, InternalAxiosRequestConfig, AxiosResponse } from 'ax
|
|||
import toast from 'react-hot-toast';
|
||||
import { z } from 'zod';
|
||||
import { TokenStorage } from '../tokenStorage';
|
||||
import { refreshToken } from '../tokenRefresh';
|
||||
import { refreshToken, isTokenExpiringSoon } from '../tokenRefresh';
|
||||
import { env } from '@/config/env';
|
||||
import { parseApiError } from '@/utils/apiErrorHandler';
|
||||
import { csrfService } from '../csrf';
|
||||
|
|
@ -208,10 +208,67 @@ const processQueue = (error: Error | null, token: string | null = null) => {
|
|||
// T0177: Interceptor de requête pour ajouter le token d'accès
|
||||
// CRITIQUE: Récupérer TOUJOURS le token frais depuis localStorage car Zustand peut ne pas être hydraté
|
||||
apiClient.interceptors.request.use(
|
||||
(config: InternalAxiosRequestConfig) => {
|
||||
async (config: InternalAxiosRequestConfig) => {
|
||||
// INT-AUTH-004: Vérifier l'expiration du token avant d'envoyer la requête
|
||||
// Buffer de 60 secondes pour éviter les 401 inutiles
|
||||
const PRE_REQUEST_REFRESH_BUFFER_MS = 60 * 1000; // 60 secondes
|
||||
|
||||
const token = TokenStorage.getAccessToken();
|
||||
|
||||
if (token && config.headers) {
|
||||
const isRefreshEndpoint = config.url?.includes('/auth/refresh');
|
||||
const isCSRFRoute = config.url?.includes('/csrf-token');
|
||||
|
||||
// Ne pas vérifier l'expiration pour les endpoints de refresh et CSRF pour éviter les boucles
|
||||
if (token && !isRefreshEndpoint && !isCSRFRoute) {
|
||||
// Vérifier si le token expire bientôt (dans moins de 60s)
|
||||
if (isTokenExpiringSoon(token, PRE_REQUEST_REFRESH_BUFFER_MS)) {
|
||||
// Si un refresh est déjà en cours, attendre qu'il se termine
|
||||
if (isRefreshing) {
|
||||
logger.debug('[API] Token expiring soon but refresh already in progress, waiting...', {
|
||||
url: config.url,
|
||||
});
|
||||
// Attendre que le refresh se termine (max 5s)
|
||||
let waitCount = 0;
|
||||
while (isRefreshing && waitCount < 50) {
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
waitCount++;
|
||||
}
|
||||
// Récupérer le nouveau token après le refresh
|
||||
const newToken = TokenStorage.getAccessToken();
|
||||
if (newToken && config.headers) {
|
||||
config.headers.Authorization = `Bearer ${newToken} `;
|
||||
}
|
||||
} else {
|
||||
// Rafraîchir proactivement le token
|
||||
try {
|
||||
logger.debug('[API] Token expiring soon, refreshing proactively before request', {
|
||||
url: config.url,
|
||||
buffer_seconds: PRE_REQUEST_REFRESH_BUFFER_MS / 1000,
|
||||
});
|
||||
await refreshToken();
|
||||
const newToken = TokenStorage.getAccessToken();
|
||||
if (newToken && config.headers) {
|
||||
config.headers.Authorization = `Bearer ${newToken} `;
|
||||
}
|
||||
} catch (refreshError) {
|
||||
// Si le refresh échoue, continuer avec le token actuel
|
||||
// L'interceptor de réponse gérera l'erreur 401 si nécessaire
|
||||
logger.warn('[API] Proactive token refresh failed, continuing with current token', {
|
||||
url: config.url,
|
||||
error: refreshError,
|
||||
});
|
||||
if (token && config.headers) {
|
||||
config.headers.Authorization = `Bearer ${token} `;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Token valide, utiliser normalement
|
||||
if (config.headers) {
|
||||
config.headers.Authorization = `Bearer ${token} `;
|
||||
}
|
||||
}
|
||||
} else if (token && config.headers) {
|
||||
// Token présent mais endpoint de refresh/CSRF, utiliser sans vérification
|
||||
config.headers.Authorization = `Bearer ${token} `;
|
||||
}
|
||||
|
||||
|
|
@ -225,7 +282,7 @@ apiClient.interceptors.request.use(
|
|||
// INT-AUTH-001: Ajouter le token CSRF pour les méthodes qui modifient l'état
|
||||
const method = config.method?.toUpperCase();
|
||||
const isStateChanging = ['POST', 'PUT', 'DELETE', 'PATCH'].includes(method || '');
|
||||
const isCSRFRoute = config.url?.includes('/csrf-token');
|
||||
// isCSRFRoute déjà défini plus haut
|
||||
|
||||
if (isStateChanging && !isCSRFRoute && config.headers) {
|
||||
const csrfToken = csrfService.getToken();
|
||||
|
|
|
|||
|
|
@ -84,11 +84,12 @@ function decodeJWT(token: string): JWTPayload | null {
|
|||
|
||||
/**
|
||||
* INT-016: Vérifie si un token est expiré ou proche de l'expiration
|
||||
* INT-AUTH-004: Exporté pour utilisation dans l'interceptor de requête
|
||||
* @param token - Token JWT à vérifier
|
||||
* @param bufferMs - Buffer en millisecondes avant expiration (défaut: 5 minutes)
|
||||
* @returns true si le token est expiré ou proche de l'expiration
|
||||
*/
|
||||
function isTokenExpiringSoon(token: string | null, bufferMs: number = PROACTIVE_REFRESH_BUFFER_MS): boolean {
|
||||
export function isTokenExpiringSoon(token: string | null, bufferMs: number = PROACTIVE_REFRESH_BUFFER_MS): boolean {
|
||||
if (!token) {
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue