Add updateUser and deleteUser API client methods with PATCH and DELETE endpoints. Add updatePolicy and deletePolicy API client methods. Add email field to User type. Add Actions column to users and policies tables with Edit and Delete buttons. Implement inline edit forms for users and policies with state management for editing mode. Add update and delete mutations with query invalidation on success. Add error notices
188 lines
4.6 KiB
TypeScript
188 lines
4.6 KiB
TypeScript
const API_BASE = import.meta.env.VITE_API_BASE_URL ?? "/api/v1";
|
|
export const AUTH_EXPIRED_EVENT = "nexavpn-admin-auth-expired";
|
|
|
|
export type User = {
|
|
id: string;
|
|
username: string;
|
|
display_name: string;
|
|
email?: string;
|
|
role: string;
|
|
is_active: boolean;
|
|
};
|
|
|
|
export type Device = {
|
|
id: string;
|
|
name: string;
|
|
user_id?: string;
|
|
platform: string;
|
|
status: string;
|
|
assigned_ip?: string;
|
|
};
|
|
|
|
export type DeviceProfile = {
|
|
device: Device;
|
|
peer: {
|
|
assigned_ip: string;
|
|
dns_servers: string[];
|
|
allowed_ips: string[];
|
|
gateway: {
|
|
id: string;
|
|
name: string;
|
|
endpoint: string;
|
|
public_key: string;
|
|
};
|
|
profile_revision: number;
|
|
};
|
|
profile: {
|
|
format: string;
|
|
content: string;
|
|
};
|
|
resources: Array<{
|
|
type: string;
|
|
value: string;
|
|
label: string;
|
|
}>;
|
|
};
|
|
|
|
export type Policy = {
|
|
id: string;
|
|
name: string;
|
|
description: string;
|
|
priority: number;
|
|
effect: string;
|
|
full_tunnel: boolean;
|
|
is_active: boolean;
|
|
destinations?: string[];
|
|
};
|
|
|
|
export type Gateway = {
|
|
id: string;
|
|
name: string;
|
|
endpoint: string;
|
|
public_key: string;
|
|
listen_port: number;
|
|
vpn_cidr: string;
|
|
dns_servers: string[];
|
|
is_active: boolean;
|
|
};
|
|
|
|
export type AuditEvent = {
|
|
id: string;
|
|
event_type: string;
|
|
entity_type: string;
|
|
status: string;
|
|
message: string;
|
|
created_at: string;
|
|
};
|
|
|
|
async function request<T>(path: string, init?: RequestInit): Promise<T> {
|
|
const token = localStorage.getItem("nexavpn_admin_token");
|
|
const response = await fetch(`${API_BASE}${path}`, {
|
|
...init,
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
|
...(init?.headers ?? {})
|
|
}
|
|
});
|
|
|
|
if (!response.ok) {
|
|
if (response.status === 401) {
|
|
localStorage.removeItem("nexavpn_admin_token");
|
|
window.dispatchEvent(new Event(AUTH_EXPIRED_EVENT));
|
|
}
|
|
throw new Error(`Request failed: ${response.status}`);
|
|
}
|
|
|
|
return response.json() as Promise<T>;
|
|
}
|
|
|
|
export const api = {
|
|
users: () => request<User[]>("/admin/users"),
|
|
createUser: (payload: {
|
|
username: string;
|
|
display_name: string;
|
|
email: string;
|
|
password: string;
|
|
role: string;
|
|
}) =>
|
|
request<User>("/admin/users", {
|
|
method: "POST",
|
|
body: JSON.stringify(payload)
|
|
}),
|
|
updateUser: (userId: string, payload: {
|
|
display_name?: string;
|
|
email?: string;
|
|
role?: string;
|
|
password?: string;
|
|
is_active?: boolean;
|
|
}) =>
|
|
request<User>(`/admin/users/${userId}`, {
|
|
method: "PATCH",
|
|
body: JSON.stringify(payload)
|
|
}),
|
|
deleteUser: (userId: string) =>
|
|
request<{ ok: boolean }>(`/admin/users/${userId}`, {
|
|
method: "DELETE"
|
|
}),
|
|
devices: () => request<Device[]>("/admin/devices"),
|
|
deviceProfile: (deviceId: string) => request<DeviceProfile>(`/admin/devices/${deviceId}/profile`),
|
|
revokeDevice: (deviceId: string) =>
|
|
request<{ ok: boolean }>(`/admin/devices/${deviceId}/revoke`, {
|
|
method: "POST",
|
|
body: JSON.stringify({})
|
|
}),
|
|
rotateDevice: (deviceId: string) =>
|
|
request<{ ok: boolean }>(`/admin/devices/${deviceId}/rotate`, {
|
|
method: "POST",
|
|
body: JSON.stringify({})
|
|
}),
|
|
policies: () => request<Policy[]>("/admin/policies"),
|
|
createPolicy: (payload: {
|
|
name: string;
|
|
description: string;
|
|
priority: number;
|
|
effect: string;
|
|
full_tunnel: boolean;
|
|
destinations: string[];
|
|
targets: Array<{ type: string; id: string }>;
|
|
}) =>
|
|
request<Policy>("/admin/policies", {
|
|
method: "POST",
|
|
body: JSON.stringify(payload)
|
|
}),
|
|
updatePolicy: (policyId: string, payload: {
|
|
name?: string;
|
|
description?: string;
|
|
priority?: number;
|
|
effect?: string;
|
|
full_tunnel?: boolean;
|
|
is_active?: boolean;
|
|
destinations?: string[];
|
|
targets?: Array<{ type: string; id: string }>;
|
|
}) =>
|
|
request<Policy>(`/admin/policies/${policyId}`, {
|
|
method: "PATCH",
|
|
body: JSON.stringify(payload)
|
|
}),
|
|
deletePolicy: (policyId: string) =>
|
|
request<{ ok: boolean }>(`/admin/policies/${policyId}`, {
|
|
method: "DELETE"
|
|
}),
|
|
gateways: () => request<Gateway[]>("/admin/gateways"),
|
|
updateGateway: (gatewayId: string, payload: {
|
|
endpoint: string;
|
|
public_key: string;
|
|
listen_port: number;
|
|
vpn_cidr: string;
|
|
dns_servers: string[];
|
|
is_active: boolean;
|
|
}) =>
|
|
request<Gateway>(`/admin/gateways/${gatewayId}`, {
|
|
method: "PATCH",
|
|
body: JSON.stringify(payload)
|
|
}),
|
|
gatewaySync: (gatewayId: string) => request<unknown>(`/admin/gateways/${gatewayId}/sync`),
|
|
audit: () => request<AuditEvent[]>("/admin/audit-logs")
|
|
};
|