// Central API client for the identity platform web app. // The auth lifecycle (login/register/refresh/logout) is handled by the Next.js // Route Handlers under app/api/auth/*, which set HttpOnly cookies so the // server-side middleware can guard routes. Data calls attach a Bearer token // from localStorage (mirroring Hamsoo's apiclient token-storage pattern). export const ACCESS_TOKEN_KEY = "access_token"; export const REFRESH_TOKEN_KEY = "refresh_token"; export const SESSION_ID_KEY = "session_id"; // Same-origin proxy base for auth lifecycle calls (sets HttpOnly cookies). const AUTH_PROXY = "/api/auth"; export function getAccessToken(): string | null { if (typeof window === "undefined") return null; return localStorage.getItem(ACCESS_TOKEN_KEY); } export function getRefreshToken(): string | null { if (typeof window === "undefined") return null; return localStorage.getItem(REFRESH_TOKEN_KEY); } export function setTokens( access: string, refresh: string, sessionId?: string, ): void { if (typeof window === "undefined") return; localStorage.setItem(ACCESS_TOKEN_KEY, access); localStorage.setItem(REFRESH_TOKEN_KEY, refresh); if (sessionId) localStorage.setItem(SESSION_ID_KEY, sessionId); } export function clearTokens(): void { if (typeof window === "undefined") return; localStorage.removeItem(ACCESS_TOKEN_KEY); localStorage.removeItem(REFRESH_TOKEN_KEY); localStorage.removeItem(SESSION_ID_KEY); } function apiBase(): string { return process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000/api/v1"; } export interface ApiResult { ok: boolean; status: number; data: any; } export async function authLogin( email: string, password: string, mfaCode?: string, ): Promise { const res = await fetch(`${AUTH_PROXY}/login`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ email, password, mfa_code: mfaCode || "", }), }); const data = await res.json().catch(() => ({})); return { ok: res.ok, status: res.status, data }; } export async function authRegister(payload: { email: string; password: string; password_confirm: string; full_name?: string; username?: string; }): Promise { const res = await fetch(`${AUTH_PROXY}/register`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }); const data = await res.json().catch(() => ({})); return { ok: res.ok, status: res.status, data }; } export async function authLogout(refresh?: string | null): Promise { try { const res = await fetch(`${AUTH_PROXY}/logout`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ refresh: refresh || getRefreshToken() || "", logout_type: "global", }), }); return res.ok; } catch { return false; } } async function tryRefresh(): Promise { const refresh = getRefreshToken(); if (!refresh) { clearTokens(); return null; } try { const res = await fetch(`${AUTH_PROXY}/refresh`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ refresh }), }); if (!res.ok) { clearTokens(); return null; } const data = await res.json(); const access = data.access; if (!access) return null; setTokens(access, data.refresh ?? refresh, data.session_id); return access; } catch { clearTokens(); return null; } } export async function apiFetch( url: string, options: RequestInit = {}, ): Promise { const fullUrl = url.startsWith("http") ? url : `${apiBase()}${url}`; const token = getAccessToken(); const buildHeaders = (authToken?: string): Headers => { const headers = new Headers(options.headers); if (!headers.has("Content-Type")) { headers.set("Content-Type", "application/json"); } if (authToken) headers.set("Authorization", `Bearer ${authToken}`); return headers; }; let res = await fetch(fullUrl, { ...options, headers: buildHeaders(token || undefined), }); // Automatic refresh on 401 (single retry). if (res.status === 401) { const newToken = await tryRefresh(); if (newToken) { res = await fetch(fullUrl, { ...options, headers: buildHeaders(newToken), }); } } return res; } export async function fetchMe(): Promise { const res = await apiFetch("/auth/me/"); if (!res.ok) return null; return res.json(); }