diff --git a/backend/src/routes/admin.ts b/backend/src/routes/admin.ts index 86a0540..63cfb1c 100644 --- a/backend/src/routes/admin.ts +++ b/backend/src/routes/admin.ts @@ -1,4 +1,5 @@ import { Router, Response, NextFunction } from 'express'; +import bcrypt from 'bcrypt'; import { AuthRequest, authMiddleware } from '../middleware/auth'; import { userQueries, systemSettingsQueries } from '../models'; @@ -38,6 +39,57 @@ router.get('/users', async (_req: AuthRequest, res: Response) => { } }); +// Create a new user +router.post('/users', async (req: AuthRequest, res: Response) => { + try { + const { email, password, is_admin } = req.body; + + if (!email || !password) { + res.status(400).json({ error: 'Email and password are required' }); + return; + } + + if (password.length < 8) { + res.status(400).json({ error: 'Password must be at least 8 characters' }); + return; + } + + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + if (!emailRegex.test(email)) { + res.status(400).json({ error: 'Invalid email format' }); + return; + } + + const existingUser = await userQueries.findByEmail(email); + if (existingUser) { + res.status(409).json({ error: 'Email already registered' }); + return; + } + + const saltRounds = 12; + const passwordHash = await bcrypt.hash(password, saltRounds); + + const user = await userQueries.create(email, passwordHash); + + // Set admin status if specified + if (is_admin) { + await userQueries.setAdmin(user.id, true); + } + + res.status(201).json({ + message: 'User created successfully', + user: { + id: user.id, + email: user.email, + is_admin: is_admin || false, + }, + }); + } catch (error) { + console.error('Error creating user:', error); + res.status(500).json({ error: 'Failed to create user' }); + } +}); + // Delete a user router.delete('/users/:id', async (req: AuthRequest, res: Response) => { try { diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts index be125da..88db844 100644 --- a/frontend/src/api/client.ts +++ b/frontend/src/api/client.ts @@ -172,6 +172,13 @@ export interface SystemSettings { export const adminApi = { getUsers: () => api.get('/admin/users'), + createUser: (email: string, password: string, isAdmin: boolean) => + api.post<{ message: string; user: UserProfile }>('/admin/users', { + email, + password, + is_admin: isAdmin, + }), + deleteUser: (id: number) => api.delete(`/admin/users/${id}`), setUserAdmin: (id: number, isAdmin: boolean) => diff --git a/frontend/src/pages/Settings.tsx b/frontend/src/pages/Settings.tsx index 81f66f6..12ef305 100644 --- a/frontend/src/pages/Settings.tsx +++ b/frontend/src/pages/Settings.tsx @@ -40,6 +40,11 @@ export default function Settings() { const [systemSettings, setSystemSettings] = useState(null); const [isLoadingAdmin, setIsLoadingAdmin] = useState(false); const [isSavingAdmin, setIsSavingAdmin] = useState(false); + const [showAddUser, setShowAddUser] = useState(false); + const [newUserEmail, setNewUserEmail] = useState(''); + const [newUserPassword, setNewUserPassword] = useState(''); + const [newUserRole, setNewUserRole] = useState<'user' | 'admin'>('user'); + const [isCreatingUser, setIsCreatingUser] = useState(false); useEffect(() => { fetchInitialData(); @@ -210,6 +215,35 @@ export default function Settings() { } }; + const handleCreateUser = async () => { + clearMessages(); + if (!newUserEmail || !newUserPassword) { + setError('Email and password are required'); + return; + } + if (newUserPassword.length < 8) { + setError('Password must be at least 8 characters'); + return; + } + setIsCreatingUser(true); + try { + await adminApi.createUser(newUserEmail, newUserPassword, newUserRole === 'admin'); + // Refresh users list + const usersRes = await adminApi.getUsers(); + setUsers(usersRes.data); + setNewUserEmail(''); + setNewUserPassword(''); + setNewUserRole('user'); + setShowAddUser(false); + setSuccess('User created successfully'); + } catch (err: unknown) { + const error = err as { response?: { data?: { error?: string } } }; + setError(error.response?.data?.error || 'Failed to create user'); + } finally { + setIsCreatingUser(false); + } + }; + const handleDeleteUser = async (userId: number) => { if (!confirm('Are you sure you want to delete this user? All their data will be lost.')) { return; @@ -224,14 +258,15 @@ export default function Settings() { } }; - const handleToggleAdmin = async (userId: number, currentStatus: boolean) => { + const handleRoleChange = async (userId: number, newRole: 'user' | 'admin') => { clearMessages(); + const isAdmin = newRole === 'admin'; try { - await adminApi.setUserAdmin(userId, !currentStatus); - setUsers(users.map(u => u.id === userId ? { ...u, is_admin: !currentStatus } : u)); - setSuccess(`Admin status ${!currentStatus ? 'granted' : 'revoked'}`); + await adminApi.setUserAdmin(userId, isAdmin); + setUsers(users.map(u => u.id === userId ? { ...u, is_admin: isAdmin } : u)); + setSuccess(`User role updated to ${newRole}`); } catch { - setError('Failed to update admin status'); + setError('Failed to update user role'); } }; @@ -872,6 +907,82 @@ export default function Settings() { Manage user accounts and permissions.

+ {!showAddUser ? ( + + ) : ( +
+

Add New User

+
+ + setNewUserEmail(e.target.value)} + placeholder="user@example.com" + /> +
+
+ + setNewUserPassword(e.target.value)} + placeholder="Minimum 8 characters" + /> +
+
+ + +
+
+ + +
+
+ )} + {isLoadingAdmin ? (
@@ -893,30 +1004,35 @@ export default function Settings() { {user.email} {user.name || '-'} - {user.is_admin && Admin} + {user.id === profile?.id ? ( + Admin (You) + ) : ( + + )} {new Date(user.created_at).toLocaleDateString()} {user.id !== profile?.id && ( - <> - - - - )} - {user.id === profile?.id && ( - - (You) - + )}