diff --git a/frontend/components/ToastProvider.tsx b/frontend/components/ToastProvider.tsx index 62a96f5..36dd8de 100644 --- a/frontend/components/ToastProvider.tsx +++ b/frontend/components/ToastProvider.tsx @@ -99,21 +99,28 @@ export function ToastProvider({ children }: { children: ReactNode }) { // createPortal needs `document`, which doesn't exist during SSR — mirrors // the mounted-guard pattern already used in ThemeToggle.tsx. - useEffect(() => setMounted(true), []); + useEffect(() => setMounted(true), []); const dismiss = useCallback((id: string) => { setToasts((prev) => prev.filter((t) => t.id !== id)); }, []); - const push = useCallback( + const push = useCallback( (type: ToastType, message: string, duration = DEFAULT_DURATION_MS) => { const id = `toast-${nextId.current++}`; setToasts((prev) => [...prev, { id, type, message, duration }].slice(-MAX_TOASTS), ); }, - [], - ); + [], + ); + + useEffect(() => { + const onSessionExpired = () => + push("error", "Your session has expired — please log in again", 0); + window.addEventListener("airflex:session-expired", onSessionExpired); + return () => window.removeEventListener("airflex:session-expired", onSessionExpired); + }, [push]); const value = useMemo( () => ({ @@ -143,4 +150,4 @@ export function ToastProvider({ children }: { children: ReactNode }) { )} ); -} \ No newline at end of file +} diff --git a/frontend/lib/apiFetch.test.ts b/frontend/lib/apiFetch.test.ts index 4e0d34f..05bc00c 100644 --- a/frontend/lib/apiFetch.test.ts +++ b/frontend/lib/apiFetch.test.ts @@ -45,4 +45,4 @@ describe("apiFetch authentication", () => { const [, options] = mockFetch.mock.calls[0] as [string, RequestInit]; expect(options.headers).not.toHaveProperty("Authorization"); }); -}); \ No newline at end of file +}); diff --git a/frontend/lib/apiFetch.ts b/frontend/lib/apiFetch.ts index f219bf0..6880e5d 100644 --- a/frontend/lib/apiFetch.ts +++ b/frontend/lib/apiFetch.ts @@ -2,7 +2,7 @@ * apiFetch.ts — Centralized API fetch wrapper and error type for frontend. */ -import { getToken } from "../app/lib/auth"; +import { clearToken, getToken } from "../app/lib/auth"; export class ApiError extends Error { status: number; @@ -63,6 +63,16 @@ export async function apiFetch( // response was not JSON } + if (response.status === 401 && typeof window !== "undefined") { + clearToken(); + window.dispatchEvent(new CustomEvent("airflex:session-expired")); + + const returnTo = `${window.location.pathname}${window.location.search}${window.location.hash}`; + if (!window.location.pathname.startsWith("/auth/signup")) { + window.location.assign(`/auth/signup?returnTo=${encodeURIComponent(returnTo)}`); + } + } + throw new ApiError(errorMessage, response.status, errorData); }