import { Fragment, useState, type FormEvent } from 'react'; import { extractApiError } from '@/api/client'; import type { Role, User } from '@/api/types'; import { useAuth } from '@/hooks/useAuth'; import { useCreateUser, useDeleteUser, usePatchUser, useUsersList, } from '@/hooks/useUsers'; import { useToast } from '@/hooks/useToast'; import { FormField, Select, TextInput } from '@/components/FormField'; import { LoadingState } from '@/components/LoadingState'; import { ErrorState } from '@/components/ErrorState'; import { EmptyState } from '@/components/EmptyState'; const ROLE_OPTIONS: { value: Role; label: string }[] = [ { value: 'admin', label: 'Admin' }, { value: 'redteam', label: 'Red Team' }, { value: 'soc', label: 'SOC' }, ]; interface CreateFormState { username: string; password: string; role: Role; } const EMPTY_CREATE: CreateFormState = { username: '', password: '', role: 'redteam' }; export function UsersAdminPage(): JSX.Element { const { user: currentUser } = useAuth(); const { push } = useToast(); const list = useUsersList(); const createMutation = useCreateUser(); const patchMutation = usePatchUser(); const deleteMutation = useDeleteUser(); const [createForm, setCreateForm] = useState(EMPTY_CREATE); const [createError, setCreateError] = useState(null); // Per-row password reset state. Only one row open at a time. const [resetOpen, setResetOpen] = useState(null); const [resetPassword, setResetPassword] = useState(''); const onCreate = async (e: FormEvent) => { e.preventDefault(); setCreateError(null); if (createForm.password.length < 8) { setCreateError('Password must be at least 8 characters'); return; } try { await createMutation.mutateAsync(createForm); setCreateForm(EMPTY_CREATE); push('User created', 'success'); } catch (err) { setCreateError(extractApiError(err, 'Could not create user')); } }; const onRoleChange = async (u: User, role: Role) => { if (u.role === role) return; try { await patchMutation.mutateAsync({ id: u.id, input: { role } }); push(`Role updated for ${u.username}`, 'success'); } catch (err) { push(extractApiError(err, 'Could not update role'), 'error'); } }; const onResetPassword = async (u: User, e: FormEvent) => { e.preventDefault(); if (resetPassword.length < 8) { push('Password must be at least 8 characters', 'error'); return; } try { await patchMutation.mutateAsync({ id: u.id, input: { password: resetPassword } }); push(`Password reset for ${u.username}`, 'success'); setResetOpen(null); setResetPassword(''); } catch (err) { push(extractApiError(err, 'Could not reset password'), 'error'); } }; const onDelete = async (u: User) => { if (currentUser?.id === u.id) { push('You cannot delete your own account', 'error'); return; } if (!window.confirm(`Delete user "${u.username}"?`)) return; try { await deleteMutation.mutateAsync(u.id); push('User deleted', 'success'); } catch (err) { push(extractApiError(err, 'Could not delete user'), 'error'); } }; return (

User accounts

Manage local accounts. Admins can create new red team or SOC analysts.

Create account

{/* items-start so each cell top-aligns; the button is wrapped in a flex column that pushes it to align with the input row (label + 4px gap = ~22px offset). */}
setCreateForm({ ...createForm, username: e.target.value })} required /> setCreateForm({ ...createForm, password: e.target.value })} required minLength={8} /> onRoleChange(u, e.target.value as Role)} options={ROLE_OPTIONS} aria-label={`Change role for ${u.username}`} disabled={patchMutation.isPending} /> {u.created_at}
{resetOpen === u.id ? ( onResetPassword(u, e)} className="flex items-end gap-md" > setResetPassword(e.target.value)} minLength={8} required /> ) : null} ); })}
) : null} ); }