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)
-
+
)}
|