'use strict'; const express = require('express'); const bcrypt = require('bcryptjs'); const { db } = require('../db'); const { log } = require('../lib/audit'); const { requireAuth, requireAdmin } = require('../middleware/auth'); const { normaliseEmail, validateEmail, validateUsername, validatePassword } = require('../lib/validate'); const router = express.Router(); router.use(requireAuth, requireAdmin); const PAGE = 'Data Management'; function publicUser(u) { const email = (u.email || '').trim(); return { id: u.id, username: u.username, email, role: u.role, mfa_enabled: !!u.mfa_enabled, created_at: u.created_at, // Databases created before this release could hold accounts with no // address. Flag them so an administrator can put one in. email_missing: !email, }; } /** Number of administrators, used to stop the last admin being removed. */ function adminCount() { return db.prepare("SELECT COUNT(*) c FROM users WHERE role = 'admin'").get().c; } // ---- list ------------------------------------------------------------- router.get('/', (req, res) => { const users = db.prepare('SELECT * FROM users ORDER BY role DESC, username').all().map(publicUser); res.json({ users, missing_email: users.filter((u) => u.email_missing).length }); }); // ---- create ----------------------------------------------------------- router.post('/', (req, res) => { const body = req.body || {}; const username = body.username === undefined ? '' : String(body.username).trim(); const email = normaliseEmail(body.email); // mandatory const password = body.password === undefined ? '' : String(body.password); const role = body.role; const err = validateUsername(username) || validateEmail(email) || validatePassword(password); if (err) return res.status(400).json({ error: err }); if (role !== 'admin' && role !== 'user') return res.status(400).json({ error: 'Role must be "admin" or "user".' }); try { const hash = bcrypt.hashSync(password, 12); const info = db .prepare('INSERT INTO users (username, email, password_hash, role) VALUES (?, ?, ?, ?)') .run(username, email, hash, role); const user = db.prepare('SELECT * FROM users WHERE id = ?').get(info.lastInsertRowid); log(req, 'USER_CREATED', PAGE, `Created ${role} "${user.username}" (${user.email})`); res.json({ ok: true, user: publicUser(user) }); } catch (e) { if (String(e.message || '').includes('UNIQUE')) { return res.status(409).json({ error: 'That username or email is already in use.' }); } throw e; } }); // ---- update (email, role, password reset) ----------------------------- router.patch('/:id', (req, res) => { const id = Number(req.params.id); const user = db.prepare('SELECT * FROM users WHERE id = ?').get(id); if (!user) return res.status(404).json({ error: 'User not found.' }); const { email, role, password } = req.body || {}; // Email is mandatory: when the field is supplied it must be a valid address, // so an existing account can never be left without one. if (email !== undefined) { const emailErr = validateEmail(email); if (emailErr) return res.status(400).json({ error: emailErr }); } if (password !== undefined && password !== '') { const pwErr = validatePassword(password); if (pwErr) return res.status(400).json({ error: pwErr }); } // Never let the last administrator demote themselves out of existence. if (role && role !== user.role && user.role === 'admin' && adminCount() <= 1) { return res.status(400).json({ error: 'This is the only administrator — promote someone else first.' }); } if (role && role !== 'admin' && role !== 'user') { return res.status(400).json({ error: 'Role must be "admin" or "user".' }); } const changes = []; try { const newEmail = normaliseEmail(email); if (email !== undefined && newEmail !== user.email) { db.prepare('UPDATE users SET email = ? WHERE id = ?').run(newEmail, id); changes.push(`email → ${newEmail}`); } if (role && role !== user.role) { db.prepare('UPDATE users SET role = ? WHERE id = ?').run(role, id); changes.push(`role → ${role}`); } if (password) { db.prepare('UPDATE users SET password_hash = ? WHERE id = ?').run(bcrypt.hashSync(password, 12), id); changes.push('password reset'); } } catch (e) { if (String(e.message || '').includes('UNIQUE')) { return res.status(409).json({ error: 'That email is already in use.' }); } throw e; } if (changes.length) log(req, 'USER_UPDATED', PAGE, `Updated "${user.username}": ${changes.join(', ')}`); res.json({ ok: true, user: publicUser(db.prepare('SELECT * FROM users WHERE id = ?').get(id)) }); }); // ---- turn off MFA (recovery: user lost their authenticator) ----------- router.post('/:id/mfa/reset', (req, res) => { const id = Number(req.params.id); const user = db.prepare('SELECT * FROM users WHERE id = ?').get(id); if (!user) return res.status(404).json({ error: 'User not found.' }); db.prepare('UPDATE users SET mfa_enabled = 0, mfa_secret = NULL WHERE id = ?').run(id); log(req, 'USER_MFA_RESET', PAGE, `Two-factor authentication reset for "${user.username}"`); res.json({ ok: true }); }); // ---- delete ----------------------------------------------------------- router.delete('/:id', (req, res) => { const id = Number(req.params.id); const user = db.prepare('SELECT * FROM users WHERE id = ?').get(id); if (!user) return res.status(404).json({ error: 'User not found.' }); if (id === req.session.user.id) { return res.status(400).json({ error: 'You cannot delete the account you are signed in with.' }); } if (user.role === 'admin' && adminCount() <= 1) { return res.status(400).json({ error: 'This is the only administrator and cannot be deleted.' }); } // Detach the user from everything that references them, in one transaction. // Content they published is KEPT (ownership is simply cleared) and the audit // log is untouched — log entries store the username as text, so history // survives the account being removed. db.transaction(() => { db.prepare('DELETE FROM access_requests WHERE user_id = ?').run(id); db.prepare('UPDATE access_requests SET decided_by = NULL WHERE decided_by = ?').run(id); db.prepare('UPDATE folders SET created_by = NULL WHERE created_by = ?').run(id); db.prepare('UPDATE files SET uploaded_by = NULL WHERE uploaded_by = ?').run(id); db.prepare('DELETE FROM users WHERE id = ?').run(id); })(); log(req, 'USER_DELETED', PAGE, `Deleted user "${user.username}" (content and audit history retained)`); res.json({ ok: true }); }); module.exports = router;