import { createContext, useContext, useEffect, useState, type ReactNode } from 'react' import * as authApi from '../api/auth' import { ApiError, NetworkError } from '../api/client' import { clearLastUser, loadLastUser, saveLastUser } from '../lib/lastUser' import { unsubscribeFromPush } from '../lib/push' import { applyTextScale, applyTheme } from '../lib/theme' import type { User } from '../types' interface AuthContextValue { user: User | null loading: boolean offline: boolean login: (usernameOrEmail: string, password: string) => Promise logout: () => Promise updateUser: (user: User) => void } const AuthContext = createContext(undefined) export function AuthProvider({ children }: { children: ReactNode }) { const [user, setUser] = useState(null) const [loading, setLoading] = useState(true) const [offline, setOffline] = useState(false) useEffect(() => { applyTheme(user?.theme ?? null, user?.active_custom_theme?.colors ?? null) }, [user?.theme, user?.active_custom_theme]) useEffect(() => { applyTextScale(user?.text_scale ?? null) }, [user?.text_scale]) useEffect(() => { authApi .me() .then((u) => { setUser(u) setOffline(false) saveLastUser(u) }) .catch((err) => { // A confirmed 401 means the server is reachable and says "not // logged in" -- that's the only case that should actually clear the // cached identity. Everything else (a real network failure, or the // backend being down behind a reverse proxy that answers with its // own 502/503/504 -- both happen in production, not just literal // offline) means we simply couldn't get a confirmed answer. // /api/auth/me is NetworkOnly by design (never trust a stale // "who am I" as the *source of truth*), but treating "couldn't // check" the same as "confirmed logged out" would lock users out of // the cached rooms/messages entirely whenever the backend is // unreachable. Every real action still re-checks the actual session // cookie server-side, so falling back here can't grant anything. if (err instanceof ApiError && err.status === 401) { clearLastUser() return } if (!(err instanceof NetworkError)) { console.error('Failed to load current user', err) } const cached = loadLastUser() setUser(cached) setOffline(cached !== null) }) .finally(() => setLoading(false)) }, []) async function login(usernameOrEmail: string, password: string) { const u = await authApi.login(usernameOrEmail, password) setUser(u) setOffline(false) saveLastUser(u) } async function logout() { // Best-effort, and must run before the session cookie is cleared below // -- the unsubscribe call is authenticated. Otherwise this browser's // push subscription (both the server-side row and the registration // itself) outlives the session, so the account being logged out of // keeps silently receiving pushes for as long as this browser stays // installed/open, with no way for the user to tell why. try { await unsubscribeFromPush() } catch { // Not fatal -- logging out must still proceed even if this failed. } await authApi.logout() setUser(null) clearLastUser() } function updateUser(u: User) { setUser(u) saveLastUser(u) } return ( {children} ) } export function useAuth(): AuthContextValue { const ctx = useContext(AuthContext) if (!ctx) throw new Error('useAuth must be used within an AuthProvider') return ctx }