const express = require('express'); const bcrypt = require('bcryptjs'); const { randomUUID } = require('crypto'); const db = require('../db'); const asyncHandler = require('../middleware/asyncHandler'); const { requireAdmin } = require('../middleware/auth'); const router = express.Router(); // All routes here are admin-only. router.use(requireAdmin); function publicShape(row) { if (!row) return null; return { uuid: row.uuid, username: row.username, displayName: row.display_name, isAdmin: !!row.is_admin, createdAt: row.created_at, }; } async function countAdmins() { const row = await db.get('SELECT COUNT(*) AS n FROM users WHERE is_admin = 1'); return row ? row.n : 0; } router.get('/', asyncHandler(async (req, res) => { const rows = await db.all( 'SELECT id, uuid, username, display_name, is_admin, created_at FROM users ORDER BY username' ); res.json(rows.map(publicShape)); })); router.post('/', asyncHandler(async (req, res) => { const { username, password, displayName, isAdmin } = req.body || {}; if (!username || !username.trim()) return res.status(400).json({ error: 'Username ist erforderlich' }); if (!password || password.length < 8) return res.status(400).json({ error: 'Passwort muss mindestens 8 Zeichen lang sein' }); const hash = await bcrypt.hash(password, 10); const uuid = randomUUID(); try { const result = await db.run( 'INSERT INTO users (uuid, username, password_hash, display_name, is_admin) VALUES (?, ?, ?, ?, ?)', [uuid, username.trim(), hash, displayName ? displayName.trim() : username.trim(), isAdmin ? 1 : 0] ); const row = await db.get('SELECT id, uuid, username, display_name, is_admin, created_at FROM users WHERE id = ?', [result.lastID]); res.json(publicShape(row)); } catch (err) { if (/UNIQUE/.test(err.message)) { return res.status(400).json({ error: 'Username existiert bereits' }); } throw err; } })); router.put('/:uuid', asyncHandler(async (req, res) => { const { uuid } = req.params; const { username, displayName, isAdmin, newPassword } = req.body || {}; const user = await db.get('SELECT * FROM users WHERE uuid = ?', [uuid]); if (!user) return res.status(404).json({ error: 'Benutzer nicht gefunden' }); const isSelf = user.id === req.session.userId; // Protect against revoking the last admin's status. if (isAdmin !== undefined && !isAdmin && user.is_admin) { const admins = await countAdmins(); if (admins <= 1) { return res.status(400).json({ error: 'Der letzte Administrator kann nicht degradiert werden' }); } } // Don't let an admin demote themselves accidentally. if (isSelf && isAdmin === false) { return res.status(400).json({ error: 'Eigene Admin-Rechte können nicht entzogen werden' }); } const nextUsername = (username !== undefined && username !== null) ? String(username).trim() : user.username; if (!nextUsername) return res.status(400).json({ error: 'Username darf nicht leer sein' }); const nextDisplay = (displayName !== undefined) ? (displayName ? String(displayName).trim() : null) : user.display_name; const nextIsAdmin = (isAdmin !== undefined) ? (isAdmin ? 1 : 0) : user.is_admin; let nextHash = user.password_hash; if (newPassword) { if (newPassword.length < 8) return res.status(400).json({ error: 'Neues Passwort muss mindestens 8 Zeichen lang sein' }); nextHash = await bcrypt.hash(newPassword, 10); } try { await db.run( 'UPDATE users SET username = ?, display_name = ?, is_admin = ?, password_hash = ? WHERE uuid = ?', [nextUsername, nextDisplay, nextIsAdmin, nextHash, uuid] ); } catch (err) { if (/UNIQUE/.test(err.message)) { return res.status(400).json({ error: 'Username existiert bereits' }); } throw err; } // Keep session in sync if the current user renamed themselves or changed admin. if (isSelf) { req.session.username = nextUsername; req.session.isAdmin = !!nextIsAdmin; } const fresh = await db.get('SELECT id, uuid, username, display_name, is_admin, created_at FROM users WHERE uuid = ?', [uuid]); res.json(publicShape(fresh)); })); router.delete('/:uuid', asyncHandler(async (req, res) => { const { uuid } = req.params; const user = await db.get('SELECT * FROM users WHERE uuid = ?', [uuid]); if (!user) return res.status(404).json({ error: 'Benutzer nicht gefunden' }); if (user.id === req.session.userId) { return res.status(400).json({ error: 'Eigener Account kann nicht gelöscht werden' }); } if (user.is_admin) { const admins = await countAdmins(); if (admins <= 1) { return res.status(400).json({ error: 'Der letzte Administrator kann nicht gelöscht werden' }); } } await db.run('DELETE FROM users WHERE uuid = ?', [uuid]); res.json({ ok: true }); })); module.exports = router;