feat: B2 — 401 interceptor + auto-refresh token (fix SuperOAuth path + response shape)
All checks were successful
CI/CD — Build & Deploy / Build & Deploy (push) Successful in 27s
All checks were successful
CI/CD — Build & Deploy / Build & Deploy (push) Successful in 27s
This commit is contained in:
@@ -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 (
|
||||
<AuthContext.Provider value={{ user, loading, setUser }}>
|
||||
{children}
|
||||
|
||||
@@ -8,19 +8,39 @@ export class ApiError extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
export async function apiFetch<T>(path: string, init?: RequestInit): Promise<T> {
|
||||
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<boolean> | null = null;
|
||||
|
||||
async function tryRefresh(): Promise<boolean> {
|
||||
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<T>(path: string, init?: RequestInit): Promise<T> {
|
||||
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<T>;
|
||||
}
|
||||
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<T>;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user