diff --git a/backend/src/routes/auth.routes.ts b/backend/src/routes/auth.routes.ts index 216e202..dcd18f2 100644 --- a/backend/src/routes/auth.routes.ts +++ b/backend/src/routes/auth.routes.ts @@ -152,7 +152,7 @@ router.post("/refresh", async (req: Request, res: Response): Promise => { } try { - const response = await fetch(`${superOAuthUrl}/api/v1/auth/token/refresh`, { + const response = await fetch(`${superOAuthUrl}/api/v1/auth/refresh`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ refreshToken }), @@ -160,22 +160,22 @@ router.post("/refresh", async (req: Request, res: Response): Promise => { const data = await response.json() as { success: boolean; - data?: { tokens: { accessToken: string; refreshToken?: string }; user?: { id: string; email: string | null; nickname: string } }; + data?: { accessToken: string; refreshToken?: string }; error?: string; }; - if (!response.ok || !data.data?.tokens?.accessToken) { + if (!response.ok || !data.data?.accessToken) { res.clearCookie(COOKIE_NAME); res.clearCookie(REFRESH_COOKIE_NAME); res.status(401).json({ success: false, error: "REFRESH_FAILED" }); return; } - res.cookie(COOKIE_NAME, data.data.tokens.accessToken, COOKIE_OPTIONS); - if (data.data.tokens.refreshToken) { - res.cookie(REFRESH_COOKIE_NAME, data.data.tokens.refreshToken, REFRESH_COOKIE_OPTIONS); + res.cookie(COOKIE_NAME, data.data.accessToken, COOKIE_OPTIONS); + if (data.data.refreshToken) { + res.cookie(REFRESH_COOKIE_NAME, data.data.refreshToken, REFRESH_COOKIE_OPTIONS); } - res.json({ success: true, data: { user: data.data.user ?? null } }); + res.json({ success: true }); } catch (err) { logger.error("POST /auth/refresh — auth service unavailable", { err }); res.status(500).json({ success: false, error: "AUTH_SERVICE_UNAVAILABLE" }); diff --git a/frontend/src/context/AuthContext.tsx b/frontend/src/context/AuthContext.tsx index 9efa02a..475e37b 100644 --- a/frontend/src/context/AuthContext.tsx +++ b/frontend/src/context/AuthContext.tsx @@ -38,6 +38,13 @@ export function AuthProvider({ children }: { children: ReactNode }) { return () => { cancelled = true; }; }, []); + // Token expiré et refresh impossible → déconnexion silencieuse + useEffect(() => { + const handler = () => setUser(null); + window.addEventListener('auth:expired', handler); + return () => window.removeEventListener('auth:expired', handler); + }, []); + return ( {children} diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index 639a438..a99b9b3 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -8,19 +8,39 @@ export class ApiError extends Error { } } -export async function apiFetch(path: string, init?: RequestInit): Promise { - const res = await fetch(`${BASE}${path}`, { - credentials: 'include', // transmet le cookie httpOnly automatiquement +function buildInit(init?: RequestInit): RequestInit { + return { + credentials: 'include', ...init, - headers: { - 'Content-Type': 'application/json', - ...init?.headers, - }, - }); + headers: { 'Content-Type': 'application/json', ...init?.headers }, + }; +} - if (!res.ok) { - throw new ApiError(res.status, path); +// Déduplique les appels refresh simultanés +let refreshingPromise: Promise | null = null; + +async function tryRefresh(): Promise { + if (refreshingPromise) return refreshingPromise; + refreshingPromise = fetch(`${BASE}/auth/refresh`, { method: 'POST', credentials: 'include' }) + .then((r) => r.ok) + .finally(() => { refreshingPromise = null; }); + return refreshingPromise; +} + +export async function apiFetch(path: string, init?: RequestInit): Promise { + const res = await fetch(`${BASE}${path}`, buildInit(init)); + + if (res.status === 401 && path !== '/auth/refresh') { + const refreshed = await tryRefresh(); + if (refreshed) { + const retry = await fetch(`${BASE}${path}`, buildInit(init)); + if (!retry.ok) throw new ApiError(retry.status, path); + return retry.json() as Promise; + } + window.dispatchEvent(new Event('auth:expired')); + throw new ApiError(401, path); } + if (!res.ok) throw new ApiError(res.status, path); return res.json() as Promise; }