diff --git a/public/js/app.js b/public/js/app.js index 7c21841..5c22a45 100644 --- a/public/js/app.js +++ b/public/js/app.js @@ -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) { diff --git a/src/routes/encounters.js b/src/routes/encounters.js index e4e0757..ae39ebc 100644 --- a/src/routes/encounters.js +++ b/src/routes/encounters.js @@ -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 }); } }); diff --git a/src/routes/logs.js b/src/routes/logs.js index b1ef8fd..aa719cc 100644 --- a/src/routes/logs.js +++ b/src/routes/logs.js @@ -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; diff --git a/src/utils/logger.js b/src/utils/logger.js index 05aaafc..e692522 100644 --- a/src/utils/logger.js +++ b/src/utils/logger.js @@ -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) {