import { useState, useMemo } from 'react'; import { Avatar, Badge, Button, Input, MonoText, SectionHeader, Tag, InlineEdit, RadioGroup, RadioItem, InfoCallout, MultiSelect, ConfirmDialog, AlertDialog, SplitPane, EntityList, Spinner, useToast, } from '@cameleer/design-system'; import { useUsers, useCreateUser, useUpdateUser, useDeleteUser, useAssignRoleToUser, useRemoveRoleFromUser, useAddUserToGroup, useRemoveUserFromGroup, useSetPassword, useGroups, useRoles, } from '../../api/queries/admin/rbac'; import type { UserDetail } from '../../api/queries/admin/rbac'; import { useAuthStore } from '../../auth/auth-store'; import styles from './UserManagement.module.css'; export default function UsersTab() { const { toast } = useToast(); const { data: users, isLoading } = useUsers(); const { data: allGroups } = useGroups(); const { data: allRoles } = useRoles(); const currentUsername = useAuthStore((s) => s.username); const [search, setSearch] = useState(''); const [selectedId, setSelectedId] = useState(null); const [creating, setCreating] = useState(false); const [deleteTarget, setDeleteTarget] = useState(null); const [removeGroupTarget, setRemoveGroupTarget] = useState(null); // Create form state const [newUsername, setNewUsername] = useState(''); const [newDisplay, setNewDisplay] = useState(''); const [newEmail, setNewEmail] = useState(''); const [newPassword, setNewPassword] = useState(''); const [newProvider, setNewProvider] = useState<'local' | 'oidc'>('local'); // Password reset state const [resettingPassword, setResettingPassword] = useState(false); const [newPw, setNewPw] = useState(''); // Mutations const createUser = useCreateUser(); const updateUser = useUpdateUser(); const deleteUser = useDeleteUser(); const assignRole = useAssignRoleToUser(); const removeRole = useRemoveRoleFromUser(); const addToGroup = useAddUserToGroup(); const removeFromGroup = useRemoveUserFromGroup(); const setPassword = useSetPassword(); const userList = users ?? []; const filtered = useMemo(() => { if (!search) return userList; const q = search.toLowerCase(); return userList.filter( (u) => u.displayName.toLowerCase().includes(q) || (u.email ?? '').toLowerCase().includes(q) || u.userId.toLowerCase().includes(q), ); }, [userList, search]); const selected = userList.find((u) => u.userId === selectedId) ?? null; const isSelf = currentUsername != null && selected != null && selected.displayName === currentUsername; const duplicateUsername = newUsername.trim() !== '' && userList.some( (u) => u.displayName.toLowerCase() === newUsername.trim().toLowerCase(), ); // Derived data for detail pane const directGroupIds = new Set(selected?.directGroups.map((g) => g.id) ?? []); const directRoleIds = new Set(selected?.directRoles.map((r) => r.id) ?? []); const inheritedRoles = selected?.effectiveRoles.filter((r) => !directRoleIds.has(r.id)) ?? []; const availableGroups = (allGroups ?? []) .filter((g) => !directGroupIds.has(g.id)) .map((g) => ({ value: g.id, label: g.name })); const availableRoles = (allRoles ?? []) .filter((r) => !directRoleIds.has(r.id)) .map((r) => ({ value: r.id, label: r.name })); function handleCreate() { if (!newUsername.trim()) return; if (newProvider === 'local' && !newPassword.trim()) return; createUser.mutate( { username: newUsername.trim(), displayName: newDisplay.trim() || undefined, email: newEmail.trim() || undefined, password: newProvider === 'local' ? newPassword : undefined, }, { onSuccess: () => { toast({ title: 'User created', description: newDisplay.trim() || newUsername.trim(), variant: 'success', }); setCreating(false); setNewUsername(''); setNewDisplay(''); setNewEmail(''); setNewPassword(''); setNewProvider('local'); }, onError: () => { toast({ title: 'Failed to create user', variant: 'error' }); }, }, ); } function handleDelete() { if (!deleteTarget) return; deleteUser.mutate(deleteTarget.userId, { onSuccess: () => { toast({ title: 'User deleted', description: deleteTarget.displayName, variant: 'warning', }); if (selectedId === deleteTarget.userId) setSelectedId(null); setDeleteTarget(null); }, onError: () => { toast({ title: 'Failed to delete user', variant: 'error' }); setDeleteTarget(null); }, }); } function handleResetPassword() { if (!selected || !newPw.trim()) return; setPassword.mutate( { userId: selected.userId, password: newPw }, { onSuccess: () => { toast({ title: 'Password updated', description: selected.displayName, variant: 'success', }); setResettingPassword(false); setNewPw(''); }, onError: () => { toast({ title: 'Failed to update password', variant: 'error' }); }, }, ); } function getUserGroupPath(user: UserDetail): string { if (user.directGroups.length === 0) return 'no groups'; return user.directGroups.map((g) => g.name).join(', '); } if (isLoading) return ; return ( <> {creating && (
setNewProvider(v as 'local' | 'oidc')} orientation="horizontal" >
setNewUsername(e.target.value)} /> setNewDisplay(e.target.value)} />
{duplicateUsername && ( Username already exists )} setNewEmail(e.target.value)} /> {newProvider === 'local' && ( setNewPassword(e.target.value)} /> )} {newProvider === 'oidc' && ( OIDC users authenticate via the configured identity provider. Pre-register to assign roles/groups before their first login. )}
)} ( <>
{user.displayName} {user.provider !== 'local' && ( )}
{user.email || user.userId} ·{' '} {getUserGroupPath(user)}
{user.directRoles.map((r) => ( ))} {user.directGroups.map((g) => ( ))}
)} getItemId={(user) => user.userId} selectedId={selectedId ?? undefined} onSelect={(id) => { setSelectedId(id); setResettingPassword(false); }} searchPlaceholder="Search users..." onSearch={setSearch} addLabel="+ Add user" onAdd={() => setCreating(true)} emptyMessage="No users match your search" /> } detail={ selected ? ( <>
updateUser.mutate( { userId: selected.userId, displayName: v }, { onSuccess: () => toast({ title: 'Display name updated', variant: 'success', }), onError: () => toast({ title: 'Failed to update name', variant: 'error', }), }, ) } />
{selected.email || selected.userId}
Status
ID {selected.userId} Created {new Date(selected.createdAt).toLocaleDateString()} Provider {selected.provider}
Security
{selected.provider === 'local' ? ( <>
Password •••••••• {!resettingPassword && ( )}
{resettingPassword && (
setNewPw(e.target.value)} className={styles.resetInput} />
)} ) : ( <>
Authentication OIDC ({selected.provider})
Password managed by the identity provider. )}
Group membership (direct only)
{selected.directGroups.map((g) => ( { removeFromGroup.mutate( { userId: selected.userId, groupId: g.id }, { onSuccess: () => toast({ title: 'Group removed', variant: 'success' }), onError: () => toast({ title: 'Failed to remove group', variant: 'error', }), }, ); }} /> ))} {selected.directGroups.length === 0 && ( (no groups) )} { for (const groupId of ids) { addToGroup.mutate( { userId: selected.userId, groupId }, { onSuccess: () => toast({ title: 'Added to group', variant: 'success' }), onError: () => toast({ title: 'Failed to add group', variant: 'error', }), }, ); } }} placeholder="+ Add" />
Effective roles (direct + inherited)
{selected.directRoles.map((r) => ( { removeRole.mutate( { userId: selected.userId, roleId: r.id }, { onSuccess: () => toast({ title: 'Role removed', description: r.name, variant: 'success', }), onError: () => toast({ title: 'Failed to remove role', variant: 'error', }), }, ); }} /> ))} {inheritedRoles.map((r) => ( ))} {selected.directRoles.length === 0 && inheritedRoles.length === 0 && ( (no roles) )} { for (const roleId of roleIds) { assignRole.mutate( { userId: selected.userId, roleId }, { onSuccess: () => toast({ title: 'Role assigned', variant: 'success', }), onError: () => toast({ title: 'Failed to assign role', variant: 'error', }), }, ); } }} placeholder="+ Add" />
{inheritedRoles.length > 0 && ( Roles with ↑ are inherited through group membership )} ) : null } emptyMessage="Select a user to view details" /> setDeleteTarget(null)} onConfirm={handleDelete} message={`Delete user "${deleteTarget?.displayName}"? This cannot be undone.`} confirmText={deleteTarget?.displayName ?? ''} loading={deleteUser.isPending} /> setRemoveGroupTarget(null)} onConfirm={() => { if (removeGroupTarget && selected) { removeFromGroup.mutate( { userId: selected.userId, groupId: removeGroupTarget }, { onSuccess: () => toast({ title: 'Group removed', variant: 'success' }), onError: () => toast({ title: 'Failed to remove group', variant: 'error', }), }, ); } setRemoveGroupTarget(null); }} title="Remove group membership" description="Removing this group may also revoke inherited roles. Continue?" confirmLabel="Remove" variant="warning" /> ); }