Enhance audit logging: user agent, session ID, PHI access tracking

Loki logs now include:
- User agent string (browser/device identification)
- Session ID (ties actions to specific login session)
- Status field (success/failure)

New logging:
- encounter_load: logged when user opens a saved encounter (with label)
- copy_to_clipboard: logged when user copies note content (PHI access)
- Client event endpoint: POST /api/logs/client-event (auth required)

Encounter save/delete/load all include the encounter label for
patient identification in audit trail.

HIPAA audit trail now covers: who, what, when, from where, which
device, which session, what patient data, success/failure.
This commit is contained in:
Daniel 2026-04-11 06:17:05 +02:00
parent 0c8a4db5c3
commit 369e440aa1
4 changed files with 49 additions and 3 deletions

View file

@ -360,6 +360,17 @@ document.addEventListener('click', function(e) {
if (action === 'copy' && targetId) {
if (typeof copyText === 'function') copyText(targetId);
// Log PHI copy event (fire-and-forget, best-effort)
try {
var token = window.AUTH_TOKEN || localStorage.getItem('ped_scribe_token') || '';
if (token) {
fetch('/api/logs/client-event', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token },
body: JSON.stringify({ action: 'copy_to_clipboard', target: targetId })
}).catch(function() {});
}
} catch(e) {}
return;
}
if (action === 'speak' && targetId) {

View file

@ -29,6 +29,7 @@ router.get('/encounters/saved/:id', async function(req, res) {
[req.params.id, req.user.id]
);
if (!row) return res.status(404).json({ error: 'Encounter not found or expired' });
logger.audit(req.user.id, 'encounter_load', 'Loaded encounter: ' + (row.label || 'unlabeled') + ' (id:' + req.params.id + ')', req, { category: 'clinical' });
res.json({ success: true, encounter: row });
} catch (e) { logger.error('GET /encounters/saved/:id', e.message); res.status(500).json({ error: e.message }); }
});

View file

@ -51,4 +51,13 @@ router.post('/logs/client-error', function(req, res) {
} catch(err) { res.status(204).end(); }
});
// Client-side event logging (copy, export — PHI access tracking)
router.post('/logs/client-event', authMiddleware, function(req, res) {
try {
var e = req.body || {};
logger.audit(req.user.id, e.action || 'client_event', e.target || '', req, { category: 'phi_access' });
res.status(204).end();
} catch(err) { res.status(204).end(); }
});
module.exports = router;

View file

@ -56,7 +56,16 @@ var logger = {
e.model || null, e.tokens || null, e.duration || null, e.status || 'success'
]
).catch(function(err) { console.error('[Logger] Audit failed:', err.message); });
pushToLoki({ type: 'audit', category: e.category || 'general', action: action }, JSON.stringify({ userId: userId, action: action, details: details, ip: req ? req.ip : null }));
pushToLoki(
{ type: 'audit', category: e.category || 'general', action: action, status: e.status || 'success' },
JSON.stringify({
userId: userId, action: action, details: details,
ip: req ? req.ip : null,
userAgent: req ? (req.headers['user-agent'] || '').substring(0, 200) : null,
sessionId: req ? (req.sessionId || null) : null,
status: e.status || 'success'
})
);
},
apiCall: function(userId, endpoint, data) {
@ -71,7 +80,14 @@ var logger = {
d.ip || null, d.error || null
]
).catch(function(err) { console.error('[Logger] API log failed:', err.message); });
pushToLoki({ type: 'api_call', endpoint: endpoint, model: d.model || '' }, JSON.stringify({ userId: userId, endpoint: endpoint, model: d.model, tokens: (d.tokensInput||0)+(d.tokensOutput||0), cost: cost, duration: d.duration }));
pushToLoki(
{ type: 'api_call', endpoint: endpoint, model: d.model || '' },
JSON.stringify({
userId: userId, endpoint: endpoint, model: d.model,
tokens: (d.tokensInput||0)+(d.tokensOutput||0), cost: cost, duration: d.duration,
ip: d.ip || null, status: d.statusCode || 200
})
);
},
access: function(userId, action, req, success) {
@ -84,7 +100,16 @@ var logger = {
success ? true : false
]
).catch(function(err) { console.error('[Logger] Access log failed:', err.message); });
pushToLoki({ type: 'access', action: action }, JSON.stringify({ userId: userId, action: action, ip: req ? req.ip : null, success: success }));
pushToLoki(
{ type: 'access', action: action },
JSON.stringify({
userId: userId, action: action,
ip: req ? req.ip : null,
userAgent: req ? (req.headers['user-agent'] || '').substring(0, 200) : null,
sessionId: req ? (req.sessionId || null) : null,
success: success
})
);
},
file: function(level, message, data) {