notXCS/routes/admin.js

249 lines
8.6 KiB
JavaScript

const express = require('express');
const bcrypt = require('bcryptjs');
const router = express.Router();
const { requireAuth, ROLES } = require('../lib/auth');
let db;
module.exports = (dbInit) => {
db = dbInit;
return router;
};
// Admins (and superadmins) can administrate users: create, delete, reset password, change role.
router.use(requireAuth(() => db, ROLES.ADMIN));
// Superadmin (role 4) can never be granted through the API — the only way to obtain that role is by
// editing the database directly (see routes/auth.js, which grants it to the very first registered user).
const VALID_ROLES = [ROLES.USER, ROLES.ELEVATED, ROLES.ADMIN];
// Only a superadmin may grant the admin role to someone else.
function canAssignRole(callerRole, role) {
if (role === ROLES.ADMIN) {
return callerRole >= ROLES.SUPERADMIN;
}
return true;
}
// Regular admins may not modify or delete admin/superadmin accounts; only a superadmin can.
function canManageTarget(callerRole, targetRole) {
if (callerRole >= ROLES.SUPERADMIN) {
return true;
}
return targetRole < ROLES.ADMIN;
}
// List all users
router.get('/users', (req, res) => {
db.all(`SELECT id, username, role, created_at FROM users ORDER BY id`, [], (err, users) => {
if (err) {
console.error('Failed to retrieve users:', err.message);
return res.status(500).json({ success: false, message: "Internal server error" });
}
res.json({ success: true, users });
});
});
// Create a new user
router.post('/users', (req, res) => {
const { username, password } = req.body || {};
const role = req.body.role !== undefined ? parseInt(req.body.role, 10) : ROLES.USER;
if (!username || !password) {
return res.status(400).json({ success: false, message: "Username and password are required" });
}
if (typeof username !== 'string' || typeof password !== 'string' || username.trim().length < 3 || password.length < 6) {
return res.status(400).json({ success: false, message: "Username must be at least 3 characters and password at least 6 characters" });
}
if (!VALID_ROLES.includes(role)) {
return res.status(400).json({ success: false, message: "Invalid role" });
}
if (!canAssignRole(req.user.role, role)) {
return res.status(403).json({ success: false, message: "Only a superadmin can grant the admin role" });
}
db.get(`SELECT id FROM users WHERE username = ?`, [username], (err, existing) => {
if (err) {
console.error('Failed to check for existing user:', err.message);
return res.status(500).json({ success: false, message: "Internal server error" });
}
if (existing) {
return res.status(409).json({ success: false, message: "Username is already taken" });
}
bcrypt.hash(password, 10, (err, hash) => {
if (err) {
console.error('Failed to hash password:', err.message);
return res.status(500).json({ success: false, message: "Internal server error" });
}
db.run(`INSERT INTO users (username, password, role) VALUES (?, ?, ?)`, [username, hash, role], function (err) {
if (err) {
console.error('Failed to create user:', err.message);
return res.status(500).json({ success: false, message: "Internal server error" });
}
res.json({ success: true, user: { id: this.lastID, username, role } });
});
});
});
});
// Update a user's role and/or reset their password
router.put('/users/:userId', (req, res) => {
const userId = parseInt(req.params.userId, 10);
db.get(`SELECT id, username, role FROM users WHERE id = ?`, [userId], (err, user) => {
if (err) {
console.error('Failed to retrieve user:', err.message);
return res.status(500).json({ success: false, message: "Internal server error" });
}
if (!user) {
return res.status(404).json({ success: false, message: "User not found" });
}
if (!canManageTarget(req.user.role, user.role)) {
return res.status(403).json({ success: false, message: "Only a superadmin can modify an admin or superadmin account" });
}
let role = user.role;
if (req.body && req.body.role !== undefined) {
role = parseInt(req.body.role, 10);
if (!VALID_ROLES.includes(role)) {
return res.status(400).json({ success: false, message: "Invalid role" });
}
if (!canAssignRole(req.user.role, role)) {
return res.status(403).json({ success: false, message: "Only a superadmin can grant the admin role" });
}
}
const password = req.body ? req.body.password : undefined;
if (password !== undefined && (typeof password !== 'string' || password.length < 6)) {
return res.status(400).json({ success: false, message: "Password must be at least 6 characters" });
}
const applyUpdate = (hash) => {
const sql = hash
? `UPDATE users SET role = ?, password = ? WHERE id = ?`
: `UPDATE users SET role = ? WHERE id = ?`;
const params = hash ? [role, hash, userId] : [role, userId];
db.run(sql, params, (err) => {
if (err) {
console.error('Failed to update user:', err.message);
return res.status(500).json({ success: false, message: "Internal server error" });
}
res.json({ success: true, user: { id: userId, username: user.username, role } });
});
};
if (!password) {
return applyUpdate(null);
}
bcrypt.hash(password, 10, (err, hash) => {
if (err) {
console.error('Failed to hash password:', err.message);
return res.status(500).json({ success: false, message: "Internal server error" });
}
applyUpdate(hash);
});
});
});
// Delete a user
router.delete('/users/:userId', (req, res) => {
const userId = parseInt(req.params.userId, 10);
if (userId === req.user.id) {
return res.status(400).json({ success: false, message: "You cannot delete your own account" });
}
db.get(`SELECT id, role FROM users WHERE id = ?`, [userId], (err, user) => {
if (err) {
console.error('Failed to retrieve user:', err.message);
return res.status(500).json({ success: false, message: "Internal server error" });
}
if (!user) {
return res.status(404).json({ success: false, message: "User not found" });
}
if (!canManageTarget(req.user.role, user.role)) {
return res.status(403).json({ success: false, message: "Only a superadmin can delete an admin or superadmin account" });
}
db.get(`SELECT COUNT(*) as count FROM places WHERE ownerId = ?`, [userId], (err, row) => {
if (err) {
console.error('Failed to check owned places:', err.message);
return res.status(500).json({ success: false, message: "Internal server error" });
}
if (row.count > 0) {
return res.status(409).json({ success: false, message: "This user still owns places. Reassign or delete them first." });
}
db.run(`DELETE FROM place_access WHERE userId = ?`, [userId], (err) => {
if (err) {
console.error('Failed to delete access grants for user:', err.message);
return res.status(500).json({ success: false, message: "Internal server error" });
}
db.run(`DELETE FROM users WHERE id = ?`, [userId], (err) => {
if (err) {
console.error('Failed to delete user:', err.message);
return res.status(500).json({ success: false, message: "Internal server error" });
}
res.json({ success: true });
});
});
});
});
});
// Get the current global lockdown state. While active, every reader at every place is disabled
// and all scans are denied, regardless of that reader's own settings or ACL entries.
router.get('/lockdown', (req, res) => {
db.get(
`SELECT active, activatedBy, activatedAt FROM system_lockdown WHERE id = 1`,
[],
(err, row) => {
if (err) {
console.error('Failed to retrieve lockdown state:', err.message);
return res.status(500).json({ success: false, message: "Internal server error" });
}
res.json({
success: true,
active: !!(row && row.active),
activatedBy: row ? row.activatedBy : null,
activatedAt: row ? row.activatedAt : null
});
}
);
});
// Engage the lockdown for all places. Any admin (or superadmin) may do this.
router.post('/lockdown', (req, res) => {
db.run(
`UPDATE system_lockdown SET active = 1, activatedBy = ?, activatedAt = CURRENT_TIMESTAMP WHERE id = 1`,
[req.user.id],
(err) => {
if (err) {
console.error('Failed to engage lockdown:', err.message);
return res.status(500).json({ success: false, message: "Internal server error" });
}
res.json({ success: true, active: true });
}
);
});
// Lift the lockdown, restoring every reader to its own configured state.
router.delete('/lockdown', (req, res) => {
db.run(
`UPDATE system_lockdown SET active = 0, activatedBy = NULL, activatedAt = NULL WHERE id = 1`,
[],
(err) => {
if (err) {
console.error('Failed to lift lockdown:', err.message);
return res.status(500).json({ success: false, message: "Internal server error" });
}
res.json({ success: true, active: false });
}
);
});