// src/pages/Settings/UserManagement.jsx import { useState, useEffect, useCallback, useRef } from 'react'; import { Users, UserPlus, Search, Shield, ShieldOff, ShieldCheck, Pencil, Trash2, QrCode, X, Check, Loader2, AlertTriangle, Copy, ExternalLink } from 'lucide-react'; import { usersApi } from '@/api/users'; import clsx from 'clsx'; function Avatar({ name, size = 'md' }) { const initials = name ? name.split(' ').map((w) => w[0]).slice(0, 2).join('').toUpperCase() : '?'; const colors = ['bg-accent/20 text-accent', 'bg-ok/20 text-ok', 'bg-info/20 text-info', 'bg-warn/20 text-warn']; const color = colors[(name?.charCodeAt(0) ?? 0) % colors.length]; const sz = size === 'lg' ? 'w-10 h-10 text-sm' : 'w-8 h-8 text-xs'; return
{initials}
; } function Toast({ message, type = 'ok', onDismiss }) { useEffect(() => { const t = setTimeout(onDismiss, 3500); return () => clearTimeout(t); }, [onDismiss]); return (
{type === 'ok' && }{type === 'error' && } {message}
); } function DeleteModal({ user, onConfirm, onCancel, loading }) { return (

Delete user

This cannot be undone

Delete {user.name} ({user.username})?

); } function TwoFactorModal({ user, onClose }) { const [state, setState] = useState('idle'); const [result, setResult] = useState(null); const generate = useCallback(async () => { setState('loading'); try { const data = await usersApi.setup2fa(user.id); setResult(data); setState('done'); } catch { setState('error'); } }, [user.id]); useEffect(() => { generate(); }, [generate]); return (

Set up 2FA

{user.name}

{state === 'loading' &&

Generating setup link…

} {state === 'error' &&

Failed to generate setup link.

} {state === 'done' && result && ( <>
2FA setup QR code

Share this QR code with {user.name} to enroll their authenticator app.

)}
); } function UserDrawer({ mode, user, groups, onSave, onClose }) { const isEdit = mode === 'edit'; const [form, setForm] = useState(isEdit ? { username: user.username, name: user.name, email: user.email, isActive: user.isActive, groups: user.groups.map((g) => g.id), password: '', } : { username: '', name: '', email: '', isActive: true, groups: [], password: '' }); const [errors, setErrors] = useState({}); const [saving, setSaving] = useState(false); const set = (field) => (e) => setForm((f) => ({ ...f, [field]: e.target.type === 'checkbox' ? e.target.checked : e.target.value })); const toggleGroup = (id) => setForm((f) => ({ ...f, groups: f.groups.includes(id) ? f.groups.filter((g) => g !== id) : [...f.groups, id] })); const handleSubmit = async () => { const e = {}; if (!form.username.trim()) e.username = 'Required'; if (!form.name.trim()) e.name = 'Required'; if (!form.email.trim()) e.email = 'Required'; if (!isEdit && !form.password) e.password = 'Required'; if (form.password && form.password.length < 8) e.password = 'Min 8 chars'; setErrors(e); if (Object.keys(e).length > 0) return; setSaving(true); try { const payload = { username: form.username, name: form.name, email: form.email, isActive: form.isActive, groups: form.groups }; if (form.password) payload.password = form.password; await onSave(payload); } finally { setSaving(false); } }; return ( <>

{isEdit ? 'Edit user' : 'Add user'}

Identity

Groups

{groups.map((g) => ( ))}

{isEdit ? 'Reset Password' : 'Password'}

{errors.password &&

{errors.password}

}
); } export default function UserManagement() { const [users, setUsers] = useState([]); const [groups, setGroups] = useState([]); const [total, setTotal] = useState(0); const [loading, setLoading] = useState(true); const [searchInput, setSearchInput] = useState(''); const [drawer, setDrawer] = useState(null); const [deleteTarget, setDeleteTarget] = useState(null); const [twoFaTarget, setTwoFaTarget] = useState(null); const [toast, setToast] = useState(null); const searchTimer = useRef(null); const showToast = (message, type = 'ok') => setToast({ message, type }); const loadUsers = useCallback(async (q = '') => { setLoading(true); try { const result = await usersApi.list({ search: q }); setUsers(result.users); setTotal(result.count); } catch (err) { showToast(`Failed to load users: ${err.message}`, 'error'); } finally { setLoading(false); } }, []); const loadGroups = useCallback(async () => { try { const g = await usersApi.listGroups(); setGroups(g); } catch {} }, []); useEffect(() => { loadUsers(); loadGroups(); }, []); const handleSearchChange = (e) => { const val = e.target.value; setSearchInput(val); clearTimeout(searchTimer.current); searchTimer.current = setTimeout(() => loadUsers(val), 350); }; const handleSave = async (payload) => { try { if (drawer.mode === 'create') { const newUser = await usersApi.create(payload); setUsers((u) => [newUser, ...u]); setTotal((t) => t + 1); showToast(`User ${newUser.username} created`); } else { const updated = await usersApi.update(drawer.user.id, payload); setUsers((u) => u.map((x) => (x.id === updated.id ? updated : x))); showToast(`User ${updated.username} updated`); } setDrawer(null); } catch (err) { showToast(err.message, 'error'); throw err; } }; const handleDelete = async () => { try { await usersApi.delete(deleteTarget.id); setUsers((u) => u.filter((x) => x.id !== deleteTarget.id)); setTotal((t) => t - 1); showToast(`User ${deleteTarget.username} deleted`); setDeleteTarget(null); } catch (err) { showToast(err.message, 'error'); } }; return (

User Management

{total} users

{loading && } {!loading && users.length === 0 && } {!loading && users.map((u) => ( ))}
User Groups Status 2FA Actions
No users found

{u.name}

{u.username}

{u.groups.length === 0 ? : u.groups.map((g) => ( {g.name} ))}
{u.isActive ? Active : Inactive} {u.totpEnrolled ? 2FA On : No 2FA}
{drawer && setDrawer(null)} />} {deleteTarget && setDeleteTarget(null)} />} {twoFaTarget && { setTwoFaTarget(null); loadUsers(); }} />} {toast && setToast(null)} />}
); }