pediatric-ai-scribe-v3/src/routes/admin.js
Daniel Onyejesi 4064ccf516 Fix quiz SQL bug, security hardening, componentize HTML, speed improvements
- Fix: "missing FROM-clause entry for table q" in quiz submission query
- Security: replace broken sanitizeHtml with allowlist-based sanitizer (prevents stored XSS)
- Security: whitelist table names in uniqueSlug() to prevent SQL injection pattern
- Security: replace raw err.message responses with generic errors in learning routes
- Security: cap admin logs query LIMIT to 500
- Refactor: break 1685-line index.html into 12 lazy-loaded component files (280 lines shell)
- Speed: add preconnect hints for Google Fonts and CDN
- Speed: defer all script tags for faster initial paint
- Speed: lazy-load tab HTML on activation (only active tab's DOM is parsed)
- Update: replace HIPAA notice with institution-guidelines cautionary note
2026-03-23 20:01:54 -04:00

241 lines
9.8 KiB
JavaScript

// ============================================================
// ADMIN ROUTES — User management, settings
// ============================================================
var express = require('express');
var router = express.Router();
var bcrypt = require('bcryptjs');
var db = require('../db/database');
var { authMiddleware, adminMiddleware } = require('../middleware/auth');
var logger = require('../utils/logger');
// All admin routes require auth + admin role
router.use(authMiddleware);
router.use(adminMiddleware);
// ============================================================
// GET ALL USERS
// ============================================================
router.get('/users', async function(req, res) {
try {
var users = await db.all(
'SELECT id, email, name, role, email_verified, totp_enabled, disabled, created_at FROM users ORDER BY created_at DESC',
[]
);
res.json({ success: true, users: users });
} catch (err) { res.status(500).json({ error: err.message }); }
});
// ============================================================
// GET SINGLE USER DETAILS
// ============================================================
router.get('/users/:id', async function(req, res) {
try {
var user = await db.get(
'SELECT id, email, name, role, email_verified, totp_enabled, disabled, nextcloud_url, created_at, updated_at FROM users WHERE id = ?',
[req.params.id]
);
if (!user) return res.status(404).json({ error: 'User not found' });
// Get user stats
var apiCount = await db.get('SELECT COUNT(*) as count FROM api_log WHERE user_id = ?', [req.params.id]);
var lastLogin = await db.get("SELECT timestamp FROM audit_log WHERE user_id = ? AND action = 'login' ORDER BY timestamp DESC LIMIT 1", [req.params.id]);
user.api_calls = apiCount ? parseInt(apiCount.count) : 0;
user.last_login = lastLogin ? lastLogin.timestamp : null;
res.json({ success: true, user: user });
} catch (err) { res.status(500).json({ error: err.message }); }
});
// ============================================================
// VERIFY USER EMAIL (admin override)
// ============================================================
router.post('/users/:id/verify', async function(req, res) {
try {
var user = await db.get('SELECT id, email, name FROM users WHERE id = ?', [req.params.id]);
if (!user) return res.status(404).json({ error: 'User not found' });
await db.run('UPDATE users SET email_verified = true, verify_token = NULL, verify_expires = NULL WHERE id = ?', [user.id]);
logger.audit(req.user.id, 'admin_verify_user', 'Verified user: ' + user.email, req, { category: 'admin' });
res.json({ success: true, message: user.email + ' verified' });
} catch (err) { res.status(500).json({ error: err.message }); }
});
// ============================================================
// DISABLE USER
// ============================================================
router.post('/users/:id/disable', async function(req, res) {
try {
var userId = parseInt(req.params.id);
// Prevent disabling yourself
if (userId === req.user.id) {
return res.status(400).json({ error: 'Cannot disable your own account' });
}
var user = await db.get('SELECT id, email, name FROM users WHERE id = ?', [userId]);
if (!user) return res.status(404).json({ error: 'User not found' });
await db.run('UPDATE users SET disabled = true WHERE id = ?', [userId]);
logger.audit(req.user.id, 'admin_disable_user', 'Disabled user: ' + user.email, req, { category: 'admin' });
res.json({ success: true, message: user.email + ' disabled' });
} catch (err) { res.status(500).json({ error: err.message }); }
});
// ============================================================
// ENABLE USER
// ============================================================
router.post('/users/:id/enable', async function(req, res) {
try {
var user = await db.get('SELECT id, email, name FROM users WHERE id = ?', [req.params.id]);
if (!user) return res.status(404).json({ error: 'User not found' });
await db.run('UPDATE users SET disabled = false WHERE id = ?', [user.id]);
logger.audit(req.user.id, 'admin_enable_user', 'Enabled user: ' + user.email, req, { category: 'admin' });
res.json({ success: true, message: user.email + ' enabled' });
} catch (err) { res.status(500).json({ error: err.message }); }
});
// ============================================================
// SET USER ROLE (admin/user)
// ============================================================
router.post('/users/:id/role', async function(req, res) {
try {
var userId = parseInt(req.params.id);
var { role } = req.body;
if (!role || (role !== 'admin' && role !== 'user' && role !== 'moderator')) {
return res.status(400).json({ error: 'Role must be "admin", "user", or "moderator"' });
}
// Prevent removing your own admin
if (userId === req.user.id && role !== 'admin') {
return res.status(400).json({ error: 'Cannot remove your own admin role' });
}
var user = await db.get('SELECT id, email FROM users WHERE id = ?', [userId]);
if (!user) return res.status(404).json({ error: 'User not found' });
await db.run('UPDATE users SET role = ? WHERE id = ?', [role, userId]);
logger.audit(req.user.id, 'admin_change_role', 'Set ' + user.email + ' role to ' + role, req, { category: 'admin' });
res.json({ success: true, message: user.email + ' is now ' + role });
} catch (err) { res.status(500).json({ error: err.message }); }
});
// ============================================================
// DELETE USER
// ============================================================
router.delete('/users/:id', async function(req, res) {
try {
var userId = parseInt(req.params.id);
if (userId === req.user.id) {
return res.status(400).json({ error: 'Cannot delete your own account' });
}
var user = await db.get('SELECT id, email, name FROM users WHERE id = ?', [userId]);
if (!user) return res.status(404).json({ error: 'User not found' });
// Delete user (audit/api logs keep user_id as NULL due to ON DELETE SET NULL)
await db.run('DELETE FROM users WHERE id = ?', [userId]);
logger.audit(req.user.id, 'admin_delete_user', 'Deleted user: ' + user.email + ' (' + user.name + ')', req, { category: 'admin' });
res.json({ success: true, message: user.email + ' deleted' });
} catch (err) { res.status(500).json({ error: err.message }); }
});
// ============================================================
// RESET USER PASSWORD (admin override)
// ============================================================
router.post('/users/:id/reset-password', async function(req, res) {
try {
var { newPassword } = req.body;
if (!newPassword || newPassword.length < 8) return res.status(400).json({ error: 'Password must be 8+ characters' });
var user = await db.get('SELECT id, email FROM users WHERE id = ?', [req.params.id]);
if (!user) return res.status(404).json({ error: 'User not found' });
var hash = await bcrypt.hash(newPassword, 12);
await db.run('UPDATE users SET password = ?, reset_token = NULL, reset_expires = NULL WHERE id = ?', [hash, user.id]);
logger.audit(req.user.id, 'admin_reset_password', 'Reset password for: ' + user.email, req, { category: 'admin' });
res.json({ success: true, message: 'Password reset for ' + user.email });
} catch (err) { res.status(500).json({ error: err.message }); }
});
// ============================================================
// TOGGLE REGISTRATION
// ============================================================
router.post('/settings/registration', async function(req, res) {
try {
var { enabled } = req.body;
var value = enabled ? 'true' : 'false';
await db.setSetting('registration_enabled', value);
logger.audit(req.user.id, 'admin_toggle_registration', 'Registration ' + (enabled ? 'enabled' : 'disabled'), req, { category: 'admin' });
res.json({ success: true, registrationEnabled: enabled, message: 'Registration ' + (enabled ? 'enabled' : 'disabled') });
} catch (err) { res.status(500).json({ error: err.message }); }
});
// ============================================================
// GET APP SETTINGS
// ============================================================
router.get('/settings', async function(req, res) {
try {
var regEnabled = await db.getSetting('registration_enabled');
var userCount = await db.get('SELECT COUNT(*) as count FROM users', []);
var apiCount = await db.get('SELECT COUNT(*) as count FROM api_log', []);
var todayApiCount = await db.get("SELECT COUNT(*) as count FROM api_log WHERE timestamp > NOW() - INTERVAL '1 day'", []);
res.json({
success: true,
settings: {
registrationEnabled: regEnabled !== 'false'
},
stats: {
totalUsers: userCount ? parseInt(userCount.count) : 0,
totalApiCalls: apiCount ? parseInt(apiCount.count) : 0,
todayApiCalls: todayApiCount ? parseInt(todayApiCount.count) : 0
}
});
} catch (err) { res.status(500).json({ error: err.message }); }
});
// ============================================================
// GET ALL LOGS (admin view)
// ============================================================
router.get('/logs/all', async function(req, res) {
try {
var limit = Math.min(parseInt(req.query.limit) || 100, 500);
var category = req.query.category;
var sql = 'SELECT a.*, u.email as user_email, u.name as user_name FROM audit_log a LEFT JOIN users u ON a.user_id = u.id';
var params = [];
if (category) {
sql += ' WHERE a.category = ?';
params.push(category);
}
sql += ' ORDER BY a.timestamp DESC LIMIT ?';
params.push(limit);
var logs = await db.all(sql, params);
res.json({ success: true, logs: logs });
} catch (err) { res.status(500).json({ error: err.message }); }
});
module.exports = router;