pediatric-ai-scribe-v3/src/routes/admin.js
Daniel 74c5cde8e1 Stop leaking e.message to clients across all routes
88 occurrences of res.status(500).json({ error: e.message }) (or
err.message) swept to generic 'Request failed'. Server-side
console.error / logger.error calls are untouched, so the full detail
still lands in logs and Grafana.

Covers: admin, adminConfig, adminMilestones, chartReview, documents,
encounters, hospitalCourse, hpi, learningAdmin, learningAI, learningHub,
logs, memories, milestones, oidc, refine, sessions, sickVisit, soap,
userPreferences, wellVisit.

Also extends .gitignore to exclude .env.backup-* files.
2026-04-14 03:04:24 +02:00

241 lines
9.9 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: 'Request failed' }); }
});
// ============================================================
// 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: 'Request failed' }); }
});
// ============================================================
// 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: 'Request failed' }); }
});
// ============================================================
// 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: 'Request failed' }); }
});
// ============================================================
// 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: 'Request failed' }); }
});
// ============================================================
// 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: 'Request failed' }); }
});
// ============================================================
// 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: 'Request failed' }); }
});
// ============================================================
// 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: 'Request failed' }); }
});
// ============================================================
// 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: 'Request failed' }); }
});
// ============================================================
// 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: 'Request failed' }); }
});
// ============================================================
// 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: 'Request failed' }); }
});
module.exports = router;