M15: synchronize contracts and acceptance
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { createContext, useCallback, useContext, useEffect, useState, type ReactNode } from "react";
|
||||
import { api, onUnauthorized } from "../api/client";
|
||||
import { api, ApiError, onUnauthorized } from "../api/client";
|
||||
import type { CurrentUser, Role, SystemStatus } from "../api/types";
|
||||
|
||||
interface AuthState {
|
||||
@@ -93,13 +93,28 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
}, []);
|
||||
|
||||
const logout = useCallback(async () => {
|
||||
setUser(null);
|
||||
cacheUser(null);
|
||||
try {
|
||||
await api.post("/api/v1/auth/logout");
|
||||
// A logout response expires an HttpOnly cookie. Chromium can expose a very small
|
||||
// race between resolving fetch() and applying that Set-Cookie header to an
|
||||
// immediate top-level navigation. Confirm the server now rejects the session
|
||||
// before allowing the router to continue; retrying logout is idempotent.
|
||||
for (let attempt = 0; attempt < 3; attempt += 1) {
|
||||
await api.post("/api/v1/auth/logout");
|
||||
try {
|
||||
await api.get<CurrentUser>("/api/v1/auth/session");
|
||||
} catch (err) {
|
||||
if (err instanceof ApiError && err.status === 401) return;
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Best effort: the cookie is cleared server-side when it works, and the client has
|
||||
// already dropped its own state either way.
|
||||
// still drops its own state when the request itself is unavailable.
|
||||
} finally {
|
||||
// Keep RequireAuth from redirecting to /login while the server-side invalidation
|
||||
// is still in flight. Otherwise a very fast navigation can race ahead of logout.
|
||||
setUser(null);
|
||||
cacheUser(null);
|
||||
}
|
||||
}, []);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user