Browse Source

Create separate advanced auth create user modal and restore original modal

Co-authored-by: cadtoolbox <12723486+cadtoolbox@users.noreply.github.com>
copilot-swe-agent[bot] 3 months ago
parent
commit
ffd8eb5903

+ 180 - 0
frontend/src/components/CreateUserAdvancedAuthModal.tsx

@@ -0,0 +1,180 @@
+import { useEffect } from 'react';
+import { useTranslation } from 'react-i18next';
+import { X, Plus, Loader2, Users as UsersIcon } from 'lucide-react';
+import { Card, CardContent, CardHeader } from './Card';
+import { Button } from './Button';
+import type { Group, UserCreate } from '../api/client';
+
+interface AdvancedAuthFormData extends UserCreate {
+  group_ids: number[];
+  confirmPassword: string;
+  email?: string;
+}
+
+interface CreateUserAdvancedAuthModalProps {
+  formData: AdvancedAuthFormData;
+  setFormData: (data: AdvancedAuthFormData) => void;
+  groups: Group[];
+  onClose: () => void;
+  onCreate: () => void;
+  isCreating: boolean;
+  isCreateButtonDisabled: boolean;
+}
+
+export function CreateUserAdvancedAuthModal({
+  formData,
+  setFormData,
+  groups,
+  onClose,
+  onCreate,
+  isCreating,
+  isCreateButtonDisabled,
+}: CreateUserAdvancedAuthModalProps) {
+  const { t } = useTranslation();
+
+  // Close modal on Escape key
+  useEffect(() => {
+    const handleKeyDown = (e: KeyboardEvent) => {
+      if (e.key === 'Escape') {
+        onClose();
+      }
+    };
+    window.addEventListener('keydown', handleKeyDown);
+    return () => window.removeEventListener('keydown', handleKeyDown);
+  }, [onClose]);
+
+  const toggleGroup = (groupId: number) => {
+    setFormData({
+      ...formData,
+      group_ids: formData.group_ids.includes(groupId)
+        ? formData.group_ids.filter(id => id !== groupId)
+        : [...formData.group_ids, groupId],
+    });
+  };
+
+  return (
+    <div
+      className="fixed inset-0 bg-black/70 flex items-center justify-center z-50 p-4"
+      onClick={onClose}
+    >
+      <Card
+        className="w-full max-w-md"
+        onClick={(e: React.MouseEvent) => e.stopPropagation()}
+      >
+        <CardHeader>
+          <div className="flex items-center justify-between">
+            <div className="flex flex-col gap-1">
+              <div className="flex items-center gap-2">
+                <UsersIcon className="w-5 h-5 text-bambu-green" />
+                <h2 className="text-lg font-semibold text-white">{t('users.modal.createUser')}</h2>
+              </div>
+              <p className="text-sm text-bambu-gray ml-7">with Advanced Authentication</p>
+            </div>
+            <Button
+              variant="ghost"
+              size="sm"
+              onClick={onClose}
+            >
+              <X className="w-5 h-5" />
+            </Button>
+          </div>
+        </CardHeader>
+        <CardContent>
+          <div className="space-y-4">
+            {/* Username Field */}
+            <div>
+              <label className="block text-sm font-medium text-white mb-2">
+                {t('users.form.username')}
+              </label>
+              <input
+                type="text"
+                value={formData.username}
+                onChange={(e) => 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"
+              />
+            </div>
+
+            {/* Email Field */}
+            <div>
+              <label className="block text-sm font-medium text-white mb-2">
+                {t('users.form.email') || 'Email'} <span className="text-red-400">*</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'}
+                required
+              />
+            </div>
+
+            {/* Info box about auto-generated password */}
+            <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>
+
+            {/* Groups Field */}
+            <div>
+              <label className="block text-sm font-medium text-white mb-2">
+                {t('users.form.groups')}
+              </label>
+              <div className="space-y-2 max-h-40 overflow-y-auto p-2 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-lg">
+                {groups.map(group => (
+                  <label
+                    key={group.id}
+                    className="flex items-center gap-3 px-2 py-1.5 rounded hover:bg-bambu-dark-tertiary cursor-pointer"
+                  >
+                    <input
+                      type="checkbox"
+                      checked={formData.group_ids.includes(group.id)}
+                      onChange={() => toggleGroup(group.id)}
+                      className="w-4 h-4 rounded border-bambu-gray text-bambu-green focus:ring-bambu-green focus:ring-offset-0 bg-bambu-dark"
+                    />
+                    <span className="text-sm text-white">{group.name}</span>
+                    {group.is_system && (
+                      <span className="text-xs text-yellow-400">({t('users.system')})</span>
+                    )}
+                  </label>
+                ))}
+                {groups.length === 0 && (
+                  <p className="text-sm text-bambu-gray">{t('users.noGroupsAvailable')}</p>
+                )}
+              </div>
+            </div>
+          </div>
+
+          {/* Action Buttons */}
+          <div className="mt-6 flex justify-end gap-3">
+            <Button
+              variant="secondary"
+              onClick={onClose}
+            >
+              {t('users.modal.cancel')}
+            </Button>
+            <Button
+              onClick={onCreate}
+              disabled={isCreateButtonDisabled}
+            >
+              {isCreating ? (
+                <>
+                  <Loader2 className="w-4 h-4 animate-spin" />
+                  {t('users.modal.creating')}
+                </>
+              ) : (
+                <>
+                  <Plus className="w-4 h-4" />
+                  {t('users.modal.createUser')}
+                </>
+              )}
+            </Button>
+          </div>
+        </CardContent>
+      </Card>
+    </div>
+  );
+}

+ 61 - 64
frontend/src/pages/UsersPage.tsx

@@ -10,6 +10,7 @@ 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';
 
 interface FormData extends UserCreate {
   group_ids: number[];
@@ -148,13 +149,19 @@ export function UsersPage() {
     const advancedAuthEnabled = advancedAuthStatus?.advanced_auth_enabled || false;
     
     if (!formData.username) {
-      showToast(t('users.toast.fillRequired'), 'error');
+      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) {
-      showToast('Email is required when advanced authentication is enabled', 'error');
+      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;
     }
     
@@ -401,7 +408,7 @@ export function UsersPage() {
       )}
 
       {/* Create User Modal */}
-      {showCreateModal && (
+      {showCreateModal && !advancedAuthStatus?.advanced_auth_enabled && (
         <div
           className="fixed inset-0 bg-black/70 flex items-center justify-center z-50 p-4"
           onClick={() => {
@@ -446,67 +453,41 @@ export function UsersPage() {
                     autoComplete="username"
                   />
                 </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.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>
                 <div>
                   <label className="block text-sm font-medium text-white mb-2">
                     {t('users.form.groups')}
@@ -567,6 +548,22 @@ export function UsersPage() {
         </div>
       )}
 
+      {/* Create User Modal - Advanced Authentication */}
+      {showCreateModal && advancedAuthStatus?.advanced_auth_enabled && (
+        <CreateUserAdvancedAuthModal
+          formData={formData}
+          setFormData={setFormData}
+          groups={groups}
+          onClose={() => {
+            setShowCreateModal(false);
+            setFormData({ username: '', password: '', email: '', confirmPassword: '', role: 'user', group_ids: [] });
+          }}
+          onCreate={handleCreate}
+          isCreating={createMutation.isPending}
+          isCreateButtonDisabled={isCreateButtonDisabled}
+        />
+      )}
+
       {/* Edit User Modal */}
       {showEditModal && editingUserId !== null && (
         <div

+ 0 - 0
static/assets/index-C99bJlwT.js → static/assets/index-DSqSK4QY.js


File diff suppressed because it is too large
+ 0 - 0
static/assets/index-DbCAeYz5.css


File diff suppressed because it is too large
+ 0 - 0
static/assets/index-e6thg2aZ.css


+ 2 - 2
static/index.html

@@ -23,8 +23,8 @@
 
     <!-- Splash screens for iOS -->
     <link rel="apple-touch-startup-image" href="/img/android-chrome-512x512.png" />
-    <script type="module" crossorigin src="/assets/index-C99bJlwT.js"></script>
-    <link rel="stylesheet" crossorigin href="/assets/index-e6thg2aZ.css">
+    <script type="module" crossorigin src="/assets/index-DSqSK4QY.js"></script>
+    <link rel="stylesheet" crossorigin href="/assets/index-DbCAeYz5.css">
   </head>
   <body>
     <div id="root"></div>

Some files were not shown because too many files changed in this diff