Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

01 — Product Overview (Audit Module)

StudyLyon — multi-tenant ERP / School Management API. This package designs the Audit module client (Flutter, forward-looking spec) against the implemented NestJS backend. Every endpoint, filter, field, event, and retention rule is derived directly from src/modules/audit/**, src/events/**, src/modules/ws/**, src/infrastructure/**, and studylyon-blueprint/03-Database/AUDITING.md + DATA_RETENTION.md. No feature is invented; gaps are flagged in the Assumptions & Open Questions section.


1. Purpose

Audit is StudyLyon's immutable, append-only record of every business action. It is the system of record for compliance, forensic analysis, dispute resolution, and tenant security review. Every domain event that crosses the in-process EventBus is captured into audit_logs by the AuditHandler wildcard subscription and can be read back through a single read-only endpoint.

ResponsibilitySource
Capture every domain event into audit_logs (wildcard onAny subscription)audit.handler.ts:18-25
Mask secrets (passwordHash, totpSecret, refreshToken, accessToken, token, password) before storageaudit.service.ts:5-12, 14-23
Append-only persistence (create/find/count only; no update/delete repo methods)audit-log.repository.ts:6, 15-28
Read endpoint with filters: action, entityType, actorId + page/limitaudit.controller.ts:17-34
Tenant isolation on every query (tenantId from request context, never from body)audit.controller.ts:30, audit.service.ts:45-48
Action vocabulary = emitted eventType strings (PascalCase, e.g. UserCreated, StudentUpdated)audit.handler.ts:36; emitters e.g. users.service.ts:65,146,177
Realtime delivery of the same events to the tenant room via WS bridgews-bridge.service.ts:16-22
Retention policy: 7 years minimum, immutable, no auto-delete (blueprint)DATA_RETENTION.md:29
Dedicated audit-write BullMQ queue declared; worker is a no-op todayqueue.constants.ts:11, system.worker.ts:5-13

2. Business goals

GoalMeasure
Legally-defensible historyEvery business action produces one append-only entry; records never updated/deleted (AUDITING.md:11-12)
Compliance retentionAudit logs retained 7 years minimum, outliving tenant purge (DATA_RETENTION.md:29, 54)
Immutability by constructionNo update/delete paths in code (audit-log.repository.ts:6); PLAN 19.3 (PLAN.md:191)
Tenant isolationEvery entry carries tenantId; queries always scoped (AUDITING.md:67-72, audit.service.ts:45)
Zero secret leakageSensitive fields masked to '***' at write time (audit.service.ts:20)
Non-blocking writesAudit persistence is async in-process (event handler, not request path) (audit.handler.ts:7-9; AUDITING.md:94)
QueryabilityRead endpoint filterable by action / entity type / actor (audit.controller.ts:22-29)

3. User goals

  • Org Admin: "show me exactly what happened with this student/user — who did it and when."
  • Compliance officer / auditor: "export a complete, tamper-proof trail for an audit window or a dispute."
  • Platform admin: "verify no cross-tenant action occurred" (platform tooling (planned) — see OQ-3).
  • Support/QA: correlate an incident using correlationId across requests, queues, and audit entries.

4. Stakeholders

Tenant admins (primary readers), compliance officers and external auditors (export, review), platform operations (cross-tenant oversight (planned)), support (incident correlation via correlationId), security review board, QA + design + engineering.

5. Why this exists

Schools handle children, fees, and credentials; disputes and inspections are routine. AUDITING.md makes audit a first-class architectural concern: every CUD + auth + config action is captured, immutable, tenant-scoped, and retained longest of any collection. The backend implements the capture + read pipeline today; the client must present it without ever implying entries can be edited, deleted, or fabricated.

6. Dependencies

DependencyRoleSource
EventBus (in-process)wildcard '*' emit → AuditHandler.onAny captureevent-bus.service.ts:11-14, 20-22; audit.handler.ts:18
DomainEvent contracteventType, tenantId, actorId, occurredAt, correlationId, payloaddomain-event.interface.ts:1-8
Emitting modules (users, students, staff, teachers, parents, auth, fees, crm, homework, results, attendance, …)produce the action vocabulary + payloads that become after snapshotse.g. users.service.ts:64-76
WsGateway + WsBridgeevery domain event broadcast to tenant:{tenantId} room → realtime append source for the listws-bridge.service.ts:16-22; ws.gateway.ts:50
BullMQ audit-write queuedeclared + routed (subset of events) but no-op worker — entries are persisted by the in-process handler, not the queuequeue.constants.ts:11; event-queue-map.ts:8-39; system.worker.ts:9-12
RBAC audit.read permissionexists in ALL_PERMISSIONS; granted to org_admin; not yet enforced on the controller (Phase-5 (planned))permissions.constants.ts:54; role.schema.ts:23; audit.controller.ts:9; docs/IMPLEMENTATION_PLAN.md:241
Global guardsRateLimitGuard (api tier 100/min default) → JwtAuthGuardRbacGuardapp.module.ts:129-133; rate-limit.constants.ts:6
Mongo collectionaudit_logs (+ indexes, listed in AUDITING.md:39 and schema)audit-log.schema.ts:13, 60-63

7. Success metrics

  • Every emitted domain event produces exactly one audit_logs doc (dedupe by correlationId/_id on client; no duplicates in list).
  • Sensitive keys never appear in any API response (masked '***' at write, audit.service.ts:20).
  • List page renders < 300 ms (cached) / ≤ 2 s (network) with default limit=50 (audit.controller.ts:20).
  • Filtering by action / actorId returns correct scoped results; entityType filter behaves as documented (see OQ-1).
  • Realtime append (WS) delivers new entries within ~1 s of the source action without duplicates.
  • Zero client paths that suggest edit/delete/export of entries that the API cannot perform (append-only honesty).

8. Edge cases

  • No detail endpoint: GET /audit-logs/:id does not exist — detail is rendered from the list payload (OQ-4).
  • Pagination shape deviation: service returns {data, total} without meta → the envelope interceptor treats it as a non-paginated payload; client must read data.data[] + data.total (OQ-2, 12_API_Mapping).
  • Empty resource metadata: most emitters do not set entityType/entityId in payloads today → entityType filter matches nothing for most actions (OQ-1).
  • No date-range filter: blueprint promises "query by date range" (AUDITING.md:86) but no from/to params exist — (planned).
  • No search/sort: q and sort params from the shared convention (00-shared/07 §5) are not supported; sort is fixed occurredAt desc (audit-log.repository.ts:23).
  • TTL contradiction: migrate.ts:17-23 creates a 90-day TTL index on occurredAt, contradicting the 7-year retention policy (OQ-6).
  • Audit reads are unauthenticated-role-broad: controller is JwtAuthGuard only — any logged-in user (even student, permissions: []) can read the full tenant audit log (OQ-5).
  • Unbounded limit: no max clamp — limit=10000 is accepted (OQ-7).
  • WS payloads unmasked: WsBridge broadcasts raw payload (may contain secrets before the masked copy is written); clients must not render it verbatim (OQ-8).
  • Non-numeric page/limit (?page=abc) → Number()NaN → Mongo CastError → 400 VALIDATION_ERROR (http-exception.filter.ts:47-55).

9. Assumptions (module)

  • Forward-looking client: backend is complete for capture + list-read; this package specs the UI. The PRD puts native mobile out of Phase 1 (read-only companion in Phase 3 — 00-shared/12 A1); these docs specify the full responsive client anyway (A2: same design system serves web/desktop).
  • Audit is an admin-dense surface — desktop/tablet table layout is the primary target; phone is a compact list.
  • Actor display names are not in the response (actorId only). Resolving names requires a client-side join against GET /users (proposed)actorId filter is exact-id matching (audit.controller.ts:24).
  • "Export" exists in the blueprint (AUDITING.md:87: CSV/PDF, streamed, itself an audited action) but no endpoint exists → export UI is (planned).
  • Realtime append is possible today because WsBridge forwards every domain event (the same set the handler persists) to the tenant room (ws-bridge.service.ts:16-22).

10. Open questions (module-level; global ledger in 00-shared/12)

#ItemImpact
OQ-1Emitters rarely populate entityType/entityId/before in payloads (e.g. users.service.ts:70-75 sends userId only). True "resource" filtering and before/after diffs are effectively absent from today's data. Add entityType/entityId/before to emitters?Filter UX, diff view
OQ-2GET /audit-logs returns data:{data:[], total} (no meta) — deviates from the shared paginated envelope. Fix service to return meta (buildPaginationMeta)?Client parsing
OQ-3Platform/tenantId:null entries and actorType: platform promised in AUDITING.md:70-71 are not implemented (enum is `usersystem
OQ-4No entry-detail endpoint — deep links to a single entry can't refetch. Add GET /audit-logs/:id?Detail screen architecture
OQ-5Controller is JwtAuthGuard only — audit.read defined but unenforced (audit.controller.ts:9); RBAC docs flag the same (design-docs/rbac/05:94-96). Phase-5 permission audit (planned) (docs/IMPLEMENTATION_PLAN.md:241). Client gates on audit.read regardless.Route gating
OQ-6migrate.ts:23 creates a 90-day TTL on audit_logs.occurredAt — destroys the 7-year compliance retention (DATA_RETENTION.md:29). Confirm: TTL must be removed or replaced with archive job.Retention UX, data loss risk
OQ-7limit unbounded (default 50, audit.controller.ts:20); no clamp to shared max 100 (00-shared/07 §5).List UX, perf
OQ-8WS broadcast carries raw, unmasked payload (ws-bridge.service.ts:17-21) while stored docs are masked. If UI renders realtime entries, secret-bearing payloads could surface. Mask at bridge or filter client-side.Realtime view safety

11. Glossary (this module)

TermMeaning
Audit entry / AuditLogone immutable doc in audit_logs (audit-log.schema.ts:13)
actionthe eventType string of the source event, e.g. UserCreated, StudentUpdated (audit.handler.ts:36)
actorId / actorTypewho performed the action; type is always user today (audit.handler.ts:35)
before / aftermasked snapshots; before is set only if the emitter provides it (rarely), after falls back to the full payload (audit.handler.ts:42-43)
correlationIdend-to-end trace id linking request → queue job → audit entry (domain-event.interface.ts:6)
occurredAtauthoritative timestamp (ISO-8601 UTC) of the action — not createdAt (schema uses timestamps: false, audit-log.schema.ts:13)
append-onlyno update/delete surface anywhere in the module (audit-log.repository.ts:6, PLAN 19.3)
Realtime appendWS push of domain events to tenant:{tenantId} room (ws.gateway.ts:50, ws-bridge.service.ts:16)
Envelope{success,message,data,meta?,timestamp,requestId} (response-envelope.interceptor.ts:11-18)