import React, { useEffect, useState, useRef } from 'react'; import { useTranslation } from 'react-i18next'; import { useNavigate } from 'react-router-dom'; import { ChevronDownIcon, CheckIcon, PencilIcon, TrashIcon, } from '@heroicons/react/24/outline'; import ConfirmDialog from '../Shared/ConfirmDialog'; import { getApiPath } from '../../config/paths'; import { useToast } from '../Shared/ToastContext'; import { fetchWithCsrf } from '../../utils/csrfService'; interface AdminUserItem { id: number; email: string; name?: string; surname?: string; created_at: string; role: 'admin' | 'user'; } const fetchAdminUsers = async (t: any): Promise => { const res = await fetch(getApiPath('admin/users'), { credentials: 'include', headers: { Accept: 'application/json' }, }); if (res.status === 401) throw new Error( t('admin.authenticationRequired', 'Authentication required') ); if (res.status === 403) throw new Error(t('admin.forbidden', 'Forbidden')); if (!res.ok) throw new Error(t('admin.failedToLoadUsers', 'Failed to load users')); return await res.json(); }; const createAdminUser = async ( email: string, password: string, t: any, name?: string, surname?: string, role?: 'admin' | 'user' ): Promise => { const res = await fetchWithCsrf(getApiPath('admin/users'), { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json', Accept: 'application/json', }, body: JSON.stringify({ email, password, name, surname, role }), }); if (res.status === 401) throw new Error( t('admin.authenticationRequired', 'Authentication required') ); if (res.status === 403) throw new Error(t('admin.forbidden', 'Forbidden')); if (res.status === 409) throw new Error(t('admin.emailAlreadyExists', 'Email already exists')); if (!res.ok) { let message = t('admin.failedToCreateUser', 'Failed to create user'); try { const body = await res.json(); if (body?.error) message = body.error; } catch { // ignore non-JSON error bodies } throw new Error(message); } return await res.json(); }; const updateAdminUser = async ( id: number, email: string, t: any, name?: string, surname?: string, role?: 'admin' | 'user', password?: string ): Promise => { const body: any = { email, name, surname, role }; if (password) body.password = password; const res = await fetchWithCsrf(getApiPath(`admin/users/${id}`), { method: 'PUT', credentials: 'include', headers: { 'Content-Type': 'application/json', Accept: 'application/json', }, body: JSON.stringify(body), }); if (res.status === 401) throw new Error( t('admin.authenticationRequired', 'Authentication required') ); if (res.status === 403) throw new Error(t('admin.forbidden', 'Forbidden')); if (res.status === 409) throw new Error(t('admin.emailAlreadyExists', 'Email already exists')); if (res.status === 404) throw new Error(t('admin.userNotFound', 'User not found')); if (!res.ok) { let message = t('admin.failedToUpdateUser', 'Failed to update user'); try { const body = await res.json(); if (body?.error) message = body.error; } catch { // ignore non-JSON error bodies } throw new Error(message); } return await res.json(); }; const deleteAdminUser = async (id: number, t: any): Promise => { const res = await fetchWithCsrf(getApiPath(`admin/users/${id}`), { method: 'DELETE', credentials: 'include', headers: { Accept: 'application/json' }, }); if (res.status === 401) throw new Error( t('admin.authenticationRequired', 'Authentication required') ); if (res.status === 403) throw new Error(t('admin.forbidden', 'Forbidden')); if (res.status === 400) { const body = await res .json() .catch(() => ({ error: t('admin.badRequest', 'Bad request') })); throw new Error(body.error || t('admin.badRequest', 'Bad request')); } if (res.status === 404) throw new Error(t('admin.userNotFound', 'User not found')); if (!res.ok && res.status !== 204) throw new Error(t('admin.failedToDeleteUser', 'Failed to delete user')); }; const AddUserModal: React.FC<{ isOpen: boolean; onClose: () => void; onCreated: (user: AdminUserItem) => void; onUpdated: (user: AdminUserItem) => void; editingUser?: AdminUserItem | null; }> = ({ isOpen, onClose, onCreated, onUpdated, editingUser }) => { const { t } = useTranslation(); const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); const [name, setName] = useState(''); const [surname, setSurname] = useState(''); const [role, setRole] = useState<'user' | 'admin'>('user'); const [submitting, setSubmitting] = useState(false); const [error, setError] = useState(null); const [isRoleDropdownOpen, setIsRoleDropdownOpen] = useState(false); const roleDropdownRef = useRef(null); const isValidEmail = (value: string) => { // Simple email format validation return /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/.test(value); }; useEffect(() => { if (isOpen) { if (editingUser) { setEmail(editingUser.email); setPassword(''); setName(editingUser.name || ''); setSurname(editingUser.surname || ''); setRole(editingUser.role); } else { setEmail(''); setPassword(''); setName(''); setSurname(''); setRole('user'); } setError(null); setIsRoleDropdownOpen(false); } }, [isOpen, editingUser]); // Close dropdown when clicking outside useEffect(() => { const handleClickOutside = (event: MouseEvent) => { if ( roleDropdownRef.current && !roleDropdownRef.current.contains(event.target as Node) ) { setIsRoleDropdownOpen(false); } }; if (isRoleDropdownOpen) { document.addEventListener('mousedown', handleClickOutside); } return () => { document.removeEventListener('mousedown', handleClickOutside); }; }, [isRoleDropdownOpen]); if (!isOpen) return null; const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setError(null); if (!email) { setError(t('errors.required', 'This field is required')); return; } if (!isValidEmail(email)) { setError(t('errors.invalidEmail', 'Invalid email address')); return; } // Password is required for new users, optional for updates if (!editingUser && !password) { setError(t('errors.required', 'This field is required')); return; } setSubmitting(true); try { if (editingUser) { const user = await updateAdminUser( editingUser.id, email, t, name, surname, role, password || undefined ); onUpdated(user); } else { const user = await createAdminUser( email, password, t, name, surname, role ); onCreated(user); } onClose(); } catch (err: any) { setError( err.message || (editingUser ? t('admin.failedToUpdateUser', 'Failed to update user') : t( 'admin.failedToCreateUser', 'Failed to create user' )) ); } finally { setSubmitting(false); } }; return (
e.stopPropagation()} >

{editingUser ? t('admin.editUser', 'Edit user') : t('admin.addUser', 'Add user')}

setEmail(e.target.value)} required />
setName(e.target.value)} />
setSurname(e.target.value)} />
setPassword(e.target.value)} required={!editingUser} minLength={6} />
{isRoleDropdownOpen && (
)}
{error && (
{error}
)}
); }; const AdminUsersPage: React.FC = () => { const { t } = useTranslation(); const { showSuccessToast, showErrorToast } = useToast(); const [users, setUsers] = useState(null); const [error, setError] = useState(null); const [loading, setLoading] = useState(true); const [addOpen, setAddOpen] = useState(false); const [editingUser, setEditingUser] = useState(null); const [userToDelete, setUserToDelete] = useState( null ); const [registrationEnabled, setRegistrationEnabled] = useState(false); const [registrationLoading, setRegistrationLoading] = useState(true); const navigate = useNavigate(); // Fetch registration status useEffect(() => { const fetchRegistrationStatus = async () => { try { const res = await fetch(getApiPath('registration-status'), { credentials: 'include', }); if (res.ok) { const data = await res.json(); setRegistrationEnabled(data.enabled); } } catch (err) { console.error('Error fetching registration status:', err); } finally { setRegistrationLoading(false); } }; fetchRegistrationStatus(); }, []); // Toggle registration const toggleRegistration = async () => { try { const res = await fetchWithCsrf(getApiPath('admin/toggle-registration'), { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ enabled: !registrationEnabled }), }); if (res.ok) { const data = await res.json(); setRegistrationEnabled(data.enabled); } else { setError( t( 'admin.failedToToggleRegistration', 'Failed to toggle registration' ) ); } } catch { setError( t( 'admin.failedToToggleRegistration', 'Failed to toggle registration' ) ); } }; const load = async () => { setLoading(true); setError(null); try { const data = await fetchAdminUsers(t); setUsers(data); } catch (err: any) { setError( err.message || t('admin.failedToLoadUsers', 'Failed to load users') ); if (err.message === t('admin.forbidden', 'Forbidden')) navigate('/today'); } finally { setLoading(false); } }; useEffect(() => { load(); }, []); const handleDeleteUser = async () => { if (!userToDelete) return; try { await deleteAdminUser(userToDelete.id, t); setUsers((prev) => prev ? prev.filter((u) => u.id !== userToDelete.id) : null ); showSuccessToast( t('admin.userDeletedSuccessfully', 'User deleted successfully') ); setUserToDelete(null); } catch (err: any) { setError( err.message || t('admin.failedToDeleteUser', 'Failed to delete user') ); showErrorToast( err.message || t('admin.failedToDeleteUser', 'Failed to delete user') ); setUserToDelete(null); } }; return (

{t('admin.userManagement', 'User Management')}

{/* Registration Toggle */}

{t( 'admin.userRegistration', 'User Registration' )}

{t( 'admin.registrationDescription', 'Allow new users to register via email verification' )}

{error && (
{error}
)}
{loading && ( )} {!loading && users && users.length === 0 && ( )} {!loading && users && users.map((u) => ( ))}
{t('admin.email', 'Email')} {t('admin.name', 'Name')} {t('admin.surname', 'Surname')} {t('admin.created', 'Created')} {t('admin.role', 'Role')} {t('admin.actions', 'Actions')}
{t( 'admin.loadingUsers', 'Loading users...' )}
{t('admin.noUsers', 'No users')}
{u.email} {u.name || '-'} {u.surname || '-'} {new Date( u.created_at ).toLocaleString()} {u.role === 'admin' ? t('admin.admin', 'admin') : t('admin.user', 'user')}
{ setAddOpen(false); setEditingUser(null); }} onCreated={(user) => setUsers((prev) => (prev ? [user, ...prev] : [user])) } onUpdated={(user) => setUsers((prev) => prev ? prev.map((u) => (u.id === user.id ? user : u)) : [user] ) } editingUser={editingUser} /> {userToDelete && ( setUserToDelete(null)} /> )}
); }; export default AdminUsersPage;