lightscale-admin/frontend/src/api.ts
centra 98eb7057a5
Some checks failed
build-local-image / build (push) Has been cancelled
Implement user-bound join flows and add admin image build pipeline
2026-02-14 15:46:25 +09:00

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}`);
}
}