diff --git a/src/frontend/src/App.tsx b/src/frontend/src/App.tsx index ee62e12..23b6530 100644 --- a/src/frontend/src/App.tsx +++ b/src/frontend/src/App.tsx @@ -25,6 +25,7 @@ import { clearUserId, ensureUserId, hasStoredUserId, + USER_SESSION_INVALIDATED_EVENT, updateUserId, } from "./lib/userIdentity"; import { THEME_MODE_KEY } from "./lib/constants"; @@ -363,6 +364,28 @@ export default function App() { }; }, []); + useEffect(() => { + const handleSessionInvalidated = () => { + setUserId(""); + setUserEmail(""); + setIsOnboardingOpen(true); + navigateTo("/"); + message.warning("Your session is no longer valid. Please sign in again."); + }; + + window.addEventListener( + USER_SESSION_INVALIDATED_EVENT, + handleSessionInvalidated, + ); + + return () => { + window.removeEventListener( + USER_SESSION_INVALIDATED_EVENT, + handleSessionInvalidated, + ); + }; + }, []); + const themeConfig = useMemo( () => ({ algorithm: themeMode === "dark" ? darkAlgorithm : defaultAlgorithm, diff --git a/src/frontend/src/lib/api.ts b/src/frontend/src/lib/api.ts index 0b9a0d1..5ec9433 100644 --- a/src/frontend/src/lib/api.ts +++ b/src/frontend/src/lib/api.ts @@ -1,4 +1,4 @@ -import { ensureUserId } from "./userIdentity"; +import { ensureUserId, invalidateUserSession } from "./userIdentity"; import type { ApiError } from "./types"; const API_BASE = "/api"; @@ -17,6 +17,15 @@ export function isNotFoundError(error: unknown) { return !!(maybeError && (maybeError.isNotFound || maybeError.status === 404)); } +function shouldClearStoredUserId(status: number, detail: unknown) { + if (status !== 401) { + return false; + } + + const normalizedDetail = String(detail || "").trim().toLowerCase(); + return normalizedDetail === "invalid user id" || normalizedDetail === "missing user id"; +} + export async function apiFetch(url: string, options: RequestInit = {}): Promise { const fullUrl = url.startsWith("/api") ? API_BASE + url.slice(4) : url; const userId = ensureUserId(); @@ -45,6 +54,10 @@ export async function apiFetch(url: string, options: RequestInit = {}): if (!response.ok) { const payload = await response.json().catch(() => ({} as { detail?: string })); + if (shouldClearStoredUserId(response.status, payload?.detail)) { + invalidateUserSession(); + } + throw createApiError(payload.detail || `Request failed (${response.status})`, { status: response.status, }); diff --git a/src/frontend/src/lib/userIdentity.ts b/src/frontend/src/lib/userIdentity.ts index fbd52f2..d9c57e8 100644 --- a/src/frontend/src/lib/userIdentity.ts +++ b/src/frontend/src/lib/userIdentity.ts @@ -2,6 +2,7 @@ import { USER_TOKEN_KEY } from "./constants"; import { getStoredValue, removeStoredValue, setStoredValue } from "./storage"; const emailByUserId = new Map(); +export const USER_SESSION_INVALIDATED_EVENT = "lunchtime:user-session-invalidated"; function generateGuid() { if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") { @@ -76,3 +77,11 @@ export function regenerateUserId() { export function clearUserId() { removeStoredValue(USER_TOKEN_KEY); } + +export function invalidateUserSession() { + clearUserId(); + + if (typeof window !== "undefined") { + window.dispatchEvent(new Event(USER_SESSION_INVALIDATED_EVENT)); + } +}