1"use client"; 2 3/** 4 * Client-side auth state. 5 * 6 * - The **access token** lives in memory only (never localStorage) — it is a 7 * short-lived JWT and memory storage keeps it out of XSS-extractable 8 * storage (security-architecture.md §AuthN). On load it is re-issued from 9 * the httpOnly refresh cookie via POST /auth/refresh (silent refresh), and 10 * the user profile is re-fetched with GET /auth/me (Bearer). 11 * - The **refresh token** is the httpOnly `factory_refresh` cookie set by the 12 * API (never readable from JS); /auth/refresh rotates it server-side and 13 * re-sets the cookie (the API must persist the rotation — see S10). 14 * - `hasSession()` is the in-memory token check (the cookie is HttpOnly and 15 * unreachable from JS — the old `document.cookie` read could only ever 16 * match the wrong cookie name and was always false, W2). The middleware is 17 * the authoritative redirect; this gates first-mount render + the 18 * silent-refresh query. 19 */ 20import { create } from "zustand"; 21import type { User } from "./types"; 22 23interface AuthState { 24 user: User | null; 25 accessToken: string | null; 26 /** True while /auth/me is resolving on first mount. */ 27 initializing: boolean; 28 setUser: (user: User | null, accessToken?: string | null) => void; 29 setAccessToken: (token: string | null) => void; 30 setInitializing: (v: boolean) => void; 31 clear: () => void; 32} 33 34export const useAuthStore = create<AuthState>()((set) => ({ 35 user: null, 36 accessToken: null, 37 initializing: true, 38 setUser: (user, accessToken = null) => set({ user, accessToken, initializing: false }), 39 setAccessToken: (accessToken) => set({ accessToken }), 40 setInitializing: (initializing) => set({ initializing }), 41 // Logout is an explicit sign-out: also end any in-flight initialization so 42 // the (app) layout can't keep spinning on a stale `initializing: true` if 43 // logout fires before the bootstrap settled. 44 clear: () => set({ user: null, accessToken: null, initializing: false }) 45})); 46 47/** 48 * Hydrated session present? The access token is the only session surface JS 49 * can see (the refresh cookie is HttpOnly). False during SSR — the store is 50 * empty on the server by construction, and `use-auth.ts` enables the 51 * silent-refresh query on client mount instead. 52 */ 53export function hasSession(): boolean { 54 return Boolean(useAuthStore.getState().accessToken); 55} 56 57/** Sync helpers for the api client (non-hook access from plain modules). */ 58export const authStore = { 59 get user(): User | null { 60 return useAuthStore.getState().user; 61 }, 62 get accessToken(): string | null { 63 return useAuthStore.getState().accessToken; 64 }, 65 setUser(user: User | null, accessToken: string | null = null): void { 66 useAuthStore.getState().setUser(user, accessToken); 67 }, 68 setAccessToken(token: string | null): void { 69 useAuthStore.getState().setAccessToken(token); 70 }, 71 setInitializing(v: boolean): void { 72 useAuthStore.getState().setInitializing(v); 73 }, 74 clear(): void { 75 useAuthStore.getState().clear(); 76 } 77}; 78