// Client-side fetch helper with automatic Bearer token injection and 401 auto-refresh. export interface ApiResponse { data: T; meta: { timestamp: string; version: "v1" }; error: null | { message: string; code?: string }; } const TOKEN_KEY = "ledgerone_token"; const REFRESH_KEY = "ledgerone_refresh_token"; const USER_KEY = "ledgerone_user"; export function getStoredToken(): string { if (typeof window === "undefined") return ""; return localStorage.getItem(TOKEN_KEY) ?? ""; } export function getStoredUser(): T | null { if (typeof window === "undefined") return null; try { const raw = localStorage.getItem(USER_KEY); return raw ? (JSON.parse(raw) as T) : null; } catch { return null; } } export function storeAuthTokens(data: { accessToken: string; refreshToken: string; user: unknown; }): void { localStorage.setItem(TOKEN_KEY, data.accessToken); localStorage.setItem(REFRESH_KEY, data.refreshToken); localStorage.setItem(USER_KEY, JSON.stringify(data.user)); // Set a non-HttpOnly cookie so Next.js middleware can detect auth state document.cookie = "ledgerone_auth=1; path=/; max-age=2592000; SameSite=Lax"; } export function clearAuth(): void { localStorage.removeItem(TOKEN_KEY); localStorage.removeItem(REFRESH_KEY); localStorage.removeItem(USER_KEY); document.cookie = "ledgerone_auth=; path=/; max-age=0"; } async function tryRefresh(): Promise { const refreshToken = localStorage.getItem(REFRESH_KEY); if (!refreshToken) return null; try { const res = await fetch("/api/auth/refresh", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ refreshToken }), }); if (!res.ok) { clearAuth(); return null; } const payload = (await res.json()) as ApiResponse<{ accessToken: string; refreshToken: string; }>; if (payload.error || !payload.data?.accessToken) { clearAuth(); return null; } localStorage.setItem(TOKEN_KEY, payload.data.accessToken); localStorage.setItem(REFRESH_KEY, payload.data.refreshToken); return payload.data.accessToken; } catch { clearAuth(); return null; } } export async function apiFetch( path: string, options: RequestInit = {} ): Promise> { const token = getStoredToken(); const headers: Record = { ...(options.headers as Record), }; if (token) headers["Authorization"] = `Bearer ${token}`; if ( options.body && typeof options.body === "string" && !headers["Content-Type"] ) { headers["Content-Type"] = "application/json"; } let res = await fetch(path, { ...options, headers }); // Auto-refresh on 401 if (res.status === 401) { const newToken = await tryRefresh(); if (newToken) { headers["Authorization"] = `Bearer ${newToken}`; res = await fetch(path, { ...options, headers }); } else { if (typeof window !== "undefined") { window.location.href = "/login"; } return { data: null as T, meta: { timestamp: new Date().toISOString(), version: "v1" }, error: { message: "Session expired. Please sign in again." }, }; } } return res.json() as Promise>; }