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

{/* Option A structural fix (AC-17.3): labels / inputs / hints in 3 explicit grid rows so the browser can never misalign them by collapsing different-height cells. grid-rows-[auto_auto_auto] ensures row 1 = labels, row 2 = inputs, row 3 = hints. */}
{/* Row 1 — labels */}
) : null}
); }