import { useEffect, useState, useMemo } from 'react' import { useNavigate } from 'react-router-dom' import { Box, Button, Chip, CircularProgress, Container, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, FormControlLabel, IconButton, MenuItem, Paper, Select, Switch, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TextField, Toolbar, Tooltip, Typography, Snackbar, Alert, InputAdornment, FormControl, InputLabel, List, ListItem, ListItemIcon, ListItemText, } from '@mui/material' import { Add as AddIcon, Lock as LockIcon, Edit as EditIcon, VpnKey as VpnKeyIcon, Delete as DeleteIcon, Search as SearchIcon, Check as CheckIcon, Close as CloseIcon, } from '@mui/icons-material' import { usersApi } from '../api/client' import { useAuthStore } from '../store/authStore' import type { User, UpdateUserRequest, AdminResetPasswordRequest } from '../types' /** Password strength checker */ function checkPasswordStrength(password: string) { return { length: password.length >= 8, uppercase: /[A-Z]/.test(password), lowercase: /[a-z]/.test(password), digit: /[0-9]/.test(password), special: /[!@#$%^&*()_+\-=\[\]{}|;:,.<>?]/.test(password), } } function isPasswordValid(checks: ReturnType) { return checks.length && checks.uppercase && checks.lowercase && checks.digit && checks.special } /** Reusable password strength checklist component */ function PasswordStrengthIndicator({ password }: { password: string }) { if (!password) return null const checks = checkPasswordStrength(password) return ( {checks.length ? : } {checks.uppercase ? : } {checks.lowercase ? : } {checks.digit ? : } {checks.special ? : } ) } export default function UsersPage() { const currentUser = useAuthStore(s => s.user) const navigate = useNavigate() const isAdmin = currentUser?.role === 'admin' const [users, setUsers] = useState([]) const [loading, setLoading] = useState(true) // Snackbar const [snack, setSnack] = useState<{ open: boolean; severity: 'success' | 'error'; message: string }>({ open: false, severity: 'success', message: '', }) const showSnack = (severity: 'success' | 'error', message: string) => setSnack({ open: true, severity, message }) // Search / filter const [searchText, setSearchText] = useState('') const [roleFilter, setRoleFilter] = useState('all') // Add User dialog const [addOpen, setAddOpen] = useState(false) const [addForm, setAddForm] = useState({ username: '', display_name: '', email: '', role: 'operator', password: '' }) // Edit User dialog const [editOpen, setEditOpen] = useState(false) const [editUser, setEditUser] = useState(null) const [editForm, setEditForm] = useState({ display_name: '', email: '', role: 'operator', is_active: true, force_password_reset: false, }) // Password Reset dialog const [resetOpen, setResetOpen] = useState(false) const [resetUser, setResetUser] = useState(null) const [resetForm, setResetForm] = useState({ new_password: '', confirm_password: '', force_password_reset: true }) // MFA Disable confirmation dialog const [mfaConfirmOpen, setMfaConfirmOpen] = useState(false) const [mfaDisableUser, setMfaDisableUser] = useState(null) // Delete confirmation dialog const [deleteOpen, setDeleteOpen] = useState(false) const [deleteUser, setDeleteUser] = useState(null) const addPwValid = isPasswordValid(checkPasswordStrength(addForm.password)) const resetPwValid = isPasswordValid(checkPasswordStrength(resetForm.new_password)) const resetPwMismatch = !!(resetForm.confirm_password && resetForm.new_password !== resetForm.confirm_password) const load = async () => { setLoading(true) try { const r = await usersApi.list() setUsers(r.data) } catch { showSnack('error', 'Failed to load users') } finally { setLoading(false) } } // eslint-disable-next-line react-hooks/exhaustive-deps useEffect(() => { load() }, []) // Filtered users const filteredUsers = useMemo(() => { let list = users if (roleFilter !== 'all') { list = list.filter(u => u.role === roleFilter) } if (searchText.trim()) { const q = searchText.toLowerCase() list = list.filter(u => u.username.toLowerCase().includes(q) || u.display_name.toLowerCase().includes(q) || u.email.toLowerCase().includes(q) ) } return list }, [users, roleFilter, searchText]) // ── Handlers ──────────────────────────────────────────────────────────────── const handleCreate = async () => { if (!addPwValid) { showSnack('error', 'Password does not meet strength requirements') return } try { await usersApi.create(addForm) setAddOpen(false) setAddForm({ username: '', display_name: '', email: '', role: 'operator', password: '' }) showSnack('success', 'User created successfully') load() } catch { showSnack('error', 'Failed to create user') } } const handleRevoke = async (id: string) => { try { await usersApi.revokeSessions(id) showSnack('success', 'Sessions revoked') } catch { showSnack('error', 'Failed to revoke sessions') } } const openEdit = (u: User) => { setEditUser(u) setEditForm({ display_name: u.display_name || '', email: u.email || '', role: u.role, is_active: u.is_active, force_password_reset: u.force_password_reset, }) setEditOpen(true) } const handleEditSave = async () => { if (!editUser) return try { await usersApi.update(editUser.id, editForm) setEditOpen(false) showSnack('success', 'User updated successfully') load() } catch { showSnack('error', 'Failed to update user') } } const openReset = (u: User) => { setResetUser(u) setResetForm({ new_password: '', confirm_password: '', force_password_reset: true }) setResetOpen(true) } const handleResetSave = async () => { if (!resetUser) return if (resetPwMismatch) { showSnack('error', 'Passwords do not match') return } if (!resetPwValid) { showSnack('error', 'Password does not meet strength requirements') return } try { const data: AdminResetPasswordRequest = { new_password: resetForm.new_password, force_password_reset: resetForm.force_password_reset, } await usersApi.adminResetPassword(resetUser.id, data) setResetOpen(false) showSnack('success', 'Password reset successfully') } catch { showSnack('error', 'Failed to reset password') } } const handleMfaDisable = (u: User) => { setMfaDisableUser(u) setMfaConfirmOpen(true) } const handleMfaDisableConfirm = async () => { if (!mfaDisableUser) return try { await usersApi.adminDisableMfa(mfaDisableUser.id) setMfaConfirmOpen(false) showSnack('success', 'MFA disabled successfully') load() } catch { showSnack('error', 'Failed to disable MFA') } } const openDelete = (u: User) => { setDeleteUser(u) setDeleteOpen(true) } const handleDeleteConfirm = async () => { if (!deleteUser) return try { await usersApi.delete(deleteUser.id) setDeleteOpen(false) showSnack('success', 'User deleted successfully') load() } catch { showSnack('error', 'Failed to delete user') } } // ── Render ───────────────────────────────────────────────────────────────── return ( Users {isAdmin && ( )} {/* Search / Filter bar */} setSearchText(e.target.value)} InputProps={{ startAdornment: ( ), }} sx={{ flexGrow: 1 }} /> Role {loading ? ( ) : ( Username Display Name Email Role MFA Status Actions {filteredUsers.map(u => ( {u.username} {u.display_name} {u.email} {u.mfa_enabled ? ( ) : currentUser?.id === u.id ? ( navigate('/mfa/setup')} /> ) : ( )} openEdit(u)}> {isAdmin && ( openReset(u)}> )} handleRevoke(u.id)}> {isAdmin && ( openDelete(u)}> )} ))} {filteredUsers.length === 0 && ( No users found )}
)} {/* ── Add User Dialog ──────────────────────────────────────────────────── */} setAddOpen(false)} maxWidth="xs" fullWidth> Add User setAddForm({ ...addForm, username: e.target.value })} margin="normal" required /> setAddForm({ ...addForm, display_name: e.target.value })} margin="normal" /> setAddForm({ ...addForm, email: e.target.value })} margin="normal" required /> setAddForm({ ...addForm, password: e.target.value })} margin="normal" required /> Role {/* ── Edit User Dialog ──────────────────────────────────────────────────── */} setEditOpen(false)} maxWidth="xs" fullWidth> Edit User setEditForm({ ...editForm, display_name: e.target.value })} margin="normal" /> setEditForm({ ...editForm, email: e.target.value })} margin="normal" /> {isAdmin && ( <> Role setEditForm({ ...editForm, is_active: e.target.checked })} /> } label="Active" /> setEditForm({ ...editForm, force_password_reset: e.target.checked })} /> } label="Force Password Reset" /> {/* MFA status */} MFA Status: {editUser?.mfa_enabled ? ( ) : ( currentUser?.id === editUser?.id ? ( ) : ( User must enable MFA from their own profile settings. ) )} {editUser?.mfa_enabled ? ( Disabling MFA reduces account security for this user. ) : ( currentUser?.id === editUser?.id && ( You will be guided through authenticator app setup. ) )} )} {/* ── Admin Password Reset Dialog ─────────────────────────────────────── */} setResetOpen(false)} maxWidth="xs" fullWidth> Reset Password for {resetUser?.username} setResetForm({ ...resetForm, new_password: e.target.value })} margin="normal" required /> setResetForm({ ...resetForm, confirm_password: e.target.value })} margin="normal" required error={resetPwMismatch} helperText={resetPwMismatch ? 'Passwords do not match' : ''} /> setResetForm({ ...resetForm, force_password_reset: e.target.checked })} /> } label="Force password reset on next login" sx={{ mt: 1 }} /> {/* ── MFA Disable Confirmation Dialog ──────────────────────────────────── */} setMfaConfirmOpen(false)} maxWidth="xs" fullWidth> Disable MFA Are you sure you want to disable MFA for user {mfaDisableUser?.username}? This will reduce the security of their account. {/* ── Delete Confirmation Dialog ────────────────────────────────────────── */} setDeleteOpen(false)} maxWidth="xs" fullWidth> Delete User Are you sure you want to delete user {deleteUser?.username}? This action cannot be undone. {/* ── Snackbar ──────────────────────────────────────────────────────────── */} setSnack(s => ({ ...s, open: false }))} anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }} > setSnack(s => ({ ...s, open: false }))} severity={snack.severity} variant="filled" sx={{ width: '100%' }} > {snack.message}
) }