// Thin API client for the operator/admin UI. // // Auth is cookie-based: the JWT lives in an HttpOnly cookie the browser sends // automatically (credentials: 'include'). For mutations we echo the readable // CSRF cookie back in the X-CSRF-Token header (double-submit). See // wiki/entities/local-jwt-auth.md. const CSRF_COOKIE = "parking_csrf"; const CSRF_HEADER = "X-CSRF-Token"; function readCookie(name: string): string | null { const m = document.cookie.match(new RegExp(`(?:^|; )${name}=([^;]*)`)); return m ? decodeURIComponent(m[1]!) : null; } /** fetch wrapper: sends cookies, adds CSRF header on mutations, parses errors. */ export async function apiFetch(path: string, init: RequestInit = {}): Promise { const method = (init.method ?? "GET").toUpperCase(); const headers = new Headers(init.headers); if (init.body && !headers.has("content-type")) { headers.set("content-type", "application/json"); } if (method !== "GET" && method !== "HEAD") { const csrf = readCookie(CSRF_COOKIE); if (csrf) headers.set(CSRF_HEADER, csrf); } const res = await fetch(path, { ...init, headers, credentials: "include" }); if (!res.ok) { const msg = (await res.json().catch(() => ({}))) as { error?: string }; throw new ApiError(msg.error ?? `${path}: ${res.status}`, res.status); } if (res.status === 204) return undefined as T; return res.json() as Promise; } export class ApiError extends Error { constructor( message: string, readonly status: number, ) { super(message); } } // --- Auth ----------------------------------------------------------------- export type Role = "admin" | "operator" | "cashier" | "readonly"; export interface SessionUser { id: string; username: string; role: Role; } export function login(username: string, password: string): Promise { return apiFetch("/api/auth/login", { method: "POST", body: JSON.stringify({ username, password }), }); } export function logout(): Promise<{ ok: boolean }> { return apiFetch("/api/auth/logout", { method: "POST" }); } /** Returns the current user, or null if not authenticated. */ export async function fetchMe(): Promise { try { return await apiFetch("/api/auth/me"); } catch (e) { if (e instanceof ApiError && (e.status === 401 || e.status === 403)) return null; throw e; } } // --- Device setup --------------------------------------------------------- export interface ConfigField { key: string; label: string; type: "string" | "number" | "boolean" | "host" | "port" | "secret" | "select"; required: boolean; default?: string | number | boolean; options?: { value: string; label: string }[]; help?: string; } export interface CatalogEntry { id: string; label: string; description: string; transports: string[]; configFields: ConfigField[]; } export type DeviceCategory = "access" | "reader" | "camera" | "printer"; export type Catalog = Record & { /** Driver ids that support LAN discovery. */ discoverable: string[]; }; export function fetchCatalog(): Promise { return apiFetch("/api/setup/catalog"); } export interface DiscoveredDevice { id: string; label: string; config: Record; info?: Record; health: { status: string; detail?: string }; } /** Scan the LAN for devices a driver can discover. Admin-only. */ export async function discoverDevices(driverId: string): Promise { const body = await apiFetch<{ devices: DiscoveredDevice[] }>( `/api/setup/discover/${driverId}`, ); return body.devices; } export type DeviceConfig = Record; export interface TestResult { health: { status: string; detail?: string }; preconditions: { ok: boolean; issues: { key: string; message: string; fixable: boolean }[]; }; } /** Test a device config (reachability + preconditions) without saving. */ export function testDevice(driverId: string, config: DeviceConfig): Promise { return apiFetch("/api/setup/test", { method: "POST", body: JSON.stringify({ driverId, config }), }); } export interface AssignBody { lane: number; category: DeviceCategory; driverId: string; config: DeviceConfig; } /** Save + configure the device (preconditions, push setup), then persist. */ export function assignDevice(body: AssignBody): Promise<{ id: string }> { return apiFetch("/api/setup/assign", { method: "POST", body: JSON.stringify(body) }); }