const BASE = "/api/v1"; // A non-sensitive flag in localStorage that tells the SPA whether the user has // an active session. The actual JWT lives in an httpOnly cookie and is never // readable by JavaScript. Clearing this flag is sufficient for client-side // route guards; the server still validates the cookie on every request. const SESSION_KEY = "rh_session"; export function markLoggedIn(): void { localStorage.setItem(SESSION_KEY, "1"); } export function markLoggedOut(): void { localStorage.removeItem(SESSION_KEY); } export function isLoggedIn(): boolean { return localStorage.getItem(SESSION_KEY) === "1"; } async function request( path: string, options: RequestInit = {}, isFormData = false ): Promise { const headers: Record = { ...(options.headers as Record), }; if (!isFormData) { headers["Content-Type"] = "application/json"; } const resp = await fetch(`${BASE}${path}`, { ...options, headers, credentials: "include", // send httpOnly cookie on every request }); if (!resp.ok) { if (resp.status === 401) { markLoggedOut(); window.location.href = "/login"; throw new Error("Session expired"); } const error = await resp.json().catch(() => ({ detail: resp.statusText })); throw new Error(error.detail ?? resp.statusText); } if (resp.status === 204) return undefined as T; return resp.json(); } export const api = { get: (path: string) => request(path), post: (path: string, body: unknown) => request(path, { method: "POST", body: JSON.stringify(body) }), upload: (path: string, formData: FormData) => request(path, { method: "POST", body: formData }, true), patch: (path: string, body: unknown) => request(path, { method: "PATCH", body: JSON.stringify(body) }), delete: (path: string) => request(path, { method: "DELETE" }), };