151 lines
7.3 KiB
TypeScript
151 lines
7.3 KiB
TypeScript
import type {
|
|
Agent,
|
|
AdminUser,
|
|
AuditLog,
|
|
ChangeRequest,
|
|
GlobalSettings,
|
|
Notification,
|
|
RequestState,
|
|
User,
|
|
} from "./types";
|
|
|
|
export class ApiError extends Error {
|
|
status: number;
|
|
data?: unknown;
|
|
constructor(message: string, status: number, data?: unknown) {
|
|
super(message);
|
|
this.status = status;
|
|
this.data = data;
|
|
}
|
|
}
|
|
|
|
async function api<T = unknown>(path: string, opts: RequestInit = {}): Promise<T> {
|
|
const res = await fetch(path, {
|
|
credentials: "include",
|
|
headers:
|
|
opts.body && !(opts.headers && "Content-Type" in opts.headers)
|
|
? { "Content-Type": "application/json", ...(opts.headers || {}) }
|
|
: opts.headers,
|
|
...opts,
|
|
});
|
|
let body: any = null;
|
|
try {
|
|
body = await res.json();
|
|
} catch {
|
|
/* no body */
|
|
}
|
|
if (!res.ok || body?.status === "FAILED") {
|
|
throw new ApiError(body?.message || `Request failed (${res.status})`, res.status, body?.data);
|
|
}
|
|
return (body?.data ?? body) as T;
|
|
}
|
|
|
|
// ── Auth ──────────────────────────────────────────────────────────────────────
|
|
export const auth = {
|
|
me: () => api<User>("/api/auth/me"),
|
|
login: (username: string, password: string, totp?: string) =>
|
|
api<User>("/api/auth/login", { method: "POST", body: JSON.stringify({ username, password, totp }) }),
|
|
register: (username: string, display_name: string, password: string) =>
|
|
api<User>("/api/auth/register", {
|
|
method: "POST",
|
|
body: JSON.stringify({ username, display_name, password }),
|
|
}),
|
|
logout: () => api("/api/auth/logout", { method: "POST" }),
|
|
setup2fa: () => api<{ secret: string; otpauth_url: string }>("/api/auth/2fa/setup", { method: "POST" }),
|
|
enable2fa: (totp: string) => api("/api/auth/2fa/enable", { method: "POST", body: JSON.stringify({ totp }) }),
|
|
disable2fa: (password: string) =>
|
|
api("/api/auth/2fa/disable", { method: "POST", body: JSON.stringify({ password }) }),
|
|
};
|
|
|
|
// ── Account ────────────────────────────────────────────────────────────────────
|
|
export const account = {
|
|
updateProfile: (display_name: string) =>
|
|
api("/api/account/profile", { method: "PATCH", body: JSON.stringify({ display_name }) }),
|
|
changePassword: (current_password: string, new_password: string) =>
|
|
api("/api/account/password", {
|
|
method: "POST",
|
|
body: JSON.stringify({ current_password, new_password }),
|
|
}),
|
|
updateSettings: (settings: { auto_delete_enabled?: boolean; auto_delete_days?: number }) =>
|
|
api<{ auto_delete_enabled: boolean; auto_delete_days: number }>("/api/account/settings", {
|
|
method: "PATCH",
|
|
body: JSON.stringify(settings),
|
|
}),
|
|
deleteAccount: (password: string) =>
|
|
api("/api/account", { method: "DELETE", body: JSON.stringify({ password, confirm: "DELETE" }) }),
|
|
exportUrl: "/api/account/export",
|
|
};
|
|
|
|
// ── Agents ─────────────────────────────────────────────────────────────────────
|
|
export const agents = {
|
|
list: () => api<Agent[]>("/api/agents"),
|
|
create: (input: Partial<Agent>) => api<Agent>("/api/agents", { method: "POST", body: JSON.stringify(input) }),
|
|
update: (id: number, input: Partial<Agent>) =>
|
|
api<Agent>(`/api/agents/${id}`, { method: "PATCH", body: JSON.stringify(input) }),
|
|
regenerateKey: (id: number) => api<Agent>(`/api/agents/${id}/regenerate-key`, { method: "POST" }),
|
|
setDisabled: (id: number, disabled: boolean) =>
|
|
api<Agent>(`/api/agents/${id}/disabled`, { method: "POST", body: JSON.stringify({ disabled }) }),
|
|
remove: (id: number) => api(`/api/agents/${id}`, { method: "DELETE" }),
|
|
};
|
|
|
|
// ── Change requests (human) ──────────────────────────────────────────────────────
|
|
export const requests = {
|
|
list: (params: { page?: number; page_size?: number; state?: string; agent_id?: number } = {}) => {
|
|
const q = new URLSearchParams();
|
|
if (params.page) q.set("page", String(params.page));
|
|
if (params.page_size) q.set("page_size", String(params.page_size));
|
|
if (params.state) q.set("state", params.state);
|
|
if (params.agent_id) q.set("agent_id", String(params.agent_id));
|
|
return api<{ page: number; page_size: number; total: number; requests: ChangeRequest[] }>(
|
|
`/api/change-requests?${q.toString()}`,
|
|
);
|
|
},
|
|
summary: () => api<{ counts: Record<RequestState, number> }>("/api/change-requests/summary"),
|
|
get: (id: string) => api<ChangeRequest>(`/api/change-requests/${id}`),
|
|
decide: (id: string, decision: "APPROVE" | "REJECT" | "REQUEST_CHANGES", comment?: string) =>
|
|
api<ChangeRequest>(`/api/change-requests/${id}/decision`, {
|
|
method: "POST",
|
|
body: JSON.stringify({ decision, comment }),
|
|
}),
|
|
};
|
|
|
|
// ── Notifications ────────────────────────────────────────────────────────────────
|
|
export const notifications = {
|
|
list: (params: { page?: number; page_size?: number; unread?: boolean } = {}) => {
|
|
const q = new URLSearchParams();
|
|
if (params.page) q.set("page", String(params.page));
|
|
if (params.page_size) q.set("page_size", String(params.page_size));
|
|
if (params.unread) q.set("unread", "true");
|
|
return api<{ total: number; unread_count: number; notifications: Notification[] }>(
|
|
`/api/notifications?${q.toString()}`,
|
|
);
|
|
},
|
|
markRead: (id: number) => api(`/api/notifications/${id}/read`, { method: "POST" }),
|
|
markAllRead: () => api("/api/notifications/read-all", { method: "POST" }),
|
|
clear: () => api("/api/notifications", { method: "DELETE" }),
|
|
};
|
|
|
|
// ── Global ───────────────────────────────────────────────────────────────────────
|
|
export const global = {
|
|
get: () => api<GlobalSettings>("/api/global"),
|
|
};
|
|
|
|
// ── Admin ─────────────────────────────────────────────────────────────────────────
|
|
export const admin = {
|
|
users: () => api<AdminUser[]>("/api/admin/users"),
|
|
updateUser: (id: number, input: { role?: "ADMIN" | "USER"; disabled?: boolean }) =>
|
|
api(`/api/admin/users/${id}`, { method: "PATCH", body: JSON.stringify(input) }),
|
|
deleteUser: (id: number) => api(`/api/admin/users/${id}`, { method: "DELETE" }),
|
|
settings: () => api<GlobalSettings>("/api/admin/settings"),
|
|
updateSettings: (input: { registration_enabled?: boolean; requests_enabled?: boolean }) =>
|
|
api<GlobalSettings>("/api/admin/settings", { method: "PATCH", body: JSON.stringify(input) }),
|
|
agents: () => api<(Agent & { owner: { id: number; username: string } })[]>("/api/admin/agents"),
|
|
setAgentDisabled: (id: number, disabled: boolean) =>
|
|
api(`/api/admin/agents/${id}/disabled`, { method: "POST", body: JSON.stringify({ disabled }) }),
|
|
deleteAgent: (id: number) => api(`/api/admin/agents/${id}`, { method: "DELETE" }),
|
|
auditLogs: (page = 1) =>
|
|
api<{ total: number; page: number; page_size: number; logs: AuditLog[] }>(
|
|
`/api/admin/audit-logs?page=${page}`,
|
|
),
|
|
};
|