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:
parent
0c8a4db5c3
commit
369e440aa1
4 changed files with 49 additions and 3 deletions
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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 }); }
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue