CreateUserAdvancedAuthModal.tsx 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238
  1. import { useEffect, useState } from 'react';
  2. import { useTranslation } from 'react-i18next';
  3. import { X, Plus, Loader2, Users as UsersIcon } from 'lucide-react';
  4. import { Card, CardContent, CardHeader } from './Card';
  5. import { Button } from './Button';
  6. import { LdapUserPicker } from './LdapUserPicker';
  7. import type { Group, UserCreate, UserResponse } from '../api/client';
  8. interface AdvancedAuthFormData extends UserCreate {
  9. group_ids: number[];
  10. confirmPassword: string;
  11. email?: string;
  12. }
  13. interface CreateUserAdvancedAuthModalProps {
  14. formData: AdvancedAuthFormData;
  15. setFormData: (data: AdvancedAuthFormData) => void;
  16. groups: Group[];
  17. onClose: () => void;
  18. onCreate: () => void;
  19. isCreating: boolean;
  20. isCreateButtonDisabled: boolean;
  21. // When LDAP is enabled in settings, the modal shows a "LDAP" tab beside
  22. // "Local"; the picker handles its own provision call and reports success
  23. // back through onLdapProvisioned.
  24. ldapEnabled?: boolean;
  25. onLdapProvisioned?: (user: UserResponse) => void;
  26. }
  27. type Tab = 'local' | 'ldap';
  28. export function CreateUserAdvancedAuthModal({
  29. formData,
  30. setFormData,
  31. groups,
  32. onClose,
  33. onCreate,
  34. isCreating,
  35. isCreateButtonDisabled,
  36. ldapEnabled = false,
  37. onLdapProvisioned,
  38. }: CreateUserAdvancedAuthModalProps) {
  39. const { t } = useTranslation();
  40. const [tab, setTab] = useState<Tab>('local');
  41. // Close modal on Escape key
  42. useEffect(() => {
  43. const handleKeyDown = (e: KeyboardEvent) => {
  44. if (e.key === 'Escape') {
  45. onClose();
  46. }
  47. };
  48. window.addEventListener('keydown', handleKeyDown);
  49. return () => window.removeEventListener('keydown', handleKeyDown);
  50. }, [onClose]);
  51. const toggleGroup = (groupId: number) => {
  52. setFormData({
  53. ...formData,
  54. group_ids: formData.group_ids.includes(groupId)
  55. ? formData.group_ids.filter(id => id !== groupId)
  56. : [...formData.group_ids, groupId],
  57. });
  58. };
  59. return (
  60. <div
  61. className="fixed inset-0 bg-black/70 flex items-center justify-center z-50 p-4"
  62. onClick={onClose}
  63. >
  64. <Card
  65. className="w-full max-w-md"
  66. onClick={(e: React.MouseEvent) => e.stopPropagation()}
  67. >
  68. <CardHeader>
  69. <div className="flex items-center justify-between">
  70. <div className="flex flex-col gap-1">
  71. <div className="flex items-center gap-2">
  72. <UsersIcon className="w-5 h-5 text-bambu-green" />
  73. <h2 className="text-lg font-semibold text-white">{t('users.modal.createUser')}</h2>
  74. </div>
  75. <p className="text-sm text-bambu-gray ml-7">{t('users.modal.advancedAuthSubtitle') || 'with Advanced Authentication'}</p>
  76. </div>
  77. <Button
  78. variant="ghost"
  79. size="sm"
  80. onClick={onClose}
  81. >
  82. <X className="w-5 h-5" />
  83. </Button>
  84. </div>
  85. </CardHeader>
  86. <CardContent>
  87. {ldapEnabled && (
  88. <div
  89. className="mb-4 flex items-center gap-1 p-1 bg-bambu-dark-secondary rounded-lg"
  90. role="tablist"
  91. aria-label={t('users.modal.tabsAriaLabel')}
  92. >
  93. <button
  94. type="button"
  95. role="tab"
  96. aria-selected={tab === 'local'}
  97. onClick={() => setTab('local')}
  98. className={`flex-1 px-3 py-2 text-sm rounded-md transition-colors ${
  99. tab === 'local'
  100. ? 'bg-bambu-green/15 text-bambu-green'
  101. : 'text-bambu-gray hover:text-white'
  102. }`}
  103. >
  104. {t('users.modal.localTab')}
  105. </button>
  106. <button
  107. type="button"
  108. role="tab"
  109. aria-selected={tab === 'ldap'}
  110. onClick={() => setTab('ldap')}
  111. className={`flex-1 px-3 py-2 text-sm rounded-md transition-colors ${
  112. tab === 'ldap'
  113. ? 'bg-bambu-green/15 text-bambu-green'
  114. : 'text-bambu-gray hover:text-white'
  115. }`}
  116. >
  117. {t('users.modal.ldapTab')}
  118. </button>
  119. </div>
  120. )}
  121. {tab === 'ldap' && ldapEnabled ? (
  122. <LdapUserPicker
  123. onSuccess={(user) => {
  124. onLdapProvisioned?.(user);
  125. }}
  126. />
  127. ) : (
  128. <div className="space-y-4">
  129. {/* Username Field */}
  130. <div>
  131. <label className="block text-sm font-medium text-white mb-2">
  132. {t('users.form.username')} <span className="text-red-400">*</span>
  133. </label>
  134. <input
  135. type="text"
  136. value={formData.username}
  137. onChange={(e) => setFormData({ ...formData, username: e.target.value })}
  138. 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"
  139. placeholder={t('users.form.usernamePlaceholder')}
  140. autoComplete="username"
  141. required
  142. />
  143. </div>
  144. {/* Email Field */}
  145. <div>
  146. <label className="block text-sm font-medium text-white mb-2">
  147. {t('users.form.email') || 'Email'} <span className="text-red-400">*</span>
  148. </label>
  149. <input
  150. type="email"
  151. value={formData.email}
  152. onChange={(e) => setFormData({ ...formData, email: e.target.value })}
  153. 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"
  154. placeholder={t('users.form.emailPlaceholder') || 'user@example.com'}
  155. required
  156. />
  157. </div>
  158. {/* Info box about auto-generated password */}
  159. <div className="bg-bambu-dark-secondary/50 border border-bambu-green/20 rounded-lg p-3">
  160. <p className="text-sm text-bambu-gray">
  161. {t('users.form.autoGeneratedPassword') || 'A secure password will be automatically generated and emailed to the user.'}
  162. </p>
  163. </div>
  164. {/* Groups Field */}
  165. <div>
  166. <label className="block text-sm font-medium text-white mb-2">
  167. {t('users.form.groups')}
  168. </label>
  169. <div className="space-y-2 max-h-40 overflow-y-auto p-2 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-lg">
  170. {groups.map(group => (
  171. <label
  172. key={group.id}
  173. className="flex items-center gap-3 px-2 py-1.5 rounded hover:bg-bambu-dark-tertiary cursor-pointer"
  174. >
  175. <input
  176. type="checkbox"
  177. checked={formData.group_ids.includes(group.id)}
  178. onChange={() => toggleGroup(group.id)}
  179. className="w-4 h-4 rounded border-bambu-gray text-bambu-green focus:ring-bambu-green focus:ring-offset-0 bg-bambu-dark"
  180. />
  181. <span className="text-sm text-white">{group.name}</span>
  182. {group.is_system && (
  183. <span className="text-xs text-yellow-400">({t('users.system')})</span>
  184. )}
  185. </label>
  186. ))}
  187. {groups.length === 0 && (
  188. <p className="text-sm text-bambu-gray">{t('users.noGroupsAvailable')}</p>
  189. )}
  190. </div>
  191. </div>
  192. </div>
  193. )}
  194. {/* Action Buttons — Cancel always shown; Create only on local tab
  195. (LDAP picker has its own submit). */}
  196. <div className="mt-6 flex justify-end gap-3">
  197. <Button
  198. variant="secondary"
  199. onClick={onClose}
  200. >
  201. {t('users.modal.cancel')}
  202. </Button>
  203. {tab === 'local' && (
  204. <Button
  205. onClick={onCreate}
  206. disabled={isCreateButtonDisabled}
  207. >
  208. {isCreating ? (
  209. <>
  210. <Loader2 className="w-4 h-4 animate-spin" />
  211. {t('users.modal.creating')}
  212. </>
  213. ) : (
  214. <>
  215. <Plus className="w-4 h-4" />
  216. {t('users.modal.createUser')}
  217. </>
  218. )}
  219. </Button>
  220. )}
  221. </div>
  222. </CardContent>
  223. </Card>
  224. </div>
  225. );
  226. }