export const AUTH_REMEMBER_ME_KEY = "authRememberMe"; const AUTH_PERSISTENCE_KEY = "authTokenPersistence"; const AUTH_USER_KEY = "authUserKey"; const AUTH_CSRF_COOKIE = "XSRF-TOKEN"; export const AUTH_USER_CHANGED_EVENT = "auth-user-changed"; export type AuthPersistence = "local" | "session"; function safeGet(storage: Storage, key: string): string | null { try { return storage.getItem(key); } catch { return null; } } function safeSet(storage: Storage, key: string, value: string) { try { storage.setItem(key, value); } catch { // ignore storage failures } } function safeRemove(storage: Storage, key: string) { try { storage.removeItem(key); } catch { // ignore storage failures } } function emitAuthChanged() { window.dispatchEvent(new Event("auth-changed")); } function normalizePersistence(value: string | null | undefined): AuthPersistence { return value === "session" ? "session" : "local"; } function persistPreference(persistence: AuthPersistence) { safeSet(window.localStorage, AUTH_PERSISTENCE_KEY, persistence); safeSet(window.localStorage, AUTH_REMEMBER_ME_KEY, persistence === "local" ? "1" : "0"); } function getStoredPersistence(): AuthPersistence { const explicit = safeGet(window.localStorage, AUTH_PERSISTENCE_KEY); if (explicit) return normalizePersistence(explicit); const rememberMe = safeGet(window.localStorage, AUTH_REMEMBER_ME_KEY); if (rememberMe === "0") return "session"; return "local"; } export function getRememberMePref(): boolean { return getAuthPersistencePreference() === "local"; } export function setRememberMePref(value: boolean) { persistPreference(value ? "local" : "session"); } export function getAuthPersistencePreference(): AuthPersistence { return getStoredPersistence(); } export function setAuthPersistencePreference(persistence: AuthPersistence) { persistPreference(persistence); emitAuthChanged(); } export function getAuthUserKey(): string { return safeGet(window.localStorage, AUTH_USER_KEY) ?? "anon"; } export function setAuthUserKey(value: string | null | undefined, emit = true) { const previous = getAuthUserKey(); const next = typeof value === "string" ? value.trim() : ""; if (!next) { safeRemove(window.localStorage, AUTH_USER_KEY); } else { safeSet(window.localStorage, AUTH_USER_KEY, next); } if (previous !== (next || "anon")) window.dispatchEvent(new Event(AUTH_USER_CHANGED_EVENT)); if (emit) emitAuthChanged(); } export function clearAuthClientState(emit = true) { // Only emit "auth-changed" when this call actually transitions from // "signed in" to "signed out". The response interceptor calls this on every // 401; without this guard each 401 re-dispatches "auth-changed", which // re-fetches /auth/me, which 401s again — an infinite request loop whenever // the user is logged out (login page, expired session). const had = safeGet(window.localStorage, AUTH_USER_KEY) != null; safeRemove(window.localStorage, AUTH_USER_KEY); if (had) window.dispatchEvent(new Event(AUTH_USER_CHANGED_EVENT)); if (emit && had) emitAuthChanged(); } export function getCsrfToken(): string | null { try { const parts = document.cookie.split(";").map((part) => part.trim()); const match = parts.find((part) => part.startsWith(`${AUTH_CSRF_COOKIE}=`)); if (!match) return null; return decodeURIComponent(match.slice(AUTH_CSRF_COOKIE.length + 1)); } catch { return null; } }