import { useState, useEffect, useMemo } from 'react'; import { useNavigate } from 'react-router-dom'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { useTranslation } from 'react-i18next'; import { X, Plus, Edit2, Trash2, Save, Loader2, Users as UsersIcon, Shield, ArrowLeft, RotateCcw } from 'lucide-react'; import { api } from '../api/client'; import type { UserCreate, UserUpdate, UserResponse } from '../api/client'; import { useAuth } from '../contexts/AuthContext'; import { useToast } from '../contexts/ToastContext'; import { Button } from '../components/Button'; import { Card, CardContent, CardHeader } from '../components/Card'; import { ConfirmModal } from '../components/ConfirmModal'; import { CreateUserAdvancedAuthModal } from '../components/CreateUserAdvancedAuthModal'; import { LdapUserPicker } from '../components/LdapUserPicker'; interface FormData extends UserCreate { group_ids: number[]; confirmPassword: string; email?: string; } export function UsersPage() { const navigate = useNavigate(); const { t } = useTranslation(); const { user: currentUser, hasPermission } = useAuth(); const { showToast } = useToast(); const queryClient = useQueryClient(); const [showCreateModal, setShowCreateModal] = useState(false); // Basic-mode (non-advanced-auth) modal: track which tab is active so the // LDAP picker can replace the local form when LDAP is enabled. const [basicCreateTab, setBasicCreateTab] = useState<'local' | 'ldap'>('local'); const [showEditModal, setShowEditModal] = useState(false); const [editingUserId, setEditingUserId] = useState(null); const [deleteUserId, setDeleteUserId] = useState(null); const [formData, setFormData] = useState({ username: '', password: '', email: '', confirmPassword: '', role: 'user', group_ids: [], }); // Check if advanced auth is enabled const { data: advancedAuthStatus = { advanced_auth_enabled: false, smtp_configured: false } } = useQuery({ queryKey: ['advancedAuthStatus'], queryFn: () => api.getAdvancedAuthStatus(), }); // LDAP status — drives whether the LDAP tab is rendered in the create modal. const { data: ldapStatus = { ldap_enabled: false, ldap_configured: false } } = useQuery({ queryKey: ['ldapStatus'], queryFn: () => api.getLDAPStatus(), }); // Close modal on Escape key useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if (e.key === 'Escape') { if (showCreateModal) { setShowCreateModal(false); setBasicCreateTab('local'); setFormData({ username: '', password: '', email: '', confirmPassword: '', role: 'user', group_ids: [] }); } if (showEditModal) { setShowEditModal(false); setEditingUserId(null); setFormData({ username: '', password: '', email: '', confirmPassword: '', role: 'user', group_ids: [] }); } } }; window.addEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown); }, [showCreateModal, showEditModal]); const { data: users = [], isLoading } = useQuery({ queryKey: ['users'], queryFn: () => api.getUsers(), enabled: hasPermission('users:read'), }); const { data: groups = [] } = useQuery({ queryKey: ['groups'], queryFn: () => api.getGroups(), enabled: hasPermission('groups:read'), }); const createMutation = useMutation({ mutationFn: (data: UserCreate) => api.createUser(data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['users'] }); queryClient.invalidateQueries({ queryKey: ['groups'] }); setShowCreateModal(false); setFormData({ username: '', password: '', email: '', confirmPassword: '', role: 'user', group_ids: [] }); showToast(t('users.toast.created')); }, onError: (error: Error) => { showToast(error.message, 'error'); }, }); const updateMutation = useMutation({ mutationFn: ({ id, data }: { id: number; data: UserUpdate }) => api.updateUser(id, data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['users'] }); queryClient.invalidateQueries({ queryKey: ['groups'] }); setShowEditModal(false); setEditingUserId(null); setFormData({ username: '', password: '', email: '', confirmPassword: '', role: 'user', group_ids: [] }); showToast(t('users.toast.updated')); }, onError: (error: Error) => { showToast(error.message, 'error'); }, }); const deleteMutation = useMutation({ mutationFn: (id: number) => api.deleteUser(id), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['users'] }); showToast(t('users.toast.deleted')); }, onError: (error: Error) => { showToast(error.message, 'error'); }, }); const resetPasswordMutation = useMutation({ mutationFn: (userId: number) => api.resetUserPassword({ user_id: userId }), onSuccess: (data) => { showToast(data.message, 'success'); }, onError: (error: Error) => { showToast(error.message, 'error'); }, }); // Validation for create user button const isCreateButtonDisabled = useMemo(() => { if (createMutation.isPending || !formData.username) { return true; } if (advancedAuthStatus?.advanced_auth_enabled) { // When advanced auth is enabled, require email (password is auto-generated) return !formData.email; } // When advanced auth is disabled, require valid password return !formData.password || formData.password !== formData.confirmPassword || formData.password.length < 6; }, [ createMutation.isPending, formData.username, formData.email, formData.password, formData.confirmPassword, advancedAuthStatus?.advanced_auth_enabled ]); const handleCreate = () => { // Use the status from the query hook const advancedAuthEnabled = advancedAuthStatus?.advanced_auth_enabled || false; if (!formData.username) { const errorMsg = t('users.toast.fillRequired'); showToast(errorMsg, 'error'); if (advancedAuthEnabled) { console.error('[Advanced Auth] Create user failed: Username is required'); } return; } // Email is required when advanced auth is enabled if (advancedAuthEnabled && !formData.email) { const errorMsg = 'Email is required when advanced authentication is enabled'; showToast(errorMsg, 'error'); console.error('[Advanced Auth] Create user failed: Email is required when advanced authentication is enabled'); return; } // Password validation only when advanced auth is disabled if (!advancedAuthEnabled) { if (!formData.password) { showToast(t('users.toast.fillRequired'), 'error'); return; } if (formData.password !== formData.confirmPassword) { showToast(t('users.toast.passwordsDoNotMatch'), 'error'); return; } if (formData.password.length < 6) { showToast(t('users.toast.passwordTooShort'), 'error'); return; } } createMutation.mutate({ username: formData.username, password: advancedAuthEnabled ? undefined : formData.password, email: formData.email || undefined, role: formData.role, group_ids: formData.group_ids.length > 0 ? formData.group_ids : undefined, }); }; const handleUpdate = (id: number) => { // Validate password confirmation if a new password is being set if (formData.password) { if (formData.password !== formData.confirmPassword) { showToast(t('users.toast.passwordsDoNotMatch'), 'error'); return; } if (formData.password.length < 6) { showToast(t('users.toast.passwordTooShort'), 'error'); return; } } const updateData: UserUpdate = { username: formData.username || undefined, password: formData.password || undefined, email: formData.email || undefined, role: formData.role, group_ids: formData.group_ids, }; // Remove password if empty if (!updateData.password) { delete updateData.password; } // Remove email if empty if (!updateData.email) { delete updateData.email; } updateMutation.mutate({ id, data: updateData }); }; const handleDelete = (id: number) => { setDeleteUserId(id); }; const startEdit = (user: UserResponse) => { setEditingUserId(user.id); setFormData({ username: user.username, password: '', email: user.email || '', confirmPassword: '', role: user.role, group_ids: user.groups?.map(g => g.id) || [], }); setShowEditModal(true); }; const closeEditModal = () => { setShowEditModal(false); setEditingUserId(null); setFormData({ username: '', password: '', email: '', confirmPassword: '', role: 'user', group_ids: [] }); }; const toggleGroup = (groupId: number) => { setFormData(prev => ({ ...prev, group_ids: prev.group_ids.includes(groupId) ? prev.group_ids.filter(id => id !== groupId) : [...prev.group_ids, groupId], })); }; if (!hasPermission('users:read')) { return (

{t('users.noPermission')}

); } return (

{t('users.title')}

{t('users.subtitle')}

{isLoading ? (
) : (
{users.map((user) => ( ))}
{t('users.table.username')} {t('users.table.groups')} {t('users.table.status')} {t('users.table.actions')}
{user.username}
{user.is_admin && ( {t('users.admin')} )} {user.groups?.map(group => ( {group.name} ))} {(!user.groups || user.groups.length === 0) && !user.is_admin && ( {t('users.noGroups')} )}
{user.is_active ? t('users.active') : t('users.inactive')}
{user.id !== currentUser?.id && ( )} {advancedAuthStatus?.advanced_auth_enabled && user.email && user.id !== currentUser?.id && ( )}
)} {/* Create User Modal */} {showCreateModal && !advancedAuthStatus?.advanced_auth_enabled && (
{ setShowCreateModal(false); setBasicCreateTab('local'); setFormData({ username: '', password: '', email: '', confirmPassword: '', role: 'user', group_ids: [] }); }} > e.stopPropagation()} >

{t('users.modal.createUser')}

{ldapStatus?.ldap_enabled && (
)} {basicCreateTab === 'ldap' && ldapStatus?.ldap_enabled ? ( <> { setShowCreateModal(false); setBasicCreateTab('local'); setFormData({ username: '', password: '', email: '', confirmPassword: '', role: 'user', group_ids: [] }); showToast(t('users.toast.ldapProvisioned', { username: user.username })); }} />
) : ( <>
setFormData({ ...formData, username: e.target.value })} className="w-full px-4 py-3 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-lg text-white placeholder-bambu-gray focus:outline-none focus:ring-2 focus:ring-bambu-green/50 focus:border-bambu-green transition-colors" placeholder={t('users.form.usernamePlaceholder')} autoComplete="username" />
setFormData({ ...formData, password: e.target.value })} className="w-full px-4 py-3 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-lg text-white placeholder-bambu-gray focus:outline-none focus:ring-2 focus:ring-bambu-green/50 focus:border-bambu-green transition-colors" placeholder={t('users.form.passwordPlaceholder')} autoComplete="new-password" minLength={6} />
setFormData({ ...formData, confirmPassword: e.target.value })} className={`w-full px-4 py-3 bg-bambu-dark-secondary border rounded-lg text-white placeholder-bambu-gray focus:outline-none focus:ring-2 focus:ring-bambu-green/50 focus:border-bambu-green transition-colors ${ formData.confirmPassword && formData.password !== formData.confirmPassword ? 'border-red-500' : 'border-bambu-dark-tertiary' }`} placeholder={t('users.form.confirmPasswordPlaceholder')} autoComplete="new-password" minLength={6} /> {formData.confirmPassword && formData.password !== formData.confirmPassword && (

{t('users.toast.passwordsDoNotMatch')}

)}
{groups.map(group => ( ))} {groups.length === 0 && (

{t('users.noGroupsAvailable')}

)}
)}
)} {/* Create User Modal - Advanced Authentication */} {showCreateModal && advancedAuthStatus?.advanced_auth_enabled && ( { setShowCreateModal(false); setFormData({ username: '', password: '', email: '', confirmPassword: '', role: 'user', group_ids: [] }); }} onCreate={handleCreate} isCreating={createMutation.isPending} isCreateButtonDisabled={isCreateButtonDisabled} ldapEnabled={ldapStatus?.ldap_enabled} onLdapProvisioned={(user) => { setShowCreateModal(false); setFormData({ username: '', password: '', email: '', confirmPassword: '', role: 'user', group_ids: [] }); showToast(t('users.toast.ldapProvisioned', { username: user.username })); }} /> )} {/* Edit User Modal */} {showEditModal && editingUserId !== null && (
e.stopPropagation()} >

{t('users.modal.editUser')}

setFormData({ ...formData, username: e.target.value })} className="w-full px-4 py-3 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-lg text-white placeholder-bambu-gray focus:outline-none focus:ring-2 focus:ring-bambu-green/50 focus:border-bambu-green transition-colors" placeholder={t('users.form.usernamePlaceholder')} autoComplete="username" />
setFormData({ ...formData, email: e.target.value })} className="w-full px-4 py-3 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-lg text-white placeholder-bambu-gray focus:outline-none focus:ring-2 focus:ring-bambu-green/50 focus:border-bambu-green transition-colors" placeholder={t('users.form.emailPlaceholder') || 'user@example.com'} />
setFormData({ ...formData, password: e.target.value, confirmPassword: '' })} className="w-full px-4 py-3 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-lg text-white placeholder-bambu-gray focus:outline-none focus:ring-2 focus:ring-bambu-green/50 focus:border-bambu-green transition-colors" placeholder={t('users.form.newPasswordPlaceholder')} autoComplete="new-password" minLength={6} />
{formData.password && (
setFormData({ ...formData, confirmPassword: e.target.value })} className={`w-full px-4 py-3 bg-bambu-dark-secondary border rounded-lg text-white placeholder-bambu-gray focus:outline-none focus:ring-2 focus:ring-bambu-green/50 focus:border-bambu-green transition-colors ${ formData.confirmPassword && formData.password !== formData.confirmPassword ? 'border-red-500' : 'border-bambu-dark-tertiary' }`} placeholder={t('users.form.confirmNewPasswordPlaceholder')} autoComplete="new-password" minLength={6} /> {formData.confirmPassword && formData.password !== formData.confirmPassword && (

{t('users.toast.passwordsDoNotMatch')}

)}
)}
{groups.map(group => ( ))} {groups.length === 0 && (

{t('users.noGroupsAvailable')}

)}
)} {/* Delete Confirmation Modal */} {deleteUserId !== null && ( { deleteMutation.mutate(deleteUserId); setDeleteUserId(null); }} onCancel={() => setDeleteUserId(null)} /> )}
); }