const TOKEN_KEY = "sac_token"; const ROLE_KEY = "sac_role"; export function getToken(): string | null { return localStorage.getItem(TOKEN_KEY); } export function getRole(): string | null { return localStorage.getItem(ROLE_KEY); } export function isAdmin(): boolean { return getRole() === "admin"; } export function setToken(token: string): void { localStorage.setItem(TOKEN_KEY, token); } export function setRole(role: string): void { localStorage.setItem(ROLE_KEY, role); } export function setAuth(token: string, role: string): void { setToken(token); setRole(role); } export function clearToken(): void { localStorage.removeItem(TOKEN_KEY); localStorage.removeItem(ROLE_KEY); } export class ApiError extends Error { status: number; constructor(message: string, status: number) { super(message); this.name = "ApiError"; this.status = status; } } export function redirectToErrorPage(status: 401 | 403 | 429): void { window.location.href = `/${status}`; } export async function apiFetch(path: string, init: RequestInit = {}): Promise { const headers = new Headers(init.headers); if (!headers.has("Content-Type") && init.body) { headers.set("Content-Type", "application/json"); } const token = getToken(); const hadToken = !!token; if (token) { headers.set("Authorization", `Bearer ${token}`); } const res = await fetch(path, { ...init, headers }); const text = await res.text(); if (res.status === 401) { if (hadToken) { clearToken(); redirectToErrorPage(401); } throw new ApiError(parseApiError(text) || "Unauthorized", 401); } if (res.status === 403) { if (hadToken) { redirectToErrorPage(403); } throw new ApiError(parseApiError(text) || "Forbidden", 403); } if (res.status === 429) { redirectToErrorPage(429); throw new ApiError(parseApiError(text) || "Too many requests", 429); } if (!res.ok) { throw new ApiError(parseApiError(text) || res.statusText, res.status); } if (!text) { return undefined as T; } return JSON.parse(text) as T; } export interface TokenResponse { access_token: string; token_type: string; username: string; role: string; } export interface MeResponse { username: string; role: string; } export function fetchMe(): Promise { return apiFetch("/api/v1/auth/me"); } export function parseApiError(text: string): string { if (!text) return ""; try { const data = JSON.parse(text) as { detail?: string | Array<{ msg?: string }> }; if (typeof data.detail === "string") return data.detail; if (Array.isArray(data.detail)) { return data.detail.map((item) => item.msg ?? String(item)).join("; "); } } catch { /* plain text */ } return text; } export interface UserSummary { id: number; username: string; role: string; is_active: boolean; created_at: string; } export interface UserListResponse { items: UserSummary[]; } export interface UpdateUserPayload { username?: string; role?: string; password?: string; is_active?: boolean; } export function fetchUsers(): Promise { return apiFetch("/api/v1/users"); } export function createUser(payload: { username: string; password: string; role: string; }): Promise { return apiFetch("/api/v1/users", { method: "POST", body: JSON.stringify(payload), }); } export function updateUser(userId: number, payload: UpdateUserPayload): Promise { return apiFetch(`/api/v1/users/${userId}`, { method: "PATCH", body: JSON.stringify(payload), }); } export interface EventSummary { id: number; event_id: string; host_id: number; hostname: string; display_name?: string | null; product_version?: string | null; occurred_at: string; received_at: string; category: string; type: string; severity: string; title: string; summary: string; actor_user?: string | null; } export interface EventListResponse { items: EventSummary[]; total: number; page: number; page_size: number; } export interface EventDetail extends EventSummary { details: Record | null; raw: Record | null; payload: Record; } export interface HostSummary { id: number; hostname: string; display_name: string | null; os_family: string; os_version?: string | null; product: string; product_version: string | null; ipv4: string | null; last_seen_at: string; event_count: number | null; last_heartbeat_at: string | null; last_daily_report_at: string | null; last_inventory_at?: string | null; agent_status: "online" | "stale" | "unknown"; } export interface HostDetail extends HostSummary { agent_instance_id: string | null; ipv6: string | null; tags: string[] | null; use_sac_mode: string | null; inventory: Record | null; inventory_updated_at: string | null; created_at: string | null; } export function fetchHost(id: number): Promise { return apiFetch(`/api/v1/hosts/${id}`); } export function fetchRecentEvents(limit = 1): Promise { return apiFetch(`/api/v1/dashboards/recent-events?limit=${limit}`); } export interface HostListResponse { items: HostSummary[]; total: number; page: number; page_size: number; latest_agent_versions?: Record; } export interface ProblemSummary { id: number; host_id: number | null; hostname: string | null; display_name?: string | null; title: string; summary: string; severity: string; status: string; rule_id: string | null; fingerprint?: string; event_count?: number; last_seen_at?: string; created_at: string; updated_at: string; } export interface ProblemListResponse { items: ProblemSummary[]; total: number; page: number; page_size: number; } export interface ProblemEventItem { id: number; event_id: string; occurred_at: string; type: string; severity: string; title: string; summary: string; } export interface ProblemDetail extends ProblemSummary { events: ProblemEventItem[]; } export function fetchProblem(id: number): Promise { return apiFetch(`/api/v1/problems/${id}`); } export async function ackProblem(problemId: number): Promise<{ id: number; status: string }> { return apiFetch(`/api/v1/problems/${problemId}/ack`, { method: "POST" }); } export async function resolveProblem(problemId: number): Promise<{ id: number; status: string }> { return apiFetch(`/api/v1/problems/${problemId}/resolve`, { method: "POST" }); } export interface TopHostItem { host_id: number; hostname: string; display_name?: string | null; count: number; } export interface TopTypeItem { type: string; count: number; } export interface MobileSettings { devices_allowed: boolean; max_devices_per_user: number; min_app_version: string | null; fcm_enabled: boolean; fcm_configured: boolean; source: string; } export interface MobileEnrollmentCode { id: number; label: string; code_prefix: string; target_user_id: number | null; target_username: string | null; login_mode: string; max_uses: number; use_count: number; expires_at: string; revoked_at: string | null; created_at: string; is_active: boolean; } export interface MobileEnrollmentCodeCreated extends MobileEnrollmentCode { enrollment_code: string; } export interface MobileDevice { id: number; user_id: number; username: string; device_uuid: string; display_name: string; platform: string; app_version: string | null; has_fcm_token: boolean; enrolled_at: string; last_seen_at: string | null; revoked_at: string | null; is_active: boolean; } export function fetchMobileSettings(): Promise { return apiFetch("/api/v1/mobile/settings"); } export function updateMobileSettings(payload: { devices_allowed: boolean; max_devices_per_user: number; min_app_version?: string | null; }): Promise { return apiFetch("/api/v1/mobile/settings", { method: "PUT", body: JSON.stringify(payload), }); } export function fetchMobileEnrollmentCodes(): Promise { return apiFetch("/api/v1/mobile/enrollment-codes"); } export function createMobileEnrollmentCode(payload: { label?: string; target_user_id?: number | null; login_mode?: string; max_uses?: number; expires_in_hours?: number; }): Promise { return apiFetch("/api/v1/mobile/enrollment-codes", { method: "POST", body: JSON.stringify(payload), }); } export function revokeMobileEnrollmentCode(codeId: number): Promise { return apiFetch(`/api/v1/mobile/enrollment-codes/${codeId}`, { method: "DELETE" }); } export function fetchMobileDevices(): Promise { return apiFetch("/api/v1/mobile/devices"); } export function revokeMobileDevice(deviceId: number): Promise { return apiFetch(`/api/v1/mobile/devices/${deviceId}/revoke`, { method: "POST" }); } export function deleteMobileDevice(deviceId: number): Promise { return apiFetch(`/api/v1/mobile/devices/${deviceId}`, { method: "DELETE" }); } export function testMobileDevicePush(deviceId: number): Promise<{ message: string }> { return apiFetch(`/api/v1/mobile/devices/${deviceId}/test-push`, { method: "POST" }); } export interface DashboardSummary { events_last_24h: number; hosts_total: number; hosts_stale: number; heartbeats_24h: number; daily_reports_24h: number; problems_open: number; problems_opened_24h: number; problems_resolved_24h: number; severity_24h: Record; top_hosts: TopHostItem[]; top_event_types: TopTypeItem[]; recent_events: EventSummary[]; }