mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-21 10:15:04 +02:00
Add email field and reset password to UsersPage
Co-authored-by: cadtoolbox <12723486+cadtoolbox@users.noreply.github.com>
This commit is contained in:
co-authored by
cadtoolbox
parent
6bdd5730de
commit
726401810c
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user