pediatric-ai-scribe-v3/src/middleware/auth.js
Daniel 020e831b3c v6.2: Session management, password change, audit logging, refine context, UI fixes
Security:
- Add session management: users can view/revoke active sessions in Settings
- Add password change in Settings (requires current password, HIBP check)
- Force logout all sessions on password reset
- Fix logout to destroy server-side session (was only clearing cookie)
- Add trust proxy for correct client IP in rate limiting and audit logs
- Add CORS support for multiple domains (CORS_ORIGINS env var)
- Add HIBP breach check endpoint and inline warnings on password fields

Audit logging:
- Add audit logging to all 24 PHI-handling endpoints across 13 route files
- Covers: generation, transcription, TTS, refine, encounters, documents, Nextcloud
- All fire-and-forget (no response delay)

AI improvements:
- Refine now includes original source material (transcript, notes, labs)
  so AI can reference the full input when modifying output
- Add correction tracking (trackAIOutput) to sick visit and well visit tabs
- Fix sickvisit missing from encounter save noteIdMap

UI fixes:
- Non-blocking busy bar for transcription and AI generation (replaces full-screen overlay)
- Fix encounter recording: hide record button during recording (was showing two stop buttons)
- Fix ROS/PE "All WNL" stacking duplicate event handlers; add Clear buttons
- Enlarge AI instructions textarea in Learning Hub CMS

Domain:
- Primary domain now app.pedshub.com, with scribe.pedshub.com and peds.danvics.com as CORS origins
2026-04-08 20:27:45 +02:00

80 lines
2.7 KiB
JavaScript

const jwt = require('jsonwebtoken');
const db = require('../db/database');
const { hashToken } = require('../utils/sessions');
const JWT_SECRET = process.env.JWT_SECRET || 'dev-secret-change-in-production';
async function authMiddleware(req, res, next) {
var token = null;
var authHeader = req.headers.authorization;
if (authHeader && authHeader.startsWith('Bearer ')) {
token = authHeader.substring(7) || null;
}
if (!token && req.cookies && req.cookies.ped_auth) {
token = req.cookies.ped_auth;
}
if (!token) {
return res.status(401).json({ error: 'Authentication required' });
}
try {
var decoded = jwt.verify(token, JWT_SECRET);
var user = await db.get('SELECT id, email, name, role, totp_enabled, disabled FROM users WHERE id = ?', [decoded.userId]);
if (!user) {
return res.status(401).json({ error: 'User not found' });
}
if (user.disabled) {
return res.status(403).json({ error: 'Account disabled. Contact administrator.' });
}
req.user = user;
// Session validation — allows revocation
try {
var tHash = hashToken(token);
var session = await db.get('SELECT id, last_activity FROM user_sessions WHERE token_hash = ?', [tHash]);
if (!session) {
// Pre-migration user? If they have no sessions at all, allow through
var sessCount = await db.get('SELECT COUNT(*) as count FROM user_sessions WHERE user_id = ?', [user.id]);
if (parseInt(sessCount.count) > 0) {
return res.status(401).json({ error: 'Session revoked' });
}
} else {
req.sessionId = session.id;
// Throttled last_activity update — at most once per 5 minutes, fire-and-forget
var lastAct = new Date(session.last_activity).getTime();
if (Date.now() - lastAct > 5 * 60 * 1000) {
db.run('UPDATE user_sessions SET last_activity = NOW() WHERE id = ?', [session.id]).catch(function() {});
}
}
} catch (sessErr) {
// Fail open — table may not exist yet during migration
}
next();
} catch (err) {
return res.status(401).json({ error: 'Invalid or expired token' });
}
}
// Admin-only middleware — use after authMiddleware
async function adminMiddleware(req, res, next) {
if (!req.user || req.user.role !== 'admin') {
return res.status(403).json({ error: 'Admin access required' });
}
next();
}
// Moderator or Admin middleware — for Learning Hub CMS
async function moderatorMiddleware(req, res, next) {
if (!req.user || (req.user.role !== 'admin' && req.user.role !== 'moderator')) {
return res.status(403).json({ error: 'Moderator or admin access required' });
}
next();
}
module.exports = { authMiddleware, adminMiddleware, moderatorMiddleware, JWT_SECRET };