462 lines
11 KiB
TypeScript
462 lines
11 KiB
TypeScript
const API_BASE = '/admin/api';
|
|
|
|
type ApiError = { error?: string };
|
|
|
|
async function request<T>(path: string, options: RequestInit = {}): Promise<T> {
|
|
const headers = new Headers(options.headers);
|
|
if (options.body && !headers.has('Content-Type')) {
|
|
headers.set('Content-Type', 'application/json');
|
|
}
|
|
|
|
const response = await fetch(`${API_BASE}${path}`, {
|
|
...options,
|
|
headers,
|
|
credentials: 'include',
|
|
});
|
|
|
|
const text = await response.text();
|
|
const data = text ? (JSON.parse(text) as T | ApiError) : undefined;
|
|
|
|
if (!response.ok) {
|
|
const err = (data as ApiError | undefined)?.error;
|
|
throw new Error(err || `Request failed (${response.status})`);
|
|
}
|
|
|
|
return data as T;
|
|
}
|
|
|
|
export type UserSummary = {
|
|
id: string;
|
|
email: string;
|
|
display_name?: string | null;
|
|
disabled: boolean;
|
|
super_admin: boolean;
|
|
created_at: string;
|
|
updated_at: string;
|
|
};
|
|
|
|
export type AuthUserSummary = {
|
|
id: string;
|
|
email: string;
|
|
display_name?: string | null;
|
|
super_admin: boolean;
|
|
};
|
|
|
|
export type AuthSession = {
|
|
user: AuthUserSummary;
|
|
permissions: string[];
|
|
console_access: boolean;
|
|
};
|
|
|
|
export type MembershipSummary = {
|
|
id: string;
|
|
role_id: string;
|
|
role_name: string;
|
|
scope_type: string;
|
|
scope_id: string;
|
|
created_at: string;
|
|
};
|
|
|
|
export type UserDetail = {
|
|
user: UserSummary;
|
|
memberships: MembershipSummary[];
|
|
};
|
|
|
|
export type RoleSummary = {
|
|
id: string;
|
|
name: string;
|
|
description?: string | null;
|
|
permissions: string[];
|
|
};
|
|
|
|
export type ControlPlaneSummary = {
|
|
id: string;
|
|
name: string;
|
|
base_url: string;
|
|
region?: string | null;
|
|
has_admin_token: boolean;
|
|
created_at: string;
|
|
updated_at: string;
|
|
};
|
|
|
|
export type NetworkSummary = {
|
|
id: string;
|
|
control_plane_id: string;
|
|
control_plane_name: string;
|
|
network_id: string;
|
|
name: string;
|
|
dns_domain?: string | null;
|
|
overlay_v4?: string | null;
|
|
overlay_v6?: string | null;
|
|
requires_approval: boolean;
|
|
created_at: string;
|
|
updated_at: string;
|
|
};
|
|
|
|
export type RouteInfo = {
|
|
prefix: string;
|
|
kind: string;
|
|
enabled: boolean;
|
|
mapped_prefix?: string | null;
|
|
};
|
|
|
|
export type NodeInfo = {
|
|
id: string;
|
|
name: string;
|
|
dns_name: string;
|
|
ipv4: string;
|
|
ipv6: string;
|
|
wg_public_key: string;
|
|
machine_public_key: string;
|
|
endpoints: string[];
|
|
tags: string[];
|
|
owner_user_id?: string | null;
|
|
owner_email?: string | null;
|
|
owner_is_admin?: boolean;
|
|
routes: RouteInfo[];
|
|
last_seen: number;
|
|
approved: boolean;
|
|
key_rotation_required?: boolean;
|
|
revoked?: boolean;
|
|
};
|
|
|
|
export type AdminNodesResponse = {
|
|
nodes: NodeInfo[];
|
|
};
|
|
|
|
export type EnrollmentToken = {
|
|
token: string;
|
|
expires_at: number;
|
|
uses_left: number;
|
|
tags: string[];
|
|
owner_user_id?: string | null;
|
|
owner_email?: string | null;
|
|
owner_is_admin?: boolean;
|
|
revoked_at?: number | null;
|
|
};
|
|
|
|
export type CreateNetworkResult = {
|
|
network: NetworkSummary;
|
|
bootstrap_token?: EnrollmentToken | null;
|
|
};
|
|
|
|
export type CreateTokenResponse = {
|
|
token: EnrollmentToken;
|
|
};
|
|
|
|
export type KeyPolicyResponse = {
|
|
policy: {
|
|
max_age_seconds?: number | null;
|
|
};
|
|
};
|
|
|
|
export type KeyHistoryResponse = {
|
|
node_id: string;
|
|
keys: {
|
|
key_type: string;
|
|
public_key: string;
|
|
created_at: number;
|
|
revoked_at?: number | null;
|
|
}[];
|
|
};
|
|
|
|
export type ApproveNodeResponse = {
|
|
node_id: string;
|
|
approved: boolean;
|
|
approved_at?: number | null;
|
|
};
|
|
|
|
export type RevokeNodeResponse = {
|
|
node_id: string;
|
|
revoked_at?: number | null;
|
|
};
|
|
|
|
export type KeyRotationResponse = {
|
|
node_id: string;
|
|
machine_public_key: string;
|
|
wg_public_key: string;
|
|
};
|
|
|
|
export type AuditEntry = {
|
|
id: string;
|
|
network_id?: string | null;
|
|
node_id?: string | null;
|
|
action: string;
|
|
timestamp: number;
|
|
detail?: unknown;
|
|
};
|
|
|
|
export type AuditLogResponse = {
|
|
entries: AuditEntry[];
|
|
};
|
|
|
|
export type AdminAuditEntry = {
|
|
id: string;
|
|
actor_user_id?: string | null;
|
|
actor_email?: string | null;
|
|
action: string;
|
|
target_type?: string | null;
|
|
target_id?: string | null;
|
|
metadata?: unknown;
|
|
created_at: string;
|
|
};
|
|
|
|
export type ProviderSummary = {
|
|
id: string;
|
|
name: string;
|
|
};
|
|
|
|
export async function getMe() {
|
|
return request<AuthSession>('/auth/me');
|
|
}
|
|
|
|
export async function login(email: string, password: string) {
|
|
return request<AuthSession>('/auth/login', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ email, password }),
|
|
});
|
|
}
|
|
|
|
export async function logout() {
|
|
return request<{ ok: boolean }>('/auth/logout', { method: 'POST' });
|
|
}
|
|
|
|
export async function listProviders() {
|
|
return request<ProviderSummary[]>('/auth/providers');
|
|
}
|
|
|
|
export async function listControlPlanes() {
|
|
return request<ControlPlaneSummary[]>('/control-planes');
|
|
}
|
|
|
|
export async function createControlPlane(payload: {
|
|
name: string;
|
|
base_url: string;
|
|
admin_token?: string;
|
|
region?: string;
|
|
}) {
|
|
return request<ControlPlaneSummary>('/control-planes', {
|
|
method: 'POST',
|
|
body: JSON.stringify(payload),
|
|
});
|
|
}
|
|
|
|
export async function updateControlPlane(id: string, payload: {
|
|
name?: string;
|
|
base_url?: string;
|
|
admin_token?: string;
|
|
clear_admin_token?: boolean;
|
|
region?: string;
|
|
}) {
|
|
return request<ControlPlaneSummary>(`/control-planes/${id}`, {
|
|
method: 'PUT',
|
|
body: JSON.stringify(payload),
|
|
});
|
|
}
|
|
|
|
export async function verifyControlPlane(id: string) {
|
|
return request<{ ok: boolean; status?: number; body?: string }>(
|
|
`/control-planes/${id}/verify`,
|
|
{ method: 'POST' },
|
|
);
|
|
}
|
|
|
|
export async function listNetworks() {
|
|
return request<NetworkSummary[]>('/networks');
|
|
}
|
|
|
|
export async function createNetwork(payload: {
|
|
control_plane_id: string;
|
|
name: string;
|
|
overlay_v4?: string;
|
|
overlay_v6?: string;
|
|
dns_domain?: string;
|
|
requires_approval?: boolean;
|
|
key_rotation_max_age_seconds?: number;
|
|
bootstrap_token_ttl_seconds?: number;
|
|
bootstrap_token_uses?: number;
|
|
bootstrap_token_tags?: string[];
|
|
}) {
|
|
return request<CreateNetworkResult>('/networks', {
|
|
method: 'POST',
|
|
body: JSON.stringify(payload),
|
|
});
|
|
}
|
|
|
|
export async function listNodes(networkId: string) {
|
|
return request<AdminNodesResponse>(`/networks/${networkId}/nodes`);
|
|
}
|
|
|
|
export async function approveNode(networkId: string, nodeId: string) {
|
|
return request<ApproveNodeResponse>(`/networks/${networkId}/nodes/${nodeId}/approve`, {
|
|
method: 'POST',
|
|
});
|
|
}
|
|
|
|
export async function revokeNode(networkId: string, nodeId: string) {
|
|
return request<RevokeNodeResponse>(`/networks/${networkId}/nodes/${nodeId}/revoke`, {
|
|
method: 'POST',
|
|
});
|
|
}
|
|
|
|
export async function rotateNodeKeys(
|
|
networkId: string,
|
|
nodeId: string,
|
|
payload: { machine_public_key?: string; wg_public_key?: string },
|
|
) {
|
|
return request<KeyRotationResponse>(`/networks/${networkId}/nodes/${nodeId}/rotate-keys`, {
|
|
method: 'POST',
|
|
body: JSON.stringify(payload),
|
|
});
|
|
}
|
|
|
|
export async function nodeKeys(networkId: string, nodeId: string) {
|
|
return request<KeyHistoryResponse>(`/networks/${networkId}/nodes/${nodeId}/keys`);
|
|
}
|
|
|
|
export async function createToken(networkId: string, payload: {
|
|
ttl_seconds: number;
|
|
uses: number;
|
|
tags: string[];
|
|
}) {
|
|
return request<CreateTokenResponse>(`/networks/${networkId}/tokens`, {
|
|
method: 'POST',
|
|
body: JSON.stringify(payload),
|
|
});
|
|
}
|
|
|
|
export async function revokeToken(networkId: string, token_id: string) {
|
|
return request<EnrollmentToken>(`/networks/${networkId}/tokens/revoke`, {
|
|
method: 'POST',
|
|
body: JSON.stringify({ token_id }),
|
|
});
|
|
}
|
|
|
|
export async function getAcl(networkId: string) {
|
|
return request<unknown>(`/networks/${networkId}/acl`);
|
|
}
|
|
|
|
export async function updateAcl(networkId: string, policy: unknown) {
|
|
return request<unknown>(`/networks/${networkId}/acl`, {
|
|
method: 'PUT',
|
|
body: JSON.stringify({ policy }),
|
|
});
|
|
}
|
|
|
|
export async function getKeyPolicy(networkId: string) {
|
|
return request<KeyPolicyResponse>(`/networks/${networkId}/key-policy`);
|
|
}
|
|
|
|
export async function updateKeyPolicy(networkId: string, payload: { max_age_seconds?: number | null }) {
|
|
return request<KeyPolicyResponse>(`/networks/${networkId}/key-policy`, {
|
|
method: 'PUT',
|
|
body: JSON.stringify(payload),
|
|
});
|
|
}
|
|
|
|
export async function listUsers() {
|
|
return request<UserSummary[]>('/users');
|
|
}
|
|
|
|
export async function getUser(userId: string) {
|
|
return request<UserDetail>(`/users/${userId}`);
|
|
}
|
|
|
|
export async function createUser(payload: {
|
|
email: string;
|
|
display_name?: string;
|
|
password: string;
|
|
role_id?: string;
|
|
super_admin?: boolean;
|
|
}) {
|
|
return request<UserDetail>('/users', {
|
|
method: 'POST',
|
|
body: JSON.stringify(payload),
|
|
});
|
|
}
|
|
|
|
export async function updateUser(userId: string, payload: {
|
|
display_name?: string | null;
|
|
disabled?: boolean;
|
|
super_admin?: boolean;
|
|
}) {
|
|
return request<UserSummary>(`/users/${userId}`, {
|
|
method: 'PUT',
|
|
body: JSON.stringify(payload),
|
|
});
|
|
}
|
|
|
|
export async function setUserPassword(userId: string, password: string) {
|
|
return request<{ ok: boolean }>(`/users/${userId}/password`, {
|
|
method: 'POST',
|
|
body: JSON.stringify({ password }),
|
|
});
|
|
}
|
|
|
|
export async function addMembership(userId: string, payload: {
|
|
role_id: string;
|
|
scope_type: string;
|
|
scope_id: string;
|
|
}) {
|
|
return request<MembershipSummary>(`/users/${userId}/memberships`, {
|
|
method: 'POST',
|
|
body: JSON.stringify(payload),
|
|
});
|
|
}
|
|
|
|
export async function deleteMembership(userId: string, membershipId: string) {
|
|
return request<{ ok: boolean }>(`/users/${userId}/memberships/${membershipId}`, {
|
|
method: 'DELETE',
|
|
});
|
|
}
|
|
|
|
export async function listRoles() {
|
|
return request<RoleSummary[]>('/users/roles');
|
|
}
|
|
|
|
export async function listAdminAudit(params: {
|
|
actor_user_id?: string;
|
|
action?: string;
|
|
limit?: number;
|
|
}) {
|
|
const search = new URLSearchParams();
|
|
if (params.actor_user_id) search.set('actor_user_id', params.actor_user_id);
|
|
if (params.action) search.set('action', params.action);
|
|
if (params.limit) search.set('limit', String(params.limit));
|
|
const query = search.toString();
|
|
return request<AdminAuditEntry[]>(`/audit${query ? `?${query}` : ''}`);
|
|
}
|
|
|
|
export async function listControlPlaneAudit(
|
|
controlPlaneId: string,
|
|
params: { network_id?: string; node_id?: string; limit?: number },
|
|
) {
|
|
const search = new URLSearchParams();
|
|
if (params.network_id) search.set('network_id', params.network_id);
|
|
if (params.node_id) search.set('node_id', params.node_id);
|
|
if (params.limit) search.set('limit', String(params.limit));
|
|
const query = search.toString();
|
|
return request<AuditLogResponse>(
|
|
`/audit/control-planes/${controlPlaneId}${query ? `?${query}` : ''}`,
|
|
);
|
|
}
|
|
|
|
export async function deleteNetwork(id: string): Promise<void> {
|
|
const res = await fetch(`${API_BASE}/networks/${id}`, {
|
|
method: 'DELETE',
|
|
credentials: 'include',
|
|
});
|
|
if (!res.ok) {
|
|
throw new Error(`Failed to delete network: ${res.statusText}`);
|
|
}
|
|
}
|
|
|
|
export async function deleteControlPlane(id: string): Promise<void> {
|
|
const res = await fetch(`${API_BASE}/control-planes/${id}`, {
|
|
method: 'DELETE',
|
|
credentials: 'include',
|
|
});
|
|
if (!res.ok) {
|
|
throw new Error(`Failed to delete control plane: ${res.statusText}`);
|
|
}
|
|
}
|