const jsonHeaders = { "Content-Type": "application/json" }; let onUnauthorized: (() => void) | null = null; export function setUnauthorizedHandler(fn: () => void) { onUnauthorized = fn; } async function handle(res: Response): Promise { if (res.status === 401) { onUnauthorized?.(); throw new Error("unauthorized"); } const data = await res.json().catch(() => ({})); if (!res.ok) throw new Error((data as { error?: string }).error || res.statusText); return data as T; } function fetchApi(path: string, init?: RequestInit) { return fetch(path, { credentials: "include", ...init }); } export interface MeResponse { logged_in: boolean; app_version?: string; org?: { name: string; logo: string }; user?: { id: number; name: string; email: string }; permissions?: string[]; } export interface Device { id: number; name: string; description?: string; ip_addresses?: IpOnDevice[]; tags?: Tag[]; custom_fields?: Record; } export interface IpOnDevice { id: number; ip: string; hostname?: string; subnet_id?: number; subnet_name?: string; cidr?: string; site?: string; notes?: string; } export interface Subnet { id: number; name: string; cidr: string; site?: string; vlan_id?: number; vlan_description?: string; vlan_notes?: string; utilization?: number; total_ips?: number; used_ips?: number; custom_fields?: Record; ip_addresses?: SubnetIp[]; } export interface SubnetIp { id: number; ip: string; hostname?: string; device_id?: number; device_name?: string; notes?: string; } export interface Tag { id: number; name: string; color?: string; description?: string; } export interface Rack { id: number; name: string; site: string; height_u: number; used_u?: number; percent_full?: number; devices?: RackDevice[]; site_devices?: { id: number; name: string; description?: string }[]; } export interface RackDevice { id: number; position_u: number; side: string; device_id?: number; device_name?: string; nonnet_device_name?: string; } export interface AuditEntry { id: number; user_name?: string; action: string; details?: string; timestamp?: string; } export interface UserRow { id: number; name: string; email: string; role_id?: number; role_name?: string; } export interface RoleRow { id: number; name: string; description?: string; require_2fa?: boolean; permissions?: { id: number; name: string; category?: string }[]; } export interface CustomFieldDef { id: number; entity_type: string; name: string; field_key: string; field_type: string; required?: boolean; display_order?: number; default_value?: string; help_text?: string; validation_rules?: { select_options?: string[] }; } export interface AuditParams { limit?: number; offset?: number; user?: string; action?: string; from?: string; to?: string; } export const api = { async me(): Promise { return handle(await fetchApi("/api/v2/auth/me")); }, async login(email: string, password: string) { return handle<{ ok?: boolean; requires_2fa?: boolean; requires_setup?: boolean }>( await fetchApi("/api/v2/auth/login", { method: "POST", headers: jsonHeaders, body: JSON.stringify({ email, password }), }), ); }, async verify2fa(code: string, useBackup = false) { return handle(await fetchApi("/api/v2/auth/verify-2fa", { method: "POST", headers: jsonHeaders, body: JSON.stringify({ code, use_backup: useBackup }), })); }, async setup2fa(action: "generate" | "verify", code?: string) { return handle<{ secret?: string; qr_code?: string; backup_codes?: string[] }>( await fetchApi("/api/v2/auth/setup-2fa", { method: "POST", headers: jsonHeaders, body: JSON.stringify({ action, code }), }), ); }, async logout() { return handle(await fetchApi("/api/v2/auth/logout", { method: "POST" })); }, async dashboard() { return handle<{ stats: { total_ips: number; used_ips: number; available_ips: number; utilization_percent: number; subnet_count: number; alerting_subnets: number; device_count: number; }; subnet_overview: { id: number; name: string; cidr: string; site: string; vlan_id?: number; utilization: number; available: number; status: "active" | "alerting"; }[]; activity: { hour: number; count: number }[]; }>(await fetchApi("/api/v2/dashboard")); }, async search(q: string) { return handle>(await fetchApi(`/api/v2/search?q=${encodeURIComponent(q)}`)); }, async devices(params?: { tag?: string; site?: string }) { const p = new URLSearchParams(); if (params?.tag) p.set("tag", params.tag); if (params?.site) p.set("site", params.site); const q = p.toString(); const d = await handle<{ items: Device[] }>(await fetchApi(`/api/v2/devices${q ? `?${q}` : ""}`)); return d.items; }, async device(id: number) { return handle(await fetchApi(`/api/v2/devices/${id}`)); }, async createDevice(body: Partial) { return handle(await fetchApi("/api/v2/devices", { method: "POST", headers: jsonHeaders, body: JSON.stringify(body) })); }, async updateDevice(id: number, body: Partial) { return handle(await fetchApi(`/api/v2/devices/${id}`, { method: "PUT", headers: jsonHeaders, body: JSON.stringify(body) })); }, async deleteDevice(id: number) { return handle(await fetchApi(`/api/v2/devices/${id}`, { method: "DELETE" })); }, async assignIp(deviceId: number, ipId: number) { return handle(await fetchApi(`/api/v2/devices/${deviceId}/ips`, { method: "POST", headers: jsonHeaders, body: JSON.stringify({ ip_id: ipId }), })); }, async removeIp(deviceId: number, ipId: number) { return handle(await fetchApi(`/api/v2/devices/${deviceId}/ips/${ipId}`, { method: "DELETE" })); }, async deviceIpHistory(deviceId: number) { const d = await handle<{ items: unknown[] }>(await fetchApi(`/api/v2/devices/${deviceId}/ip-history`)); return d.items; }, async subnets(includeUtil = true) { const d = await handle<{ items: Subnet[] }>( await fetchApi(`/api/v2/subnets${includeUtil ? "?include=utilization" : ""}`), ); return d.items; }, async subnet(id: number) { return handle(await fetchApi(`/api/v2/subnets/${id}`)); }, async createSubnet(body: Partial) { return handle(await fetchApi("/api/v2/subnets", { method: "POST", headers: jsonHeaders, body: JSON.stringify(body) })); }, async updateSubnet(id: number, body: Partial) { return handle(await fetchApi(`/api/v2/subnets/${id}`, { method: "PUT", headers: jsonHeaders, body: JSON.stringify(body) })); }, async deleteSubnet(id: number) { return handle(await fetchApi(`/api/v2/subnets/${id}`, { method: "DELETE" })); }, async availableIps(subnetId: number) { const d = await handle<{ items: { id: number; ip: string }[] }>(await fetchApi(`/api/v2/subnets/${subnetId}/available-ips`)); return d.items; }, async patchIpNotes(ipId: number, notes: string) { return handle(await fetchApi(`/api/v2/ip-addresses/${ipId}`, { method: "PATCH", headers: jsonHeaders, body: JSON.stringify({ notes }), })); }, async ipHistory(ip: string) { const d = await handle<{ items: unknown[] }>(await fetchApi(`/api/v2/ips/${encodeURIComponent(ip)}/history`)); return d.items; }, subnetExportUrl(id: number) { return `/api/v2/subnets/${id}/export`; }, async tags() { const d = await handle<{ items: Tag[] }>(await fetchApi("/api/v2/tags")); return d.items; }, async createTag(body: Partial) { return handle(await fetchApi("/api/v2/tags", { method: "POST", headers: jsonHeaders, body: JSON.stringify(body) })); }, async updateTag(id: number, body: Partial) { return handle(await fetchApi(`/api/v2/tags/${id}`, { method: "PUT", headers: jsonHeaders, body: JSON.stringify(body) })); }, async deleteTag(id: number) { return handle(await fetchApi(`/api/v2/tags/${id}`, { method: "DELETE" })); }, async assignTag(deviceId: number, tagId: number) { return handle(await fetchApi(`/api/v2/devices/${deviceId}/tags`, { method: "POST", headers: jsonHeaders, body: JSON.stringify({ tag_id: tagId }), })); }, async removeTag(deviceId: number, tagId: number) { return handle(await fetchApi(`/api/v2/devices/${deviceId}/tags/${tagId}`, { method: "DELETE" })); }, async racks() { const d = await handle<{ items: Rack[] }>(await fetchApi("/api/v2/racks")); return d.items; }, async rack(id: number) { return handle(await fetchApi(`/api/v2/racks/${id}`)); }, async createRack(body: Partial) { return handle(await fetchApi("/api/v2/racks", { method: "POST", headers: jsonHeaders, body: JSON.stringify(body) })); }, async deleteRack(id: number) { return handle(await fetchApi(`/api/v2/racks/${id}`, { method: "DELETE" })); }, async updateRack(id: number, body: Partial) { return handle(await fetchApi(`/api/v2/racks/${id}`, { method: "PUT", headers: jsonHeaders, body: JSON.stringify(body) })); }, async addRackDevice(rackId: number, body: { position_u: number; side: string; device_id?: number; nonnet_device_name?: string }) { return handle(await fetchApi(`/api/v2/racks/${rackId}/devices`, { method: "POST", headers: jsonHeaders, body: JSON.stringify(body) })); }, async removeRackDevice(rackId: number, rackDeviceId: number) { return handle(await fetchApi(`/api/v2/racks/${rackId}/devices/${rackDeviceId}`, { method: "DELETE" })); }, rackExportUrl(id: number) { return `/api/v2/racks/${id}/export`; }, async createCustomField(body: Record) { return handle(await fetchApi("/api/v2/custom_fields", { method: "POST", headers: jsonHeaders, body: JSON.stringify(body) })); }, async updateCustomField(id: number, body: Record) { return handle(await fetchApi(`/api/v2/custom_fields/${id}`, { method: "PUT", headers: jsonHeaders, body: JSON.stringify(body) })); }, async deleteCustomField(id: number) { return handle(await fetchApi(`/api/v2/custom_fields/${id}`, { method: "DELETE" })); }, async reorderCustomFields(entityType: string, fieldOrders: Record) { return handle(await fetchApi("/api/v2/custom-fields/reorder", { method: "POST", headers: jsonHeaders, body: JSON.stringify({ entity_type: entityType, field_orders: fieldOrders }), })); }, async createUser(body: { name: string; email: string; password: string; role_id?: number }) { return handle(await fetchApi("/api/v2/users", { method: "POST", headers: jsonHeaders, body: JSON.stringify(body) })); }, async updateUser(id: number, body: Record) { return handle(await fetchApi(`/api/v2/users/${id}`, { method: "PUT", headers: jsonHeaders, body: JSON.stringify(body) })); }, async deleteUser(id: number) { return handle(await fetchApi(`/api/v2/users/${id}`, { method: "DELETE" })); }, async regenerateApiKey(userId: number) { return handle<{ api_key: string }>(await fetchApi(`/api/v2/users/${userId}/regenerate-api-key`, { method: "POST" })); }, async createRole(body: { name: string; description?: string; permission_ids?: number[]; require_2fa?: boolean }) { return handle(await fetchApi("/api/v2/roles", { method: "POST", headers: jsonHeaders, body: JSON.stringify(body) })); }, async updateRole(id: number, body: Record) { return handle(await fetchApi(`/api/v2/roles/${id}`, { method: "PUT", headers: jsonHeaders, body: JSON.stringify(body) })); }, async deleteRole(id: number) { return handle(await fetchApi(`/api/v2/roles/${id}`, { method: "DELETE" })); }, async disable2fa(password: string) { return handle(await fetchApi("/api/v2/account/disable-2fa", { method: "POST", headers: jsonHeaders, body: JSON.stringify({ password }), })); }, async regenerateBackupCodes(password: string) { return handle<{ backup_codes: string[] }>(await fetchApi("/api/v2/account/regenerate-backup-codes", { method: "POST", headers: jsonHeaders, body: JSON.stringify({ password }), })); }, async audit(params: AuditParams = {}) { const p = new URLSearchParams(); if (params.limit != null) p.set("limit", String(params.limit)); if (params.offset != null) p.set("offset", String(params.offset)); if (params.user) p.set("user", params.user); if (params.action) p.set("action", params.action); if (params.from) p.set("from", params.from); if (params.to) p.set("to", params.to); const q = p.toString(); return handle<{ items: AuditEntry[]; total: number }>(await fetchApi(`/api/v2/audit${q ? `?${q}` : ""}`)); }, async auditActions() { const d = await handle<{ items: string[] }>(await fetchApi("/api/v2/audit/actions")); return d.items; }, auditExportUrl(params: AuditParams = {}) { const p = new URLSearchParams(); if (params.user) p.set("user", params.user); if (params.action) p.set("action", params.action); if (params.from) p.set("from", params.from); if (params.to) p.set("to", params.to); const q = p.toString(); return `/api/v2/audit/export${q ? `?${q}` : ""}`; }, async users() { const d = await handle<{ items: UserRow[] }>(await fetchApi("/api/v2/users")); return d.items; }, async roles() { const d = await handle<{ items: RoleRow[] }>(await fetchApi("/api/v2/roles")); return d.items; }, async permissions() { const d = await handle<{ items: { id: number; name: string; category?: string }[] }>( await fetchApi("/api/v2/permissions"), ); return d.items; }, async settings() { return handle<{ org_name: string; org_logo: string }>(await fetchApi("/api/v2/settings")); }, async updateSettings(body: { org_name: string; org_logo: string }) { return handle<{ org_name: string; org_logo: string; org?: { name: string; logo: string } }>( await fetchApi("/api/v2/settings", { method: "PUT", headers: jsonHeaders, body: JSON.stringify(body) }), ); }, async customFields(entityType: string) { const d = await handle<{ items: CustomFieldDef[] }>(await fetchApi(`/api/v2/custom_fields/${entityType}`)); return d.items; }, async patchDeviceCustomFields(deviceId: number, customFields: Record) { return handle(await fetchApi(`/api/v2/devices/${deviceId}/custom-fields`, { method: "PATCH", headers: jsonHeaders, body: JSON.stringify({ custom_fields: customFields }), })); }, async patchSubnetCustomFields(subnetId: number, customFields: Record) { return handle(await fetchApi(`/api/v2/subnets/${subnetId}/custom-fields`, { method: "PATCH", headers: jsonHeaders, body: JSON.stringify({ custom_fields: customFields }), })); }, async bulkAssignIps(deviceId: number, ipIds: number[]) { return handle(await fetchApi("/api/v2/bulk/assign-ips", { method: "POST", headers: jsonHeaders, body: JSON.stringify({ device_id: deviceId, ip_ids: ipIds }), })); }, async bulkCreateDevices(names: string[]) { return handle(await fetchApi("/api/v2/bulk/create-devices", { method: "POST", headers: jsonHeaders, body: JSON.stringify({ names }), })); }, async bulkAssignTags(deviceIds: number[], tagId: number) { return handle(await fetchApi("/api/v2/bulk/assign-tags", { method: "POST", headers: jsonHeaders, body: JSON.stringify({ device_ids: deviceIds, tag_id: tagId }), })); }, async account() { return handle(await fetchApi("/api/v2/account")); }, async changePassword(current: string, newPw: string) { return handle(await fetchApi("/api/v2/account/change-password", { method: "POST", headers: jsonHeaders, body: JSON.stringify({ current_password: current, new_password: newPw }), })); }, async getDhcp(subnetId: number) { return handle(await fetchApi(`/api/v2/subnets/${subnetId}/dhcp`)); }, async setDhcp(subnetId: number, body: unknown) { return handle(await fetchApi(`/api/v2/subnets/${subnetId}/dhcp`, { method: "POST", headers: jsonHeaders, body: JSON.stringify(body), })); }, };