All checks were successful
Build & Push Docker Image / build (push) Successful in 6m30s
167 lines
5.9 KiB
JavaScript
167 lines
5.9 KiB
JavaScript
import { Router } from 'express';
|
||
import bcrypt from 'bcryptjs';
|
||
import { getDb } from '../config/database.js';
|
||
import { authenticateToken, requireAdmin } from '../middleware/auth.js';
|
||
|
||
const EMAIL_RE = /^[^\s@]{1,64}@[^\s@]{1,253}\.[^\s@]{2,}$/;
|
||
|
||
const router = Router();
|
||
|
||
// POST /api/admin/users - Create user (admin)
|
||
router.post('/users', authenticateToken, requireAdmin, async (req, res) => {
|
||
try {
|
||
const { name, display_name, email, password, role } = req.body;
|
||
|
||
if (!name || !display_name || !email || !password) {
|
||
return res.status(400).json({ error: 'All fields are required' });
|
||
}
|
||
|
||
// L4: display_name length limit
|
||
if (display_name.length > 100) {
|
||
return res.status(400).json({ error: 'Display name must not exceed 100 characters' });
|
||
}
|
||
|
||
const usernameRegex = /^[a-zA-Z0-9_-]{3,30}$/;
|
||
if (!usernameRegex.test(name)) {
|
||
return res.status(400).json({ error: 'Username may only contain letters, numbers, _ and - (3–30 chars)' });
|
||
}
|
||
|
||
if (password.length < 8) {
|
||
return res.status(400).json({ error: 'Password must be at least 8 characters long' });
|
||
}
|
||
|
||
// M9: email format validation
|
||
if (!EMAIL_RE.test(email)) {
|
||
return res.status(400).json({ error: 'Invalid email address' });
|
||
}
|
||
|
||
const validRole = ['user', 'admin'].includes(role) ? role : 'user';
|
||
const db = getDb();
|
||
|
||
const existing = await db.get('SELECT id FROM users WHERE email = ?', [email.toLowerCase()]);
|
||
if (existing) {
|
||
return res.status(409).json({ error: 'Email is already in use' });
|
||
}
|
||
|
||
const existingUsername = await db.get('SELECT id FROM users WHERE LOWER(name) = LOWER(?)', [name]);
|
||
if (existingUsername) {
|
||
return res.status(409).json({ error: 'Username is already taken' });
|
||
}
|
||
|
||
const hash = bcrypt.hashSync(password, 12);
|
||
const result = await db.run(
|
||
'INSERT INTO users (name, display_name, email, password_hash, role, email_verified) VALUES (?, ?, ?, ?, ?, 1)',
|
||
[name, display_name, email.toLowerCase(), hash, validRole]
|
||
);
|
||
|
||
const user = await db.get('SELECT id, name, display_name, email, role, created_at FROM users WHERE id = ?', [result.lastInsertRowid]);
|
||
res.status(201).json({ user });
|
||
} catch (err) {
|
||
console.error('Create user error:', err);
|
||
res.status(500).json({ error: 'User could not be created' });
|
||
}
|
||
});
|
||
|
||
// GET /api/admin/users - List all users
|
||
router.get('/users', authenticateToken, requireAdmin, async (req, res) => {
|
||
try {
|
||
const db = getDb();
|
||
const users = await db.all(`
|
||
SELECT id, name, display_name, email, role, language, theme, avatar_color, avatar_image, created_at,
|
||
(SELECT COUNT(*) FROM rooms WHERE rooms.user_id = users.id) as room_count
|
||
FROM users
|
||
ORDER BY created_at DESC
|
||
`);
|
||
|
||
res.json({ users });
|
||
} catch (err) {
|
||
console.error('List users error:', err);
|
||
res.status(500).json({ error: 'Users could not be loaded' });
|
||
}
|
||
});
|
||
|
||
// PUT /api/admin/users/:id/role - Update user role
|
||
router.put('/users/:id/role', authenticateToken, requireAdmin, async (req, res) => {
|
||
try {
|
||
const { role } = req.body;
|
||
if (!['user', 'admin'].includes(role)) {
|
||
return res.status(400).json({ error: 'Invalid role' });
|
||
}
|
||
|
||
const db = getDb();
|
||
|
||
// Prevent demoting last admin
|
||
if (role === 'user') {
|
||
const adminCount = await db.get('SELECT COUNT(*) as count FROM users WHERE role = ?', ['admin']);
|
||
const currentUser = await db.get('SELECT role FROM users WHERE id = ?', [req.params.id]);
|
||
if (currentUser?.role === 'admin' && adminCount.count <= 1) {
|
||
return res.status(400).json({ error: 'The last admin cannot be demoted' });
|
||
}
|
||
}
|
||
|
||
await db.run('UPDATE users SET role = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?', [role, req.params.id]);
|
||
const updated = await db.get('SELECT id, name, email, role, created_at FROM users WHERE id = ?', [req.params.id]);
|
||
|
||
// S7: verify user actually exists
|
||
if (!updated) {
|
||
return res.status(404).json({ error: 'User not found' });
|
||
}
|
||
|
||
res.json({ user: updated });
|
||
} catch (err) {
|
||
console.error('Update role error:', err);
|
||
res.status(500).json({ error: 'Role could not be updated' });
|
||
}
|
||
});
|
||
|
||
// DELETE /api/admin/users/:id - Delete user
|
||
router.delete('/users/:id', authenticateToken, requireAdmin, async (req, res) => {
|
||
try {
|
||
const db = getDb();
|
||
|
||
if (parseInt(req.params.id) === req.user.id) {
|
||
return res.status(400).json({ error: 'You cannot delete yourself' });
|
||
}
|
||
|
||
const user = await db.get('SELECT id, role FROM users WHERE id = ?', [req.params.id]);
|
||
if (!user) {
|
||
return res.status(404).json({ error: 'User not found' });
|
||
}
|
||
|
||
// Check if it's the last admin
|
||
if (user.role === 'admin') {
|
||
const adminCount = await db.get('SELECT COUNT(*) as count FROM users WHERE role = ?', ['admin']);
|
||
if (adminCount.count <= 1) {
|
||
return res.status(400).json({ error: 'The last admin cannot be deleted' });
|
||
}
|
||
}
|
||
|
||
await db.run('DELETE FROM users WHERE id = ?', [req.params.id]);
|
||
res.json({ message: 'User deleted' });
|
||
} catch (err) {
|
||
console.error('Delete user error:', err);
|
||
res.status(500).json({ error: 'User could not be deleted' });
|
||
}
|
||
});
|
||
|
||
// PUT /api/admin/users/:id/password - Reset user password (admin)
|
||
router.put('/users/:id/password', authenticateToken, requireAdmin, async (req, res) => {
|
||
try {
|
||
const { newPassword } = req.body;
|
||
if (!newPassword || typeof newPassword !== 'string' || newPassword.length < 8) {
|
||
return res.status(400).json({ error: 'Password must be at least 8 characters long' });
|
||
}
|
||
|
||
const db = getDb();
|
||
const hash = bcrypt.hashSync(newPassword, 12);
|
||
await db.run('UPDATE users SET password_hash = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?', [hash, req.params.id]);
|
||
|
||
res.json({ message: 'Password reset' });
|
||
} catch (err) {
|
||
console.error('Reset password error:', err);
|
||
res.status(500).json({ error: 'Password could not be reset' });
|
||
}
|
||
});
|
||
|
||
export default router;
|