import type { Blocklist, BlocklistEcho, BlocklistInput, Client, ClientEdit, ClientPrefix, ClientPrefixInput, ForwardZone, ForwardZoneInput, Group, GroupInput, Health, LocalRecord, LocalRecordInput, LoginRequest, LoginResponse, LogoutResponse, LookupResult, PausePost, PauseState, Period, QueriesFilter, QueriesPage, Rule, RuleEcho, RuleInput, SettingsEnvelope, SettingsPatch, SourceStatus, StatsTimeseries, StatsTotals, Upstream, UpstreamEcho, UpstreamHealth, UpstreamInput, Version, } from "@/lib/types"; export class ApiError extends Error { readonly status: number; readonly retryAfter?: number; constructor(status: number, message: string, retryAfter?: number) { super(message); this.name = "ApiError"; this.status = status; this.retryAfter = retryAfter; } } async function toApiError(res: Response): Promise { let message = `HTTP ${res.status}`; try { const body: unknown = await res.json(); if (typeof body === "object" && body !== null && "error" in body && typeof body.error === "string") { message = body.error; } } catch { // Non-JSON error body; keep the status fallback. } let retryAfter: number | undefined; if (res.status === 429) { const header = res.headers.get("Retry-After"); const seconds = header === null ? NaN : Number(header); if (Number.isFinite(seconds) && seconds >= 0) retryAfter = seconds; } return new ApiError(res.status, message, retryAfter); } async function request(path: string, init?: { method?: string; body?: unknown }): Promise { const body = init?.body; const res = await fetch(path, { method: init?.method ?? "GET", credentials: "same-origin", headers: body !== undefined ? { "content-type": "application/json" } : undefined, body: body !== undefined ? JSON.stringify(body) : undefined, }); if (!res.ok) throw await toApiError(res); if (res.status === 204) return undefined as T; return (await res.json()) as T; } type QueryValue = string | number | boolean | undefined; function qs(params: Record): string { const search = new URLSearchParams(); for (const [key, value] of Object.entries(params)) { if (value !== undefined) search.set(key, String(value)); } const encoded = search.toString(); return encoded === "" ? "" : `?${encoded}`; } // Monitoring + meta export const getHealth = (): Promise => request("/api/health"); export const getVersion = (): Promise => request("/api/version"); // Auth export const login = (body: LoginRequest): Promise => request("/api/auth/login", { method: "POST", body }); export const logout = (): Promise => request("/api/auth/logout", { method: "POST", body: {} }); // Query log + stats export const getQueries = (filter: QueriesFilter = {}): Promise => request(`/api/queries${qs({ ...filter })}`); /** `EventSource` URL for the live stream; not a fetch route. */ export const liveQueriesUrl = "/api/queries/live"; export const getStats = (period?: Period): Promise => request(`/api/stats${qs({ period })}`); export const getStatsTimeseries = (period?: Period): Promise => request(`/api/stats/timeseries${qs({ period })}`); export const getLookup = (domain: string, groupId?: number): Promise => request(`/api/lookup${qs({ domain, group_id: groupId })}`); export const getUpstreamHealth = (): Promise => request("/api/upstream/health"); // Groups export const listGroups = async (): Promise => (await request<{ groups: Group[] }>("/api/groups")).groups; export const createGroup = (input: GroupInput): Promise => request("/api/groups", { method: "POST", body: input }); export const updateGroup = (id: number, input: GroupInput): Promise => request(`/api/groups/${id}`, { method: "PUT", body: input }); export const deleteGroup = (id: number): Promise => request(`/api/groups/${id}`, { method: "DELETE" }); export const getGroupSources = async (id: number): Promise => (await request<{ source_ids: number[] }>(`/api/groups/${id}/sources`)).source_ids; export const putGroupSources = async (id: number, sourceIds: number[]): Promise => ( await request<{ source_ids: number[] }>(`/api/groups/${id}/sources`, { method: "PUT", body: { source_ids: sourceIds }, }) ).source_ids; // Blocklists export const listBlocklists = async (): Promise => (await request<{ blocklists: Blocklist[] }>("/api/blocklists")).blocklists; export const createBlocklist = (input: BlocklistInput): Promise => request("/api/blocklists", { method: "POST", body: input }); export const updateBlocklistsNow = async (): Promise => (await request<{ sources: SourceStatus[] }>("/api/blocklists/update", { method: "POST", body: {} })).sources; export const updateBlocklist = (id: number, input: BlocklistInput): Promise => request(`/api/blocklists/${id}`, { method: "PUT", body: input }); export const deleteBlocklist = (id: number): Promise => request(`/api/blocklists/${id}`, { method: "DELETE" }); // Rules export const listRules = async (): Promise => (await request<{ rules: Rule[] }>("/api/rules")).rules; export const createRule = (input: RuleInput): Promise => request("/api/rules", { method: "POST", body: input }); export const updateRule = (id: number, input: RuleInput): Promise => request(`/api/rules/${id}`, { method: "PUT", body: input }); export const deleteRule = (id: number): Promise => request(`/api/rules/${id}`, { method: "DELETE" }); // Local records export const listLocalRecords = async (): Promise => (await request<{ local_records: LocalRecord[] }>("/api/local-records")).local_records; export const createLocalRecord = (input: LocalRecordInput): Promise => request("/api/local-records", { method: "POST", body: input }); export const updateLocalRecord = (id: number, input: LocalRecordInput): Promise => request(`/api/local-records/${id}`, { method: "PUT", body: input }); export const deleteLocalRecord = (id: number): Promise => request(`/api/local-records/${id}`, { method: "DELETE" }); // Forward zones export const listForwardZones = async (): Promise => (await request<{ forward_zones: ForwardZone[] }>("/api/forward-zones")).forward_zones; export const createForwardZone = (input: ForwardZoneInput): Promise => request("/api/forward-zones", { method: "POST", body: input }); export const updateForwardZone = (id: number, input: ForwardZoneInput): Promise => request(`/api/forward-zones/${id}`, { method: "PUT", body: input }); export const deleteForwardZone = (id: number): Promise => request(`/api/forward-zones/${id}`, { method: "DELETE" }); // Clients + prefixes export const listClients = async (): Promise => (await request<{ clients: Client[] }>("/api/clients")).clients; export const updateClient = (id: number, edit: ClientEdit): Promise => request(`/api/clients/${id}`, { method: "PUT", body: edit }); export const deleteClient = (id: number): Promise => request(`/api/clients/${id}`, { method: "DELETE" }); export const listClientPrefixes = async (): Promise => (await request<{ client_prefixes: ClientPrefix[] }>("/api/client-prefixes")).client_prefixes; export const putClientPrefixes = async (prefixes: ClientPrefixInput[]): Promise => ( await request<{ client_prefixes: ClientPrefix[] }>("/api/client-prefixes", { method: "PUT", body: { client_prefixes: prefixes }, }) ).client_prefixes; // Upstreams export const listUpstreams = async (): Promise => (await request<{ upstreams: Upstream[] }>("/api/upstreams")).upstreams; export const createUpstream = (input: UpstreamInput): Promise => request("/api/upstreams", { method: "POST", body: input }); export const updateUpstream = (id: number, input: UpstreamInput): Promise => request(`/api/upstreams/${id}`, { method: "PUT", body: input }); export const deleteUpstream = (id: number): Promise => request(`/api/upstreams/${id}`, { method: "DELETE" }); // Pause + settings export const getPause = (): Promise => request("/api/pause"); export const postPause = (body: PausePost): Promise => request("/api/pause", { method: "POST", body }); export const getSettings = (): Promise => request("/api/settings"); export const putSettings = (patch: SettingsPatch): Promise => request("/api/settings", { method: "PUT", body: patch });