Files
redlight/server/routes/admin.js
Michelle c281628fdc
All checks were successful
Build & Push Docker Image / build (push) Successful in 6m30s
Update README and configuration to replace RSA with Ed25519 for federation security
2026-02-28 20:19:59 +01:00

167 lines
5.9 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 - (330 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;