const TOKEN_KEY = "control_token"; const USER_KEY = "control_user"; export function getToken(): string | null { return localStorage.getItem(TOKEN_KEY); } export function getUsername(): string | null { return localStorage.getItem(USER_KEY); } export function setSession(token: string, username: string) { localStorage.setItem(TOKEN_KEY, token); localStorage.setItem(USER_KEY, username); } export function clearSession() { localStorage.removeItem(TOKEN_KEY); localStorage.removeItem(USER_KEY); } export async function apiFetch( path: string, options: RequestInit = {}, ): Promise { const headers = new Headers(options.headers || {}); if (!headers.has("Content-Type") && options.body) { headers.set("Content-Type", "application/json"); } const token = getToken(); if (token) headers.set("Authorization", `Bearer ${token}`); const res = await fetch(path, { ...options, headers }); const text = await res.text(); let data: unknown = null; try { data = text ? JSON.parse(text) : null; } catch { data = { detail: text }; } if (res.status === 401) { // 仅中控自身鉴权失败才清会话;勿把策略机错误当成掉登录 clearSession(); const detail = typeof data === "object" && data && "detail" in data ? String((data as { detail: unknown }).detail) : "unauthorized"; throw new Error(detail || "unauthorized"); } if (!res.ok) { const detail = typeof data === "object" && data && "detail" in data ? String((data as { detail: unknown }).detail) : res.statusText; throw new Error(detail || `HTTP ${res.status}`); } return data as T; } export async function login(username: string, password: string) { const res = await apiFetch<{ token: string; username: string }>( "/api/auth/login", { method: "POST", body: JSON.stringify({ username, password }), }, ); setSession(res.token, res.username); return res; } export type NodeCard = { id: number; name: string; base_url: string; token_configured: boolean; online?: boolean; health?: Record | null; fleet?: Record | null; fleet_ok?: boolean; fleet_error?: string | null; error?: string | null; };