Files
ipam/frontend/src/api.ts
T
2026-05-30 14:31:01 +00:00

456 lines
16 KiB
TypeScript

const jsonHeaders = { "Content-Type": "application/json" };
let onUnauthorized: (() => void) | null = null;
export function setUnauthorizedHandler(fn: () => void) {
onUnauthorized = fn;
}
async function handle<T>(res: Response): Promise<T> {
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<string, unknown>;
}
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<string, unknown>;
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<MeResponse> {
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<Record<string, unknown[]>>(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<Device>(await fetchApi(`/api/v2/devices/${id}`));
},
async createDevice(body: Partial<Device>) {
return handle(await fetchApi("/api/v2/devices", { method: "POST", headers: jsonHeaders, body: JSON.stringify(body) }));
},
async updateDevice(id: number, body: Partial<Device>) {
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<Subnet>(await fetchApi(`/api/v2/subnets/${id}`));
},
async createSubnet(body: Partial<Subnet>) {
return handle(await fetchApi("/api/v2/subnets", { method: "POST", headers: jsonHeaders, body: JSON.stringify(body) }));
},
async updateSubnet(id: number, body: Partial<Subnet>) {
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<Tag>) {
return handle(await fetchApi("/api/v2/tags", { method: "POST", headers: jsonHeaders, body: JSON.stringify(body) }));
},
async updateTag(id: number, body: Partial<Tag>) {
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<Rack>(await fetchApi(`/api/v2/racks/${id}`));
},
async createRack(body: Partial<Rack>) {
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<Rack>) {
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<string, unknown>) {
return handle(await fetchApi("/api/v2/custom_fields", { method: "POST", headers: jsonHeaders, body: JSON.stringify(body) }));
},
async updateCustomField(id: number, body: Record<string, unknown>) {
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<number, number>) {
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<string, unknown>) {
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<string, unknown>) {
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<string, unknown>) {
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<string, unknown>) {
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),
}));
},
};