diff --git a/frontend/src/pages/UsersPage.tsx b/frontend/src/pages/UsersPage.tsx index b38fac6f5..0a8a10cfd 100644 --- a/frontend/src/pages/UsersPage.tsx +++ b/frontend/src/pages/UsersPage.tsx @@ -2,7 +2,7 @@ import { useState, useEffect } 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 } from 'lucide-react'; +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'; @@ -14,6 +14,7 @@ import { ConfirmModal } from '../components/ConfirmModal'; interface FormData extends UserCreate { group_ids: number[]; confirmPassword: string; + email?: string; } export function UsersPage() { @@ -29,23 +30,30 @@ export function UsersPage() { const [formData, setFormData] = useState({ username: '', password: '', + email: '', confirmPassword: '', role: 'user', group_ids: [], }); + // Check if advanced auth is enabled + const { data: advancedAuthStatus } = useQuery({ + queryKey: ['advancedAuthStatus'], + queryFn: () => api.getAdvancedAuthStatus(), + }); + // Close modal on Escape key useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if (e.key === 'Escape') { if (showCreateModal) { setShowCreateModal(false); - setFormData({ username: '', password: '', confirmPassword: '', role: 'user', group_ids: [] }); + setFormData({ username: '', password: '', email: '', confirmPassword: '', role: 'user', group_ids: [] }); } if (showEditModal) { setShowEditModal(false); setEditingUserId(null); - setFormData({ username: '', password: '', confirmPassword: '', role: 'user', group_ids: [] }); + setFormData({ username: '', password: '', email: '', confirmPassword: '', role: 'user', group_ids: [] }); } } }; @@ -71,7 +79,7 @@ export function UsersPage() { queryClient.invalidateQueries({ queryKey: ['users'] }); queryClient.invalidateQueries({ queryKey: ['groups'] }); setShowCreateModal(false); - setFormData({ username: '', password: '', confirmPassword: '', role: 'user', group_ids: [] }); + setFormData({ username: '', password: '', email: '', confirmPassword: '', role: 'user', group_ids: [] }); showToast(t('users.toast.created')); }, onError: (error: Error) => { @@ -86,7 +94,7 @@ export function UsersPage() { queryClient.invalidateQueries({ queryKey: ['groups'] }); setShowEditModal(false); setEditingUserId(null); - setFormData({ username: '', password: '', confirmPassword: '', role: 'user', group_ids: [] }); + setFormData({ username: '', password: '', email: '', confirmPassword: '', role: 'user', group_ids: [] }); showToast(t('users.toast.updated')); }, onError: (error: Error) => { @@ -105,22 +113,50 @@ export function UsersPage() { }, }); + const resetPasswordMutation = useMutation({ + mutationFn: (userId: number) => api.resetUserPassword({ user_id: userId }), + onSuccess: (data) => { + showToast(data.message, 'success'); + }, + onError: (error: Error) => { + showToast(error.message, 'error'); + }, + }); + const handleCreate = () => { - if (!formData.username || !formData.password) { + const advancedAuthEnabled = advancedAuthStatus?.advanced_auth_enabled; + + if (!formData.username) { showToast(t('users.toast.fillRequired'), 'error'); return; } - if (formData.password !== formData.confirmPassword) { - showToast(t('users.toast.passwordsDoNotMatch'), 'error'); + + // Email is required when advanced auth is enabled + if (advancedAuthEnabled && !formData.email) { + showToast('Email is required when advanced authentication is enabled', 'error'); return; } - if (formData.password.length < 6) { - showToast(t('users.toast.passwordTooShort'), 'error'); - 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: formData.password, + password: advancedAuthEnabled ? undefined : formData.password, + email: formData.email || undefined, role: formData.role, group_ids: formData.group_ids.length > 0 ? formData.group_ids : undefined, }); @@ -141,6 +177,7 @@ export function UsersPage() { const updateData: UserUpdate = { username: formData.username || undefined, password: formData.password || undefined, + email: formData.email || undefined, role: formData.role, group_ids: formData.group_ids, }; @@ -148,6 +185,10 @@ export function UsersPage() { if (!updateData.password) { delete updateData.password; } + // Remove email if empty + if (!updateData.email) { + delete updateData.email; + } updateMutation.mutate({ id, data: updateData }); }; @@ -160,6 +201,7 @@ export function UsersPage() { setFormData({ username: user.username, password: '', + email: user.email || '', confirmPassword: '', role: user.role, group_ids: user.groups?.map(g => g.id) || [], @@ -170,7 +212,7 @@ export function UsersPage() { const closeEditModal = () => { setShowEditModal(false); setEditingUserId(null); - setFormData({ username: '', password: '', confirmPassword: '', role: 'user', group_ids: [] }); + setFormData({ username: '', password: '', email: '', confirmPassword: '', role: 'user', group_ids: [] }); }; const toggleGroup = (groupId: number) => { @@ -221,7 +263,7 @@ export function UsersPage() { )} + {advancedAuthStatus?.advanced_auth_enabled && user.email && user.id !== currentUser?.id && ( + + )} @@ -332,7 +385,7 @@ export function UsersPage() { className="fixed inset-0 bg-black/70 flex items-center justify-center z-50 p-4" onClick={() => { setShowCreateModal(false); - setFormData({ username: '', password: '', confirmPassword: '', role: 'user', group_ids: [] }); + setFormData({ username: '', password: '', email: '', confirmPassword: '', role: 'user', group_ids: [] }); }} > { setShowCreateModal(false); - setFormData({ username: '', password: '', confirmPassword: '', role: 'user', group_ids: [] }); + setFormData({ username: '', password: '', email: '', confirmPassword: '', role: 'user', group_ids: [] }); }} > @@ -372,41 +425,67 @@ export function UsersPage() { 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')}

- )} -
+ {advancedAuthStatus?.advanced_auth_enabled && ( +
+ + 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'} + required={advancedAuthStatus?.advanced_auth_enabled} + /> +
+ )} + {!advancedAuthStatus?.advanced_auth_enabled && ( + <> +
+ + 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')}

+ )} +
+ + )} + {advancedAuthStatus?.advanced_auth_enabled && ( +
+

+ {t('users.form.autoGeneratedPassword') || 'A secure password will be automatically generated and emailed to the user.'} +

+
+ )}
+
+ + 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'} + /> +