448 lines
16 KiB
TypeScript
448 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 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),
|
|
}));
|
|
},
|
|
};
|