Skip to Content

Admin Audit Log

The audit-log viewer (GET /api/admin/audit-log) is a unified, reverse-chronological read of the two append-only admin-security tables:

  • SecurityAuditLog records security boundary violations such as forbidden-free-text attempts and sensitive-note access attempts. Rows are tenant-scoped via organizationId.
  • SuperAdminBypassLog records every time a platform operator (SUPER admin) reads across organizations. Rows carry targetOrgId (the org reached into) instead of an organizationId column.

Route and permission

GET /api/admin/audit-log

Requires three middleware layers: requireAuth, requireTenantContext, and requirePermission("VIEW_QA_REGISTRY"). A token without VIEW_QA_REGISTRY returns 403.

Secret-free DTO

The response is secret-free by contract. The following fields are deliberately omitted from every row regardless of the caller’s permission level:

Omitted fieldReason
ipAddressForensic PII; not needed for oversight
userAgentForensic PII; not needed for oversight
requestIdCorrelation ID; not needed by the oversight table
Attempted valueSensitive; only the byte-length is surfaced

attemptedValueLength (an integer or null) tells you that an attempt was made and how long the blocked value was, without storing the value itself.

Filters

All four filter parameters are optional and independent. Combine them freely.

ParameterTypeDescription
eventTypestringsecurity_attempt or super_admin_bypass (omit for both)
actorUserIdintegerfilter to one actor (the User.id of the admin or super-admin)
fromstringISO date; inclusive lower bound on the event timestamp
tostringISO date; inclusive upper bound on the event timestamp

An unparseable from or to value is silently dropped rather than returning a 400 error. The filter is best-effort UX, not a correctness gate.

Pagination

ParameterDefaultMaximum
limit50200
offset0(unbounded)

The total field in the response counts all matching rows across both tables before the limit/offset window, so callers can compute the number of pages.

curl "https://localhost:3000/api/admin/audit-log?limit=50&offset=0" \ -H "Authorization: Bearer <accessToken>"
{ "total": 142, "rows": [ { "id": "sec:clx4z2...", "kind": "security_attempt", "eventType": "FORBIDDEN_FREE_TEXT", "actorUserId": 7, "targetOrgId": "clx4z2...", "targetStudentId": 42, "action": null, "attemptedPath": "/api/students/42/context-flag", "attemptedFieldName": "freeText", "attemptedValueLength": 83, "occurredAt": "2026-06-25T08:14:22.000Z" }, { "id": "bypass:clx4z3...", "kind": "super_admin_bypass", "eventType": "BYPASS", "actorUserId": 1, "targetOrgId": "clx4z2...", "targetStudentId": null, "action": "GET /api/admin/audit-log", "attemptedPath": null, "attemptedFieldName": null, "attemptedValueLength": null, "occurredAt": "2026-06-25T07:00:01.000Z" } ] }

The id field is a source-prefixed stable identifier: sec:<id> for security rows, bypass:<id> for bypass rows. Results are sorted reverse-chronologically by occurredAt; rows that share a millisecond timestamp are broken by descending id for deterministic pagination (V-6).

Tenant scoping

A regular org admin (with VIEW_QA_REGISTRY) sees:

  • Security rows where organizationId matches their own org.
  • Bypass rows where targetOrgId matches their own org (bypass events that reached into their org).

A super-admin (platform operator with skipTenantFilter = true) sees all security rows and all bypass rows across every organization, including bypass rows where targetOrgId is null.

Every time a super-admin reads this endpoint, the server appends one SuperAdminBypassLog row recording the access. That row is itself visible to other super-admins on their next read of this page. Super-admin reads of the audit log are therefore self-auditing.

Invariants

  • Read-only. The handler imports no writer; it never updates, deletes, or upserts any audit row. The lint:no-update-on-audit-tables build guard enforces this.
  • Append-only source tables. Neither SecurityAuditLog nor SuperAdminBypassLog supports edits or deletions. Every row you see is the original record.
  • Secret-free by contract (SPEC-AL-4). Raw IP, user agent, request ID, and attempted values are stripped at the select layer via an explicit allow-list, so a future column addition cannot silently leak into the response.
  • Tenant-isolated. A regular admin cannot see another organization’s records through any filter or pagination combination.