import { useState, useMemo } from 'react'; import { Avatar, Badge, Button, Input, MonoText, Tag, InfoCallout, ConfirmDialog, Select, Spinner, InlineEdit, useToast, } from '@cameleer/design-system'; import { useUsers, useCreateUser, useDeleteUser, useAssignRoleToUser, useRemoveRoleFromUser, useAddUserToGroup, useRemoveUserFromGroup, useSetPassword, useGroups, useRoles, } from '../../api/queries/admin/rbac'; import { useAuthStore } from '../../auth/auth-store'; import styles from './UserManagement.module.css'; export default function UsersTab() { const { data: users, isLoading } = useUsers(); const { data: allGroups } = useGroups(); const { data: allRoles } = useRoles(); const currentUsername = useAuthStore((s) => s.username); const { toast } = useToast(); const [search, setSearch] = useState(''); const [selectedUserId, setSelectedUserId] = useState(null); const [showCreateForm, setShowCreateForm] = useState(false); // Create form state const [createUsername, setCreateUsername] = useState(''); const [createDisplayName, setCreateDisplayName] = useState(''); const [createEmail, setCreateEmail] = useState(''); const [createPassword, setCreatePassword] = useState(''); // Detail pane state const [showPasswordForm, setShowPasswordForm] = useState(false); const [newPassword, setNewPassword] = useState(''); const [addGroupId, setAddGroupId] = useState(''); const [addRoleId, setAddRoleId] = useState(''); const [showDeleteDialog, setShowDeleteDialog] = useState(false); // Mutations const createUser = useCreateUser(); const deleteUser = useDeleteUser(); const assignRole = useAssignRoleToUser(); const removeRole = useRemoveRoleFromUser(); const addToGroup = useAddUserToGroup(); const removeFromGroup = useRemoveUserFromGroup(); const setPassword = useSetPassword(); // Filtered user list const filteredUsers = useMemo(() => { if (!users) return []; const q = search.toLowerCase(); if (!q) return users; return users.filter( (u) => u.displayName.toLowerCase().includes(q) || (u.email ?? '').toLowerCase().includes(q) || u.userId.toLowerCase().includes(q), ); }, [users, search]); const selectedUser = useMemo( () => users?.find((u) => u.userId === selectedUserId) ?? null, [users, selectedUserId], ); // ── Handlers ────────────────────────────────────────────────────────── function handleCreateUser() { if (!createUsername.trim() || !createPassword.trim()) return; createUser.mutate( { username: createUsername.trim(), displayName: createDisplayName.trim() || undefined, email: createEmail.trim() || undefined, password: createPassword, }, { onSuccess: () => { toast({ title: 'User created', variant: 'success' }); setShowCreateForm(false); setCreateUsername(''); setCreateDisplayName(''); setCreateEmail(''); setCreatePassword(''); }, onError: () => { toast({ title: 'Failed to create user', variant: 'error' }); }, }, ); } function handleResetPassword() { if (!selectedUser || !newPassword.trim()) return; setPassword.mutate( { userId: selectedUser.userId, password: newPassword }, { onSuccess: () => { toast({ title: 'Password updated', variant: 'success' }); setShowPasswordForm(false); setNewPassword(''); }, onError: () => { toast({ title: 'Failed to update password', variant: 'error' }); }, }, ); } function handleAddGroup() { if (!selectedUser || !addGroupId) return; addToGroup.mutate( { userId: selectedUser.userId, groupId: addGroupId }, { onSuccess: () => { toast({ title: 'Added to group', variant: 'success' }); setAddGroupId(''); }, onError: () => { toast({ title: 'Failed to add group', variant: 'error' }); }, }, ); } function handleAddRole() { if (!selectedUser || !addRoleId) return; assignRole.mutate( { userId: selectedUser.userId, roleId: addRoleId }, { onSuccess: () => { toast({ title: 'Role assigned', variant: 'success' }); setAddRoleId(''); }, onError: () => { toast({ title: 'Failed to assign role', variant: 'error' }); }, }, ); } function handleDeleteUser() { if (!selectedUser) return; deleteUser.mutate(selectedUser.userId, { onSuccess: () => { toast({ title: 'User deleted', variant: 'success' }); setSelectedUserId(null); setShowDeleteDialog(false); }, onError: () => { toast({ title: 'Failed to delete user', variant: 'error' }); setShowDeleteDialog(false); }, }); } // Derived data for detail pane const directGroupIds = new Set(selectedUser?.directGroups.map((g) => g.id) ?? []); const directRoleIds = new Set(selectedUser?.directRoles.map((r) => r.id) ?? []); const inheritedRoles = selectedUser?.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 })); // Find group name for inherited role display function findInheritingGroupName(roleId: string): string { if (!selectedUser) return ''; for (const g of selectedUser.effectiveGroups) { // We don't have group→roles in the summary, so just show "group" void roleId; return g.name; } return 'group'; } const isSelf = currentUsername != null && selectedUser != null && selectedUser.displayName === currentUsername; // ── Render ──────────────────────────────────────────────────────────── return (
{/* ── Left pane ── */}
setSearch(e.target.value)} onClear={() => setSearch('')} style={{ flex: 1 }} />
{showCreateForm && (
setCreateUsername(e.target.value)} style={{ marginBottom: 6 }} /> setCreateDisplayName(e.target.value)} style={{ marginBottom: 6 }} /> setCreateEmail(e.target.value)} style={{ marginBottom: 6 }} /> setCreatePassword(e.target.value)} style={{ marginBottom: 6 }} />
)} {isLoading && }
{filteredUsers.map((user) => (
setSelectedUserId(user.userId)} onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); setSelectedUserId(user.userId); } }} >
{user.displayName} {user.provider !== 'local' && ( )}
{user.email || user.userId} {user.directGroups.length > 0 && ` · ${user.directGroups.map((g) => g.name).join(', ')}`} {user.directGroups.length === 0 && ' · no groups'}
{(user.directRoles.length > 0 || user.directGroups.length > 0) && (
{user.directRoles.map((r) => ( ))} {user.directGroups.map((g) => ( ))}
)}
))}
{/* ── Right pane ── */}
{!selectedUser ? (
Select a user to view details
) : ( <> {/* Header */}
{ // useUpdateUser not imported here to keep things clean; // display only — wired via displayName update if desired void val; }} /> {selectedUser.email && (
{selectedUser.email}
)}
{/* Metadata grid */}
User ID {selectedUser.userId} Created {new Date(selectedUser.createdAt).toLocaleString()} Provider {selectedUser.provider}
{/* Security section */}
Security
{selectedUser.provider === 'local' ? ( <> {!showPasswordForm ? ( ) : (
setNewPassword(e.target.value)} style={{ flex: 1 }} />
)} ) : ( Password managed by identity provider )}
{/* Group membership */}
Group Membership
{selectedUser.directGroups.map((g) => ( removeFromGroup.mutate( { userId: selectedUser.userId, groupId: g.id }, { onError: () => toast({ title: 'Failed to remove group', variant: 'error' }), }, ) } /> ))}
{availableGroups.length > 0 && (
setAddRoleId(e.target.value)} style={{ flex: 1 }} />
)} {/* Delete confirmation */} setShowDeleteDialog(false)} onConfirm={handleDeleteUser} title="Delete user" message={`This will permanently delete the user "${selectedUser.displayName}". Type their username to confirm.`} confirmText={selectedUser.displayName} confirmLabel="Delete" variant="danger" loading={deleteUser.isPending} /> )}
); }