فهرست منبع

Add email field and reset password to UsersPage

Co-authored-by: cadtoolbox <12723486+cadtoolbox@users.noreply.github.com>
copilot-swe-agent[bot] 3 ماه پیش
والد
کامیت
726401810c
1فایلهای تغییر یافته به همراه143 افزوده شده و 52 حذف شده
  1. 143 52
      frontend/src/pages/UsersPage.tsx

+ 143 - 52
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<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>