| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810 |
- import { useState, useEffect, useMemo } 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, RotateCcw } from 'lucide-react';
- import { api } from '../api/client';
- import type { UserCreate, UserUpdate, UserResponse } from '../api/client';
- import { useAuth } from '../contexts/AuthContext';
- 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';
- import { LdapUserPicker } from '../components/LdapUserPicker';
- interface FormData extends UserCreate {
- group_ids: number[];
- confirmPassword: string;
- email?: string;
- }
- export function UsersPage() {
- const navigate = useNavigate();
- const { t } = useTranslation();
- const { user: currentUser, hasPermission } = useAuth();
- const { showToast } = useToast();
- const queryClient = useQueryClient();
- const [showCreateModal, setShowCreateModal] = useState(false);
- // Basic-mode (non-advanced-auth) modal: track which tab is active so the
- // LDAP picker can replace the local form when LDAP is enabled.
- const [basicCreateTab, setBasicCreateTab] = useState<'local' | 'ldap'>('local');
- const [showEditModal, setShowEditModal] = useState(false);
- const [editingUserId, setEditingUserId] = useState<number | null>(null);
- const [deleteUserId, setDeleteUserId] = useState<number | null>(null);
- const [formData, setFormData] = useState<FormData>({
- username: '',
- password: '',
- email: '',
- confirmPassword: '',
- role: 'user',
- group_ids: [],
- });
- // Check if advanced auth is enabled
- const { data: advancedAuthStatus = { advanced_auth_enabled: false, smtp_configured: false } } = useQuery({
- queryKey: ['advancedAuthStatus'],
- queryFn: () => api.getAdvancedAuthStatus(),
- });
- // LDAP status — drives whether the LDAP tab is rendered in the create modal.
- const { data: ldapStatus = { ldap_enabled: false, ldap_configured: false } } = useQuery({
- queryKey: ['ldapStatus'],
- queryFn: () => api.getLDAPStatus(),
- });
- // Close modal on Escape key
- useEffect(() => {
- const handleKeyDown = (e: KeyboardEvent) => {
- if (e.key === 'Escape') {
- if (showCreateModal) {
- setShowCreateModal(false);
- setBasicCreateTab('local');
- setFormData({ username: '', password: '', email: '', confirmPassword: '', role: 'user', group_ids: [] });
- }
- if (showEditModal) {
- setShowEditModal(false);
- setEditingUserId(null);
- setFormData({ username: '', password: '', email: '', confirmPassword: '', role: 'user', group_ids: [] });
- }
- }
- };
- window.addEventListener('keydown', handleKeyDown);
- return () => window.removeEventListener('keydown', handleKeyDown);
- }, [showCreateModal, showEditModal]);
- const { data: users = [], isLoading } = useQuery({
- queryKey: ['users'],
- queryFn: () => api.getUsers(),
- enabled: hasPermission('users:read'),
- });
- const { data: groups = [] } = useQuery({
- queryKey: ['groups'],
- queryFn: () => api.getGroups(),
- enabled: hasPermission('groups:read'),
- });
- const createMutation = useMutation({
- mutationFn: (data: UserCreate) => api.createUser(data),
- onSuccess: () => {
- queryClient.invalidateQueries({ queryKey: ['users'] });
- queryClient.invalidateQueries({ queryKey: ['groups'] });
- setShowCreateModal(false);
- setFormData({ username: '', password: '', email: '', confirmPassword: '', role: 'user', group_ids: [] });
- showToast(t('users.toast.created'));
- },
- onError: (error: Error) => {
- showToast(error.message, 'error');
- },
- });
- const updateMutation = useMutation({
- mutationFn: ({ id, data }: { id: number; data: UserUpdate }) => api.updateUser(id, data),
- onSuccess: () => {
- queryClient.invalidateQueries({ queryKey: ['users'] });
- queryClient.invalidateQueries({ queryKey: ['groups'] });
- setShowEditModal(false);
- setEditingUserId(null);
- setFormData({ username: '', password: '', email: '', confirmPassword: '', role: 'user', group_ids: [] });
- showToast(t('users.toast.updated'));
- },
- onError: (error: Error) => {
- showToast(error.message, 'error');
- },
- });
- const deleteMutation = useMutation({
- mutationFn: (id: number) => api.deleteUser(id),
- onSuccess: () => {
- queryClient.invalidateQueries({ queryKey: ['users'] });
- showToast(t('users.toast.deleted'));
- },
- onError: (error: Error) => {
- showToast(error.message, 'error');
- },
- });
- const resetPasswordMutation = useMutation({
- mutationFn: (userId: number) => api.resetUserPassword({ user_id: userId }),
- onSuccess: (data) => {
- showToast(data.message, 'success');
- },
- onError: (error: Error) => {
- showToast(error.message, 'error');
- },
- });
- // Validation for create user button
- const isCreateButtonDisabled = useMemo(() => {
- if (createMutation.isPending || !formData.username) {
- return true;
- }
- if (advancedAuthStatus?.advanced_auth_enabled) {
- // When advanced auth is enabled, require email (password is auto-generated)
- return !formData.email;
- }
- // When advanced auth is disabled, require valid password
- return !formData.password || formData.password !== formData.confirmPassword || formData.password.length < 6;
- }, [
- createMutation.isPending,
- formData.username,
- formData.email,
- formData.password,
- formData.confirmPassword,
- advancedAuthStatus?.advanced_auth_enabled
- ]);
- const handleCreate = () => {
- // Use the status from the query hook
- const advancedAuthEnabled = advancedAuthStatus?.advanced_auth_enabled || false;
- if (!formData.username) {
- 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) {
- 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;
- }
- // 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: advancedAuthEnabled ? undefined : formData.password,
- email: formData.email || undefined,
- role: formData.role,
- group_ids: formData.group_ids.length > 0 ? formData.group_ids : undefined,
- });
- };
- const handleUpdate = (id: number) => {
- // Validate password confirmation if a new password is being set
- if (formData.password) {
- if (formData.password !== formData.confirmPassword) {
- showToast(t('users.toast.passwordsDoNotMatch'), 'error');
- return;
- }
- if (formData.password.length < 6) {
- showToast(t('users.toast.passwordTooShort'), 'error');
- return;
- }
- }
- const updateData: UserUpdate = {
- username: formData.username || undefined,
- password: formData.password || undefined,
- email: formData.email || undefined,
- role: formData.role,
- group_ids: formData.group_ids,
- };
- // Remove password if empty
- if (!updateData.password) {
- delete updateData.password;
- }
- // Remove email if empty
- if (!updateData.email) {
- delete updateData.email;
- }
- updateMutation.mutate({ id, data: updateData });
- };
- const handleDelete = (id: number) => {
- setDeleteUserId(id);
- };
- const startEdit = (user: UserResponse) => {
- setEditingUserId(user.id);
- setFormData({
- username: user.username,
- password: '',
- email: user.email || '',
- confirmPassword: '',
- role: user.role,
- group_ids: user.groups?.map(g => g.id) || [],
- });
- setShowEditModal(true);
- };
- const closeEditModal = () => {
- setShowEditModal(false);
- setEditingUserId(null);
- setFormData({ username: '', password: '', email: '', confirmPassword: '', role: 'user', group_ids: [] });
- };
- const toggleGroup = (groupId: number) => {
- setFormData(prev => ({
- ...prev,
- group_ids: prev.group_ids.includes(groupId)
- ? prev.group_ids.filter(id => id !== groupId)
- : [...prev.group_ids, groupId],
- }));
- };
- if (!hasPermission('users:read')) {
- return (
- <div className="p-6">
- <Card>
- <CardContent className="py-6">
- <div className="flex items-center gap-3 text-red-400">
- <Shield className="w-5 h-5" />
- <p className="text-white">{t('users.noPermission')}</p>
- </div>
- </CardContent>
- </Card>
- </div>
- );
- }
- return (
- <div className="p-6">
- <div className="flex justify-between items-center mb-6">
- <div className="flex items-center gap-4">
- <button
- onClick={() => navigate('/settings?tab=users')}
- className="p-2 rounded-lg bg-bambu-dark-secondary hover:bg-bambu-dark-tertiary text-bambu-gray hover:text-white transition-colors"
- title={t('users.backToSettings')}
- >
- <ArrowLeft className="w-5 h-5" />
- </button>
- <div>
- <h1 className="text-2xl font-bold text-white flex items-center gap-2">
- <UsersIcon className="w-6 h-6 text-bambu-green" />
- {t('users.title')}
- </h1>
- <p className="text-sm text-bambu-gray mt-1">
- {t('users.subtitle')}
- </p>
- </div>
- </div>
- <Button
- onClick={() => {
- setShowCreateModal(true);
- setFormData({ username: '', password: '', email: '', confirmPassword: '', role: 'user', group_ids: [] });
- }}
- >
- <Plus className="w-4 h-4" />
- {t('users.createUser')}
- </Button>
- </div>
- {isLoading ? (
- <div className="flex items-center justify-center py-12">
- <Loader2 className="w-8 h-8 text-bambu-green animate-spin" />
- </div>
- ) : (
- <Card>
- <div className="overflow-x-auto">
- <table className="min-w-full divide-y divide-bambu-dark-tertiary">
- <thead>
- <tr>
- <th className="px-6 py-3 text-left text-xs font-medium text-bambu-gray uppercase tracking-wider">
- {t('users.table.username')}
- </th>
- <th className="px-6 py-3 text-left text-xs font-medium text-bambu-gray uppercase tracking-wider">
- {t('users.table.groups')}
- </th>
- <th className="px-6 py-3 text-left text-xs font-medium text-bambu-gray uppercase tracking-wider">
- {t('users.table.status')}
- </th>
- <th className="px-6 py-3 text-left text-xs font-medium text-bambu-gray uppercase tracking-wider">
- {t('users.table.actions')}
- </th>
- </tr>
- </thead>
- <tbody className="divide-y divide-bambu-dark-tertiary">
- {users.map((user) => (
- <tr key={user.id} className="hover:bg-bambu-dark-tertiary/50 transition-colors">
- <td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-white">
- {user.username}
- </td>
- <td className="px-6 py-4 text-sm">
- <div className="flex flex-wrap gap-1">
- {user.is_admin && (
- <span className="px-2 py-0.5 rounded-full text-xs font-medium bg-purple-500/20 text-purple-300">
- {t('users.admin')}
- </span>
- )}
- {user.groups?.map(group => (
- <span
- key={group.id}
- className={`px-2 py-0.5 rounded-full text-xs font-medium ${
- group.name === 'Administrators'
- ? 'bg-purple-500/20 text-purple-300'
- : group.name === 'Operators'
- ? 'bg-blue-500/20 text-blue-300'
- : group.name === 'Viewers'
- ? 'bg-green-500/20 text-green-300'
- : 'bg-gray-500/20 text-gray-300'
- }`}
- >
- {group.name}
- </span>
- ))}
- {(!user.groups || user.groups.length === 0) && !user.is_admin && (
- <span className="text-bambu-gray">{t('users.noGroups')}</span>
- )}
- </div>
- </td>
- <td className="px-6 py-4 whitespace-nowrap text-sm">
- <span className={`px-3 py-1 rounded-full text-xs font-medium ${
- user.is_active
- ? 'bg-bambu-green/20 text-bambu-green'
- : 'bg-red-500/20 text-red-400'
- }`}>
- {user.is_active ? t('users.active') : t('users.inactive')}
- </span>
- </td>
- <td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
- <div className="flex items-center gap-2">
- <Button
- size="sm"
- variant="ghost"
- onClick={() => startEdit(user)}
- >
- <Edit2 className="w-4 h-4" />
- {t('users.edit')}
- </Button>
- {user.id !== currentUser?.id && (
- <Button
- size="sm"
- variant="ghost"
- onClick={() => handleDelete(user.id)}
- >
- <Trash2 className="w-4 h-4" />
- {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.form.resetPassword') || 'Reset Password'}
- </Button>
- )}
- </div>
- </td>
- </tr>
- ))}
- </tbody>
- </table>
- </div>
- </Card>
- )}
- {/* Create User Modal */}
- {showCreateModal && !advancedAuthStatus?.advanced_auth_enabled && (
- <div
- className="fixed inset-0 bg-black/70 flex items-center justify-center z-50 p-4"
- onClick={() => {
- setShowCreateModal(false);
- setBasicCreateTab('local');
- setFormData({ username: '', password: '', email: '', confirmPassword: '', role: 'user', group_ids: [] });
- }}
- >
- <Card
- className="w-full max-w-md"
- onClick={(e: React.MouseEvent) => e.stopPropagation()}
- >
- <CardHeader>
- <div className="flex items-center justify-between">
- <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>
- <Button
- variant="ghost"
- size="sm"
- onClick={() => {
- setShowCreateModal(false);
- setBasicCreateTab('local');
- setFormData({ username: '', password: '', email: '', confirmPassword: '', role: 'user', group_ids: [] });
- }}
- >
- <X className="w-5 h-5" />
- </Button>
- </div>
- </CardHeader>
- <CardContent>
- {ldapStatus?.ldap_enabled && (
- <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={basicCreateTab === 'local'}
- onClick={() => setBasicCreateTab('local')}
- className={`flex-1 px-3 py-2 text-sm rounded-md transition-colors ${
- basicCreateTab === '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={basicCreateTab === 'ldap'}
- onClick={() => setBasicCreateTab('ldap')}
- className={`flex-1 px-3 py-2 text-sm rounded-md transition-colors ${
- basicCreateTab === 'ldap'
- ? 'bg-bambu-green/15 text-bambu-green'
- : 'text-bambu-gray hover:text-white'
- }`}
- >
- {t('users.modal.ldapTab')}
- </button>
- </div>
- )}
- {basicCreateTab === 'ldap' && ldapStatus?.ldap_enabled ? (
- <>
- <LdapUserPicker
- onSuccess={(user) => {
- setShowCreateModal(false);
- setBasicCreateTab('local');
- setFormData({ username: '', password: '', email: '', confirmPassword: '', role: 'user', group_ids: [] });
- showToast(t('users.toast.ldapProvisioned', { username: user.username }));
- }}
- />
- <div className="mt-6 flex justify-end">
- <Button
- variant="secondary"
- onClick={() => {
- setShowCreateModal(false);
- setBasicCreateTab('local');
- setFormData({ username: '', password: '', email: '', confirmPassword: '', role: 'user', group_ids: [] });
- }}
- >
- {t('users.modal.cancel')}
- </Button>
- </div>
- </>
- ) : (
- <>
- <div className="space-y-4">
- <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>
- <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')}
- </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>
- <div className="mt-6 flex justify-end gap-3">
- <Button
- variant="secondary"
- onClick={() => {
- setShowCreateModal(false);
- setFormData({ username: '', password: '', email: '', confirmPassword: '', role: 'user', group_ids: [] });
- }}
- >
- {t('users.modal.cancel')}
- </Button>
- <Button
- onClick={handleCreate}
- disabled={isCreateButtonDisabled}
- >
- {createMutation.isPending ? (
- <>
- <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>
- )}
- {/* 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}
- ldapEnabled={ldapStatus?.ldap_enabled}
- onLdapProvisioned={(user) => {
- setShowCreateModal(false);
- setFormData({ username: '', password: '', email: '', confirmPassword: '', role: 'user', group_ids: [] });
- showToast(t('users.toast.ldapProvisioned', { username: user.username }));
- }}
- />
- )}
- {/* Edit User Modal */}
- {showEditModal && editingUserId !== null && (
- <div
- className="fixed inset-0 bg-black/70 flex items-center justify-center z-50 p-4"
- onClick={closeEditModal}
- >
- <Card
- className="w-full max-w-md"
- onClick={(e: React.MouseEvent) => e.stopPropagation()}
- >
- <CardHeader>
- <div className="flex items-center justify-between">
- <div className="flex items-center gap-2">
- <Edit2 className="w-5 h-5 text-bambu-green" />
- <h2 className="text-lg font-semibold text-white">{t('users.modal.editUser')}</h2>
- </div>
- <Button
- variant="ghost"
- size="sm"
- onClick={closeEditModal}
- >
- <X className="w-5 h-5" />
- </Button>
- </div>
- </CardHeader>
- <CardContent>
- <div className="space-y-4">
- <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>
- <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>
- </label>
- <input
- type="password"
- value={formData.password}
- onChange={(e) => setFormData({ ...formData, password: e.target.value, confirmPassword: '' })}
- 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.newPasswordPlaceholder')}
- autoComplete="new-password"
- minLength={6}
- />
- </div>
- {formData.password && (
- <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.confirmNewPasswordPlaceholder')}
- 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')}
- </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>
- <div className="mt-6 flex justify-end gap-3">
- <Button
- variant="secondary"
- onClick={closeEditModal}
- >
- {t('users.modal.cancel')}
- </Button>
- <Button
- onClick={() => handleUpdate(editingUserId)}
- disabled={updateMutation.isPending || !formData.username || !!(formData.password && (formData.password !== formData.confirmPassword || formData.password.length < 6))}
- >
- {updateMutation.isPending ? (
- <>
- <Loader2 className="w-4 h-4 animate-spin" />
- {t('users.modal.saving')}
- </>
- ) : (
- <>
- <Save className="w-4 h-4" />
- {t('users.modal.saveChanges')}
- </>
- )}
- </Button>
- </div>
- </CardContent>
- </Card>
- </div>
- )}
- {/* Delete Confirmation Modal */}
- {deleteUserId !== null && (
- <ConfirmModal
- title={t('users.deleteModal.title')}
- message={t('users.deleteModal.message')}
- confirmText={t('users.deleteModal.confirm')}
- variant="danger"
- onConfirm={() => {
- deleteMutation.mutate(deleteUserId);
- setDeleteUserId(null);
- }}
- onCancel={() => setDeleteUserId(null)}
- />
- )}
- </div>
- );
- }
|