Add email field and reset password to UsersPage

Co-authored-by: cadtoolbox <12723486+cadtoolbox@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2026-02-08 15:28:19 +00:00
co-authored by cadtoolbox
parent 6bdd5730de
commit 726401810c
+143 -52
View File
@@ -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<FormData>({
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() {
<Button
onClick={() => {
setShowCreateModal(true);
setFormData({ username: '', password: '', confirmPassword: '', role: 'user', group_ids: [] });
setFormData({ username: '', password: '', email: '', confirmPassword: '', role: 'user', group_ids: [] });
}}
>
<Plus className="w-4 h-4" />
@@ -316,6 +358,17 @@ export function UsersPage() {
{t('users.delete')}
</Button>
)}
{advancedAuthStatus?.advanced_auth_enabled && user.email && user.id !== currentUser?.id && (
<Button
size="sm"
variant="ghost"
onClick={() => resetPasswordMutation.mutate(user.id)}
disabled={resetPasswordMutation.isPending}
>
<RotateCcw className="w-4 h-4" />
{t('users.resetPassword') || 'Reset Password'}
</Button>
)}
</div>
</td>
</tr>
@@ -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: [] });
}}
>
<Card
@@ -350,7 +403,7 @@ export function UsersPage() {
size="sm"
onClick={() => {
setShowCreateModal(false);
setFormData({ username: '', password: '', confirmPassword: '', role: 'user', group_ids: [] });
setFormData({ username: '', password: '', email: '', confirmPassword: '', role: 'user', group_ids: [] });
}}
>
<X className="w-5 h-5" />
@@ -372,41 +425,67 @@ export function UsersPage() {
autoComplete="username"
/>
</div>
<div>
<label className="block text-sm font-medium text-white mb-2">
{t('users.form.password')}
</label>
<input
type="password"
value={formData.password}
onChange={(e) => 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}
/>
</div>
<div>
<label className="block text-sm font-medium text-white mb-2">
{t('users.form.confirmPassword')}
</label>
<input
type="password"
value={formData.confirmPassword}
onChange={(e) => 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 && (
<p className="text-red-400 text-xs mt-1">{t('users.toast.passwordsDoNotMatch')}</p>
)}
</div>
{advancedAuthStatus?.advanced_auth_enabled && (
<div>
<label className="block text-sm font-medium text-white mb-2">
{t('users.form.email') || 'Email'}
</label>
<input
type="email"
value={formData.email}
onChange={(e) => 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}
/>
</div>
)}
{!advancedAuthStatus?.advanced_auth_enabled && (
<>
<div>
<label className="block text-sm font-medium text-white mb-2">
{t('users.form.password')}
</label>
<input
type="password"
value={formData.password}
onChange={(e) => 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}
/>
</div>
<div>
<label className="block text-sm font-medium text-white mb-2">
{t('users.form.confirmPassword')}
</label>
<input
type="password"
value={formData.confirmPassword}
onChange={(e) => 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 && (
<p className="text-red-400 text-xs mt-1">{t('users.toast.passwordsDoNotMatch')}</p>
)}
</div>
</>
)}
{advancedAuthStatus?.advanced_auth_enabled && (
<div className="bg-bambu-dark-secondary/50 border border-bambu-green/20 rounded-lg p-3">
<p className="text-sm text-bambu-gray">
{t('users.form.autoGeneratedPassword') || 'A secure password will be automatically generated and emailed to the user.'}
</p>
</div>
)}
<div>
<label className="block text-sm font-medium text-white mb-2">
{t('users.form.groups')}
@@ -440,7 +519,7 @@ export function UsersPage() {
variant="secondary"
onClick={() => {
setShowCreateModal(false);
setFormData({ username: '', password: '', confirmPassword: '', role: 'user', group_ids: [] });
setFormData({ username: '', password: '', email: '', confirmPassword: '', role: 'user', group_ids: [] });
}}
>
{t('users.modal.cancel')}
@@ -507,6 +586,18 @@ export function UsersPage() {
autoComplete="username"
/>
</div>
<div>
<label className="block text-sm font-medium text-white mb-2">
{t('users.form.email') || 'Email'} <span className="text-bambu-gray font-normal">({t('users.form.optional') || 'optional'})</span>
</label>
<input
type="email"
value={formData.email}
onChange={(e) => 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'}
/>
</div>
<div>
<label className="block text-sm font-medium text-white mb-2">
{t('users.form.password')} <span className="text-bambu-gray font-normal">({t('users.form.leaveBlankToKeep')})</span>