| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238 |
- import { useEffect, useState } 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 { LdapUserPicker } from './LdapUserPicker';
- import type { Group, UserCreate, UserResponse } 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;
- // When LDAP is enabled in settings, the modal shows a "LDAP" tab beside
- // "Local"; the picker handles its own provision call and reports success
- // back through onLdapProvisioned.
- ldapEnabled?: boolean;
- onLdapProvisioned?: (user: UserResponse) => void;
- }
- type Tab = 'local' | 'ldap';
- export function CreateUserAdvancedAuthModal({
- formData,
- setFormData,
- groups,
- onClose,
- onCreate,
- isCreating,
- isCreateButtonDisabled,
- ldapEnabled = false,
- onLdapProvisioned,
- }: CreateUserAdvancedAuthModalProps) {
- const { t } = useTranslation();
- const [tab, setTab] = useState<Tab>('local');
- // 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">{t('users.modal.advancedAuthSubtitle') || 'with Advanced Authentication'}</p>
- </div>
- <Button
- variant="ghost"
- size="sm"
- onClick={onClose}
- >
- <X className="w-5 h-5" />
- </Button>
- </div>
- </CardHeader>
- <CardContent>
- {ldapEnabled && (
- <div
- className="mb-4 flex items-center gap-1 p-1 bg-bambu-dark-secondary rounded-lg"
- role="tablist"
- aria-label={t('users.modal.tabsAriaLabel')}
- >
- <button
- type="button"
- role="tab"
- aria-selected={tab === 'local'}
- onClick={() => setTab('local')}
- className={`flex-1 px-3 py-2 text-sm rounded-md transition-colors ${
- tab === 'local'
- ? 'bg-bambu-green/15 text-bambu-green'
- : 'text-bambu-gray hover:text-white'
- }`}
- >
- {t('users.modal.localTab')}
- </button>
- <button
- type="button"
- role="tab"
- aria-selected={tab === 'ldap'}
- onClick={() => setTab('ldap')}
- className={`flex-1 px-3 py-2 text-sm rounded-md transition-colors ${
- tab === 'ldap'
- ? 'bg-bambu-green/15 text-bambu-green'
- : 'text-bambu-gray hover:text-white'
- }`}
- >
- {t('users.modal.ldapTab')}
- </button>
- </div>
- )}
- {tab === 'ldap' && ldapEnabled ? (
- <LdapUserPicker
- onSuccess={(user) => {
- onLdapProvisioned?.(user);
- }}
- />
- ) : (
- <div className="space-y-4">
- {/* Username Field */}
- <div>
- <label className="block text-sm font-medium text-white mb-2">
- {t('users.form.username')} <span className="text-red-400">*</span>
- </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"
- required
- />
- </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 — Cancel always shown; Create only on local tab
- (LDAP picker has its own submit). */}
- <div className="mt-6 flex justify-end gap-3">
- <Button
- variant="secondary"
- onClick={onClose}
- >
- {t('users.modal.cancel')}
- </Button>
- {tab === 'local' && (
- <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>
- );
- }
|