Files
AgentHub/server/lib/audit.js
T
nntrivi2001 0c2f49876d feat: v2.7.6 — edit users in one form, Settings → Account, display names, copy over plain HTTP
- Users page: one Edit user form (display name, email, role, active, a new or
  generated password, workspaces given or taken back, sign out everywhere),
  validated as a whole before anything is written (PATCH /api/users/:id,
  GET /api/users/:id); users.display_name/email added migration-safe.
- Settings → Account for every signed-in user: profile, own password (the
  current one required, this login kept), sign out other devices
  (/api/me/account, /api/me/password, /api/me/sign-out-others; login cookie
  only, never a pane token).
- Display names wherever a person is named (sidebar, workspace owner groups,
  Users page and audit log); owner_display_name / reader_display additive.
- Users page: admins add and remove Claude/Codex pool accounts.
- Folder browser: an admin with their own Linux account opens in their home;
  the "Server folder" tick opens the host's (never remembered).
- AI Proxy: "Config 9router" beside Add profile when 9router is installed,
  opened on the dashboard's own host (GET /api/ai-proxies/9router).
- Terminal: a tmux selection reaches the clipboard over plain HTTP too
  (writeClipboard's textarea fallback, focus handed back).
- Chat: a file the session touched outside the workspace previews
  (GET /api/sessions/:id/file).
- install-system: `!requiretty` only when visudo accepts it (sudo-rs on
  Ubuntu 25.10+ rejects the setting).
- openapi.yaml regenerated; docs (API, SERVER-MODE, LANES) updated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 17:10:29 +07:00

72 lines
2.7 KiB
JavaScript

/**
* @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ĩ <vinnt@smartgift.vn>
*/
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,
r.display_name AS reader_display, o.display_name AS owner_display
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 };