/** * @file audit.js * @description Server mode's audit trail (docs/SERVER-MODE.md): every read by * an admin or analyst of a session that a MEMBER owns — the session page, its * transcript, its events, its cost — is written to `audit_log` with who read * it, whose it was and which path. `auditGuard` runs on `/api` after the * scope guard; it never blocks a request, and it is a no-op in local mode and * for a member (who can only read their own). An admin's account change on * the Users page is written here too (`recordAccountChange`). * @author Nguyễn Ngọc Trí Vĩ */ const sql = (q) => require("../db").db.prepare(q); // Paths that name one session's content. const SESSION_PATHS = [ /^\/sessions\/(?!facets\/?$)([^/]+)/i, /^\/pricing\/cost\/([^/]+)/i, /^\/workflows\/session\/([^/]+)/i, ]; function auditGuard(req, res, next) { const u = req.user; if (!u || u.role === "developer" || (req.method !== "GET" && req.method !== "HEAD")) return next(); const p = require("./auth").apiPath(req); for (const re of SESSION_PATHS) { const m = re.exec(p); if (!m) continue; try { const { sessionOwners } = require("./scope"); const id = m[1]; for (const owner of sessionOwners(id)) { if (owner === u.id) continue; const ownerRole = sql("SELECT role FROM users WHERE id = ?").get(owner)?.role; if (ownerRole !== "developer") continue; sql( "INSERT INTO audit_log (user_id, owner_id, session_id, method, path) VALUES (?, ?, ?, ?, ?)" ).run(u.id, owner, id, req.method, req.originalUrl.slice(0, 500)); } } catch (err) { console.warn("[audit] could not record:", err.message); } break; } return next(); } /** The newest entries, with user names. */ function listAudit(limit = 100) { const n = Math.min(Math.max(Number(limit) || 100, 1), 1000); return sql( `SELECT a.id, a.at, a.method, a.path, a.session_id, r.username AS reader, o.username AS owner FROM audit_log a LEFT JOIN users r ON r.id = a.user_id LEFT JOIN users o ON o.id = a.owner_id ORDER BY a.id DESC LIMIT ?` ).all(n); } /** An admin's account change from the Users page: who, on whom, what (never * a password). `owner_id` is the account changed; `path` carries the action. */ function recordAccountChange(actorId, targetId, method, action) { sql( "INSERT INTO audit_log (user_id, owner_id, session_id, method, path) VALUES (?, ?, NULL, ?, ?)" ).run(actorId, targetId, method, String(action).slice(0, 500)); } module.exports = { auditGuard, listAudit, recordAccountChange };