feat: SSH keys management, backups fixes, instances/nodes/templates improvements, dev mode support
Some checks failed
Backend Tests / pytest (Python 3.12) (push) Has been cancelled
Some checks failed
Backend Tests / pytest (Python 3.12) (push) Has been cancelled
This commit is contained in:
@@ -19,8 +19,13 @@ export function useBackupStorages(serverId: number) {
|
||||
export function useBackupList(serverId: number, node: string, storage: string) {
|
||||
return useQuery({
|
||||
queryKey: backupKeys.list(serverId, node, storage),
|
||||
queryFn: () => apiClient.get<{ backups: unknown[] }>(`/proxmox/api/backups/list/${serverId}?node=${node}&storage=${storage}`),
|
||||
enabled: serverId > 0 && !!node && !!storage,
|
||||
queryFn: () => {
|
||||
const params = new URLSearchParams();
|
||||
params.set('storage', storage);
|
||||
if (node) params.set('node', node);
|
||||
return apiClient.get<{ backups: unknown[] }>(`/proxmox/api/backups/list/${serverId}?${params.toString()}`);
|
||||
},
|
||||
enabled: serverId > 0 && !!storage,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -44,7 +49,11 @@ export function useDeleteBackup() {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (data: { server_id: number; node: string; storage: string; volid: string }) =>
|
||||
apiClient.delete(`/proxmox/api/backups/${data.server_id}/backup`),
|
||||
apiClient.delete(`/proxmox/api/backups/${data.server_id}/backup`, {
|
||||
node: data.node,
|
||||
storage: data.storage,
|
||||
volid: data.volid,
|
||||
}),
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: ['backups'] }),
|
||||
});
|
||||
}
|
||||
@@ -56,6 +65,48 @@ export function useRestoreBackup() {
|
||||
});
|
||||
}
|
||||
|
||||
export type BackupJobInput = {
|
||||
server_id: number;
|
||||
node: string;
|
||||
vmids: (number | string)[];
|
||||
storage: string;
|
||||
mode?: string;
|
||||
compress?: string;
|
||||
notes?: string;
|
||||
keep_last?: number;
|
||||
keep_daily?: number;
|
||||
keep_weekly?: number;
|
||||
keep_monthly?: number;
|
||||
cron_expression: string;
|
||||
enabled?: boolean;
|
||||
};
|
||||
|
||||
export function useCreateBackupJob() {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (data: BackupJobInput) =>
|
||||
apiClient.post<{ success: boolean; job: unknown }>('/proxmox/api/backups/jobs', data),
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: backupKeys.jobs }),
|
||||
});
|
||||
}
|
||||
|
||||
export function useUpdateBackupJob() {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: ({ id, data }: { id: number; data: Partial<BackupJobInput> }) =>
|
||||
apiClient.put<{ success: boolean; job: unknown }>(`/proxmox/api/backups/jobs/${id}`, data),
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: backupKeys.jobs }),
|
||||
});
|
||||
}
|
||||
|
||||
export function useDeleteBackupJob() {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (jobId: number) => apiClient.delete<{ success: boolean }>(`/proxmox/api/backups/jobs/${jobId}`),
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: backupKeys.jobs }),
|
||||
});
|
||||
}
|
||||
|
||||
export function useToggleBackupJob() {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { useEffect } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { apiClient } from '@/lib/api-client';
|
||||
import { getTasksWebSocket } from '@/lib/websocket';
|
||||
import type { VMInstance, Snapshot, VMConfig } from '@/types';
|
||||
@@ -18,21 +18,25 @@ export const vmKeys = {
|
||||
};
|
||||
|
||||
// ==================== VM List ====================
|
||||
// No HTTP polling: live metrics arrive via WebSocket (`useInstancesMetricsSync`).
|
||||
// We still refetch on reconnect/focus to recover from missed events.
|
||||
export function useVirtualMachines() {
|
||||
return useQuery({
|
||||
queryKey: vmKeys.all,
|
||||
queryFn: () => apiClient.get<VMInstance[]>('/proxmox/api/virtual-machines'),
|
||||
refetchInterval: 30000,
|
||||
refetchInterval: false,
|
||||
refetchOnWindowFocus: false,
|
||||
staleTime: Infinity,
|
||||
});
|
||||
}
|
||||
|
||||
/** Subscribes to WebSocket vm_status_update events and patches the VM list cache in real time. */
|
||||
/** Subscribes to WebSocket vm_status_update / vm_deleted / vm_created events and patches the VM list cache in real time. */
|
||||
export function useVMStatusSync() {
|
||||
const qc = useQueryClient();
|
||||
useEffect(() => {
|
||||
const ws = getTasksWebSocket();
|
||||
ws.connect();
|
||||
const unsub = ws.onType('vm_status_update', (data: unknown) => {
|
||||
const unsubStatus = ws.onType('vm_status_update', (data: unknown) => {
|
||||
const msg = data as { vmid: number; server_id: number; status: string };
|
||||
qc.setQueryData<VMInstance[]>(vmKeys.all, (old) =>
|
||||
old?.map((vm) =>
|
||||
@@ -42,10 +46,56 @@ export function useVMStatusSync() {
|
||||
)
|
||||
);
|
||||
});
|
||||
return () => unsub();
|
||||
const unsubDeleted = ws.onType('vm_deleted', (data: unknown) => {
|
||||
const msg = data as { vmid: number; server_id: number };
|
||||
qc.setQueryData<VMInstance[]>(vmKeys.all, (old) =>
|
||||
old?.filter((vm) => !(vm.server_id === msg.server_id && vm.vmid === msg.vmid))
|
||||
);
|
||||
// Also invalidate any per-VM detail queries so stale pages refresh
|
||||
qc.invalidateQueries({ queryKey: vmKeys.resourcesAll });
|
||||
});
|
||||
const unsubCreated = ws.onType('vm_created', () => {
|
||||
// A new VM appeared on Proxmox; refetch the full list
|
||||
qc.invalidateQueries({ queryKey: vmKeys.all });
|
||||
qc.invalidateQueries({ queryKey: vmKeys.resourcesAll });
|
||||
});
|
||||
return () => {
|
||||
unsubStatus();
|
||||
unsubDeleted();
|
||||
unsubCreated();
|
||||
};
|
||||
}, [qc]);
|
||||
}
|
||||
|
||||
export type LiveInstanceMetrics = {
|
||||
server_id: number; vmid: number; node?: string; status?: string;
|
||||
cpu?: number; mem?: number; maxmem?: number; disk?: number; maxdisk?: number;
|
||||
uptime?: number; netin?: number; netout?: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Subscribes to live VM/LXC metrics via WebSocket channel `instances_list_metrics`.
|
||||
* Returns a Map keyed by `"server_id:vmid"` that updates every ~1 s.
|
||||
* Uses its own useState — bypasses TanStack Query structural sharing entirely.
|
||||
*/
|
||||
export function useInstancesMetricsSync(): Map<string, LiveInstanceMetrics> {
|
||||
const [metricsMap, setMetricsMap] = useState(() => new Map<string, LiveInstanceMetrics>());
|
||||
useEffect(() => {
|
||||
const ws = getTasksWebSocket();
|
||||
ws.connect();
|
||||
const apply = (data: unknown) => {
|
||||
const items = (data as { data?: { items?: LiveInstanceMetrics[] } })?.data?.items;
|
||||
if (!items?.length) return;
|
||||
const next = new Map<string, LiveInstanceMetrics>();
|
||||
for (const it of items) next.set(`${it.server_id}:${it.vmid}`, it);
|
||||
setMetricsMap(next);
|
||||
};
|
||||
const unsub = ws.subscribe('instances_list_metrics', apply);
|
||||
return () => unsub();
|
||||
}, []);
|
||||
return metricsMap;
|
||||
}
|
||||
|
||||
export function useAllResources() {
|
||||
return useQuery({
|
||||
queryKey: vmKeys.resourcesAll,
|
||||
|
||||
@@ -99,6 +99,13 @@ export function useDeleteServer() {
|
||||
|
||||
export function useTestServer() {
|
||||
return useMutation({
|
||||
mutationFn: (id: number) => apiClient.post<{ success: boolean; message?: string }>(`/proxmox/api/servers/${id}/test`, {}),
|
||||
mutationFn: (id: number) => apiClient.post<{ success: boolean; status?: string; message?: string }>(`/proxmox/api/servers/${id}/test`, {}),
|
||||
});
|
||||
}
|
||||
|
||||
export function useTestServerCredentials() {
|
||||
return useMutation({
|
||||
mutationFn: (data: ProxmoxServerCreate) =>
|
||||
apiClient.post<{ success: boolean; status?: string; message?: string }>(`/proxmox/api/servers/test`, data),
|
||||
});
|
||||
}
|
||||
|
||||
132
frontend/src/hooks/use-ssh-keys.ts
Normal file
132
frontend/src/hooks/use-ssh-keys.ts
Normal file
@@ -0,0 +1,132 @@
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { apiClient } from '@/lib/api-client';
|
||||
|
||||
export interface SSHKey {
|
||||
id: number;
|
||||
user_id: number;
|
||||
name: string;
|
||||
public_key: string;
|
||||
fingerprint: string | null;
|
||||
comment: string | null;
|
||||
has_private_key: boolean;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export interface CreateSSHKeyPayload {
|
||||
name: string;
|
||||
public_key: string;
|
||||
private_key?: string;
|
||||
comment?: string;
|
||||
}
|
||||
|
||||
export interface UpdateSSHKeyPayload {
|
||||
name?: string;
|
||||
public_key?: string;
|
||||
private_key?: string;
|
||||
comment?: string;
|
||||
}
|
||||
|
||||
export const sshKeyKeys = {
|
||||
my: ['ssh-keys', 'my'] as const,
|
||||
byUser: (userId: number) => ['ssh-keys', 'user', userId] as const,
|
||||
};
|
||||
|
||||
// ── Self-service ────────────────────────────────────────────────────────────
|
||||
|
||||
export function useMySSHKeys() {
|
||||
return useQuery({
|
||||
queryKey: sshKeyKeys.my,
|
||||
queryFn: () => apiClient.get<SSHKey[]>('/api/ssh-keys/my'),
|
||||
});
|
||||
}
|
||||
|
||||
export function useCreateMySSHKey() {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (data: CreateSSHKeyPayload) =>
|
||||
apiClient.post<SSHKey>('/api/ssh-keys/my', data),
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: sshKeyKeys.my }),
|
||||
});
|
||||
}
|
||||
|
||||
export function useUpdateMySSHKey() {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: ({ id, data }: { id: number; data: UpdateSSHKeyPayload }) =>
|
||||
apiClient.put<SSHKey>(`/api/ssh-keys/my/${id}`, data),
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: sshKeyKeys.my }),
|
||||
});
|
||||
}
|
||||
|
||||
export function useDeleteMySSHKey() {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (id: number) =>
|
||||
apiClient.delete<{ success: boolean; message: string }>(`/api/ssh-keys/my/${id}`),
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: sshKeyKeys.my }),
|
||||
});
|
||||
}
|
||||
|
||||
export function useDownloadMyPrivateKey() {
|
||||
return useMutation({
|
||||
mutationFn: (id: number) =>
|
||||
apiClient.get<{ id: number; name: string; private_key: string }>(
|
||||
`/api/ssh-keys/my/${id}/private`,
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
// ── Admin: per-user ─────────────────────────────────────────────────────────
|
||||
|
||||
export function useUserSSHKeys(userId: number | null | undefined) {
|
||||
return useQuery({
|
||||
queryKey: userId ? sshKeyKeys.byUser(userId) : ['ssh-keys', 'user', 'none'],
|
||||
queryFn: () =>
|
||||
apiClient.get<SSHKey[]>(`/api/ssh-keys/users/${userId}`),
|
||||
enabled: !!userId,
|
||||
});
|
||||
}
|
||||
|
||||
export function useAdminCreateUserSSHKey() {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: ({ userId, data }: { userId: number; data: CreateSSHKeyPayload }) =>
|
||||
apiClient.post<SSHKey>(`/api/ssh-keys/users/${userId}`, data),
|
||||
onSuccess: (_data, vars) => {
|
||||
qc.invalidateQueries({ queryKey: sshKeyKeys.byUser(vars.userId) });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useAdminUpdateUserSSHKey() {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: ({ userId, keyId, data }: { userId: number; keyId: number; data: UpdateSSHKeyPayload }) =>
|
||||
apiClient.put<SSHKey>(`/api/ssh-keys/users/${userId}/${keyId}`, data),
|
||||
onSuccess: (_data, vars) => {
|
||||
qc.invalidateQueries({ queryKey: sshKeyKeys.byUser(vars.userId) });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useAdminDeleteUserSSHKey() {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: ({ userId, keyId }: { userId: number; keyId: number }) =>
|
||||
apiClient.delete<{ success: boolean; message: string }>(
|
||||
`/api/ssh-keys/users/${userId}/${keyId}`,
|
||||
),
|
||||
onSuccess: (_data, vars) => {
|
||||
qc.invalidateQueries({ queryKey: sshKeyKeys.byUser(vars.userId) });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useAdminDownloadUserPrivateKey() {
|
||||
return useMutation({
|
||||
mutationFn: ({ userId, keyId }: { userId: number; keyId: number }) =>
|
||||
apiClient.get<{ id: number; name: string; private_key: string }>(
|
||||
`/api/ssh-keys/users/${userId}/${keyId}/private`,
|
||||
),
|
||||
});
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import type { OSTemplate, OSTemplateGroup } from '@/types';
|
||||
|
||||
export const templateKeys = {
|
||||
groups: ['template-groups'] as const,
|
||||
templates: (groupId?: number, serverId?: number) => ['templates', groupId, serverId] as const,
|
||||
templates: (groupId?: number, serverId?: number, vmType?: string) => ['templates', groupId, serverId, vmType] as const,
|
||||
template: (id: number) => ['template', id] as const,
|
||||
discover: (serverId: number) => ['template-discover', serverId] as const,
|
||||
};
|
||||
@@ -16,13 +16,14 @@ export function useTemplateGroups() {
|
||||
});
|
||||
}
|
||||
|
||||
export function useTemplates(groupId?: number, serverId?: number) {
|
||||
export function useTemplates(groupId?: number, serverId?: number, vmType?: string) {
|
||||
const params = new URLSearchParams();
|
||||
if (groupId) params.set('group_id', String(groupId));
|
||||
if (serverId) params.set('server_id', String(serverId));
|
||||
if (vmType) params.set('vm_type', vmType);
|
||||
const qs = params.toString();
|
||||
return useQuery({
|
||||
queryKey: templateKeys.templates(groupId, serverId),
|
||||
queryKey: templateKeys.templates(groupId, serverId, vmType),
|
||||
queryFn: () => apiClient.get<OSTemplate[]>(`/templates/api/templates${qs ? `?${qs}` : ''}`),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import type { User } from '@/types';
|
||||
export const userKeys = {
|
||||
users: ['users'] as const,
|
||||
user: (id: number) => ['user', id] as const,
|
||||
userServers: (id: number) => ['user-server-assignments', id] as const,
|
||||
roles: ['roles'] as const,
|
||||
permissions: ['permissions-v2'] as const,
|
||||
sessions: ['sessions'] as const,
|
||||
@@ -13,17 +14,80 @@ export const userKeys = {
|
||||
securityEvents: ['security-events'] as const,
|
||||
};
|
||||
|
||||
interface Role {
|
||||
export interface Role {
|
||||
id: number;
|
||||
name: string;
|
||||
display_name: string;
|
||||
description?: string;
|
||||
permissions: string[];
|
||||
permissions: Record<string, boolean>;
|
||||
is_system: boolean;
|
||||
is_active: boolean;
|
||||
user_count: number;
|
||||
created_at?: string;
|
||||
}
|
||||
|
||||
export interface SessionInfo {
|
||||
id: number;
|
||||
user_id: number;
|
||||
username: string;
|
||||
ip_address?: string;
|
||||
device_info?: string;
|
||||
created_at?: string;
|
||||
last_activity?: string;
|
||||
expires_at?: string;
|
||||
}
|
||||
|
||||
export interface BlockedIp {
|
||||
id: number;
|
||||
ip_address: string;
|
||||
reason?: string;
|
||||
blocked_by?: string;
|
||||
blocked_at?: string;
|
||||
expires_at?: string;
|
||||
is_permanent: boolean;
|
||||
attempts_count?: number;
|
||||
is_active: boolean;
|
||||
}
|
||||
|
||||
export interface SecuritySettings {
|
||||
max_login_attempts: number;
|
||||
lockout_duration_minutes: number;
|
||||
session_timeout_minutes: number;
|
||||
ip_block_threshold: number;
|
||||
ip_block_duration_minutes: number;
|
||||
password_min_length: number;
|
||||
password_require_uppercase: boolean;
|
||||
password_require_lowercase: boolean;
|
||||
password_require_numbers: boolean;
|
||||
password_require_special: boolean;
|
||||
}
|
||||
|
||||
export interface SecurityEvent {
|
||||
id: number;
|
||||
event_type: string;
|
||||
ip_address?: string;
|
||||
username?: string;
|
||||
message?: string;
|
||||
created_at?: string;
|
||||
}
|
||||
|
||||
export interface ServerAssignment {
|
||||
id: number;
|
||||
name: string;
|
||||
ip_address: string;
|
||||
is_online: boolean;
|
||||
workspaces: { id: number; name: string }[];
|
||||
compatible: boolean;
|
||||
assigned: boolean;
|
||||
}
|
||||
|
||||
export interface UserServerAssignmentsResponse {
|
||||
servers: ServerAssignment[];
|
||||
user_workspaces: { id: number; name: string }[];
|
||||
}
|
||||
|
||||
// ==================== Users ====================
|
||||
|
||||
export function useUsers() {
|
||||
return useQuery({
|
||||
queryKey: userKeys.users,
|
||||
@@ -39,21 +103,43 @@ export function useUser(userId: number) {
|
||||
});
|
||||
}
|
||||
|
||||
export interface CreateUserPayload {
|
||||
username: string;
|
||||
email: string;
|
||||
password: string;
|
||||
full_name?: string;
|
||||
role_id?: number | null;
|
||||
is_active?: boolean;
|
||||
is_admin?: boolean;
|
||||
}
|
||||
|
||||
export function useCreateUser() {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (data: { username: string; email: string; password: string; full_name?: string; role_id?: number; is_active?: boolean; is_admin?: boolean }) =>
|
||||
apiClient.post<User>('/admin/api/users', data),
|
||||
mutationFn: (data: CreateUserPayload) => apiClient.post<User>('/admin/api/users', data),
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: userKeys.users }),
|
||||
});
|
||||
}
|
||||
|
||||
export interface UpdateUserPayload {
|
||||
email?: string;
|
||||
full_name?: string;
|
||||
password?: string;
|
||||
role_id?: number | null;
|
||||
is_active?: boolean;
|
||||
is_admin?: boolean;
|
||||
require_password_change?: boolean;
|
||||
}
|
||||
|
||||
export function useUpdateUser() {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: ({ id, ...data }: { id: number; email?: string; full_name?: string; role_id?: number; is_active?: boolean; is_admin?: boolean }) =>
|
||||
mutationFn: ({ id, ...data }: { id: number } & UpdateUserPayload) =>
|
||||
apiClient.put<User>(`/admin/api/users/${id}`, data),
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: userKeys.users }),
|
||||
onSuccess: (_, vars) => {
|
||||
qc.invalidateQueries({ queryKey: userKeys.users });
|
||||
qc.invalidateQueries({ queryKey: userKeys.user(vars.id) });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -68,8 +154,10 @@ export function useDeleteUser() {
|
||||
export function useResetPassword() {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (userId: number) =>
|
||||
apiClient.post<{ message: string }>(`/admin/api/users/${userId}/reset-password`, {}),
|
||||
mutationFn: ({ userId, newPassword }: { userId: number; newPassword: string }) =>
|
||||
apiClient.post<{ message: string }>(`/admin/api/users/${userId}/reset-password`, {
|
||||
new_password: newPassword,
|
||||
}),
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: userKeys.users }),
|
||||
});
|
||||
}
|
||||
@@ -77,11 +165,51 @@ export function useResetPassword() {
|
||||
export function useUnlockUser() {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (userId: number) => apiClient.post<{ message: string }>(`/admin/api/users/${userId}/unlock`),
|
||||
mutationFn: (userId: number) =>
|
||||
apiClient.post<{ message: string }>(`/admin/api/users/${userId}/unlock`),
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: userKeys.users }),
|
||||
});
|
||||
}
|
||||
|
||||
export function useTerminateUserSessions() {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (userId: number) =>
|
||||
apiClient.post<{ message: string }>(`/admin/api/users/${userId}/terminate-sessions`),
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: userKeys.sessions }),
|
||||
});
|
||||
}
|
||||
|
||||
// ==================== User → Servers ====================
|
||||
|
||||
export function useUserServerAssignments(userId: number) {
|
||||
return useQuery({
|
||||
queryKey: userKeys.userServers(userId),
|
||||
queryFn: () =>
|
||||
apiClient.get<UserServerAssignmentsResponse>(
|
||||
`/admin/api/users/${userId}/server-assignments`
|
||||
),
|
||||
enabled: userId > 0,
|
||||
});
|
||||
}
|
||||
|
||||
export function useSetUserServers() {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: ({ userId, serverIds }: { userId: number; serverIds: number[] }) =>
|
||||
apiClient.put<{ message: string; server_ids: number[] }>(
|
||||
`/admin/api/users/${userId}/servers`,
|
||||
{ server_ids: serverIds }
|
||||
),
|
||||
onSuccess: (_, vars) => {
|
||||
qc.invalidateQueries({ queryKey: userKeys.userServers(vars.userId) });
|
||||
qc.invalidateQueries({ queryKey: userKeys.users });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// ==================== Roles ====================
|
||||
|
||||
export function useRoles() {
|
||||
return useQuery({
|
||||
queryKey: userKeys.roles,
|
||||
@@ -92,8 +220,12 @@ export function useRoles() {
|
||||
export function useCreateRole() {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (data: { name: string; display_name: string; description?: string; permissions: string[] }) =>
|
||||
apiClient.post<Role>('/admin/api/roles', data),
|
||||
mutationFn: (data: {
|
||||
name: string;
|
||||
display_name: string;
|
||||
description?: string;
|
||||
permissions: Record<string, boolean>;
|
||||
}) => apiClient.post<Role>('/admin/api/roles', data),
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: userKeys.roles }),
|
||||
});
|
||||
}
|
||||
@@ -101,8 +233,16 @@ export function useCreateRole() {
|
||||
export function useUpdateRole() {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: ({ id, ...data }: { id: number; display_name?: string; description?: string; permissions?: string[]; is_active?: boolean }) =>
|
||||
apiClient.put<Role>(`/admin/api/roles/${id}`, data),
|
||||
mutationFn: ({
|
||||
id,
|
||||
...data
|
||||
}: {
|
||||
id: number;
|
||||
display_name?: string;
|
||||
description?: string;
|
||||
permissions?: Record<string, boolean>;
|
||||
is_active?: boolean;
|
||||
}) => apiClient.put<Role>(`/admin/api/roles/${id}`, data),
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: userKeys.roles }),
|
||||
});
|
||||
}
|
||||
@@ -118,15 +258,18 @@ export function useDeleteRole() {
|
||||
export function usePermissions() {
|
||||
return useQuery({
|
||||
queryKey: userKeys.permissions,
|
||||
queryFn: () => apiClient.get<Record<string, unknown>>('/admin/api/permissions/v2'),
|
||||
queryFn: () =>
|
||||
apiClient.get<Record<string, Record<string, string>>>('/admin/api/permissions/v2'),
|
||||
staleTime: Infinity,
|
||||
});
|
||||
}
|
||||
|
||||
// ==================== Sessions ====================
|
||||
|
||||
export function useSessions() {
|
||||
return useQuery({
|
||||
queryKey: userKeys.sessions,
|
||||
queryFn: () => apiClient.get<any[]>('/admin/api/sessions'),
|
||||
queryFn: () => apiClient.get<SessionInfo[]>('/admin/api/sessions'),
|
||||
refetchInterval: 15000,
|
||||
});
|
||||
}
|
||||
@@ -134,7 +277,70 @@ export function useSessions() {
|
||||
export function useTerminateSession() {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (sessionId: string) => apiClient.delete(`/admin/api/sessions/${sessionId}`),
|
||||
mutationFn: (sessionId: number) => apiClient.delete(`/admin/api/sessions/${sessionId}`),
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: userKeys.sessions }),
|
||||
});
|
||||
}
|
||||
|
||||
export function useTerminateAllSessions() {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: () => apiClient.post<{ message: string }>('/admin/api/sessions/terminate-all'),
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: userKeys.sessions }),
|
||||
});
|
||||
}
|
||||
|
||||
// ==================== Security ====================
|
||||
|
||||
export function useSecuritySettings() {
|
||||
return useQuery({
|
||||
queryKey: userKeys.securitySettings,
|
||||
queryFn: () => apiClient.get<SecuritySettings>('/admin/api/security/settings'),
|
||||
});
|
||||
}
|
||||
|
||||
export function useUpdateSecuritySettings() {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (data: Partial<SecuritySettings>) =>
|
||||
apiClient.put<{ message: string; updated: string[] }>(
|
||||
'/admin/api/security/settings',
|
||||
data
|
||||
),
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: userKeys.securitySettings }),
|
||||
});
|
||||
}
|
||||
|
||||
export function useBlockedIps() {
|
||||
return useQuery({
|
||||
queryKey: userKeys.blockedIps,
|
||||
queryFn: () => apiClient.get<BlockedIp[]>('/admin/api/security/blocked-ips'),
|
||||
refetchInterval: 30000,
|
||||
});
|
||||
}
|
||||
|
||||
export function useBlockIp() {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (data: { ip_address: string; reason?: string; duration_minutes?: number }) =>
|
||||
apiClient.post<{ message: string; id: number }>('/admin/api/security/blocked-ips', data),
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: userKeys.blockedIps }),
|
||||
});
|
||||
}
|
||||
|
||||
export function useUnblockIp() {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (id: number) => apiClient.delete(`/admin/api/security/blocked-ips/${id}`),
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: userKeys.blockedIps }),
|
||||
});
|
||||
}
|
||||
|
||||
export function useSecurityEvents(limit = 50) {
|
||||
return useQuery({
|
||||
queryKey: [...userKeys.securityEvents, limit],
|
||||
queryFn: () =>
|
||||
apiClient.get<SecurityEvent[]>(`/admin/api/security/events?limit=${limit}`),
|
||||
refetchInterval: 30000,
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user