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

03 — User Journeys (Audit Module)

End-to-end journeys for the Audit module computed from audit.controller.ts + audit.service.ts + audit.handler.ts + ws-bridge.service.ts. Each journey: entry, intent, decision points, system responses, loading, failures, recovery, exit. (planned) / (proposed) marks per global rules. All requests: GET /api/v1/audit-logs, Bearer JWT, envelope per 00-shared/07.


1. Browse the audit log

entry: /settings/audit from Settings hub, or "Access audit" entry from RBAC
intent: see recent tenant activity
sequenceDiagram
    actor U as Admin
    participant L as AuditListPage
    participant C as AuditCubit
    participant R as AuditRepository
    participant API as GET /audit-logs
    U->>L: open audit log
    L->>C: load(page=1, limit=50)
    C->>R: query(filters={})
    R->>API: ?page=1&limit=50
    alt success
        API-->>R: envelope.data = {data:[...], total:N}  (audit.service.ts:41-50)
        R-->>C: entries + total
        C-->>L: render table (desktop) / cards (phone)
    else 401
        API-->>R: UNAUTHENTICATED
        C-->>L: session-expiry flow (00-shared/06 §3.6)
    else 429
        API-->>R: RATE_LIMITED (api tier 100/min, rate-limit.constants.ts:6)
        L-->>U: countdown chip, no auto-retry
    else network
        L-->>U: AppErrorState + Retry
    end
    L-->>U: scroll → Load more (next page) until total reached
  • Loading: AppSkeleton rows ≤ 200 ms → content; default page size 50 (audit.controller.ts:20).
  • Entry: row tap → detail (from in-memory entry — no detail endpoint, OQ-4).
  • Exit: back to Settings; realtime WS appends visible on list (journey 5).

2. Filter by actor / action / resource

entry: filter bar on audit list
intent: narrow the trail to one actor, one action, or one resource type
sequenceDiagram
    actor U as Admin
    participant L as AuditListPage
    participant C as AuditCubit
    participant API as GET /audit-logs
    U->>L: pick Action = "StudentUpdated"
    L->>C: changeFilter(action: "StudentUpdated")
    C->>C: reset page=1
    C->>API: ?page=1&limit=50&action=StudentUpdated
    API-->>C: data:[entries], total
    Note over U,API: filters combine (AND): action + entityType + actorId (audit.controller.ts:26-29)
    alt entityType filter chosen
        Note over U,API: entityType matches only entries whose payload carried entityType — most emitters don't set it (OQ-1); show helper copy
    end
    U->>L: "Clear filters" → resets all three → reload unfiltered
  • Filter controls: three AppDropdowns (Action, Entity type, Actor) — all optional, AND-combined (audit.controller.ts:26-29).
  • Recovery: empty result → AppEmptyState "No activity matches these filters" + Clear button.
  • State: filter selection resets pagination to page 1; total refreshes.

3. Search (free text) — (planned)

entry: AppSearchBar on audit list
intent: find an entry by entity id, actor name, or event payload value
  • Server today: no q param on GET /audit-logs (audit.controller.ts:19-25) — shared convention sort/q unsupported (OQ-2 in 00-shared; 12_API_Mapping).
  • (planned) client-side: filter current page by visible fields (action, entityType, actorId, id) as an interim; full server search requires a backend q (regex across payload) — flag to backend team.

4. Inspect an entry — before/after diff

sequenceDiagram
    actor U as Admin
    participant L as AuditListPage
    participant D as AuditDetailView
    U->>L: tap row (action=UserUpdated, actor=A.K., 10:32)
    L-->>D: pass in-memory AuditEntry (no refetch — no :id endpoint, OQ-4)
    D-->>U: header: action chip + actor + occurredAt (local tz) + correlationId
    Note over D,U: body = AuditDiffView
    alt emitter provided before/after
        D-->>U: changed/added/removed rows (OQ-1: rare today)
    else after only (common: after = payload, audit.handler.ts:43)
        D-->>U: "Snapshot after action" — payload rendered as JSON tree (e.g. {userId, changes:[...]}, users.service.ts:70-75)
    else neither
        D-->>U: "No snapshot captured for this action" (empty diff)
    end
    U->>D: expand nested JSON value / copy JSON (SelectableText)
    U->>U: back → list keeps scroll + filters
  • Immutability cues: no edit/delete buttons anywhere on the detail view (append-only, audit-log.repository.ts:6); footnote "This record cannot be modified."
  • Diff math is client-side: server stores full before/after maps; the client computes changed/added/removed keys.

5. Realtime append (WS)

entry: audit list open while another user performs an action
intent: watch the trail update without manual refresh
sequenceDiagram
    participant S as Source service (e.g. users)
    participant B as EventBus
    participant H as AuditHandler
    participant DB as audit_logs
    participant W as WsBridge
    participant C as AuditCubit (connected)
    S->>B: emit(UserUpdated, {tenantId, actorId, payload})
    B-->>H: onAny(event) → write masked entry (audit.handler.ts:18-45)
    B-->>W: onAny(event) → broadcast to tenant room (ws-bridge.service.ts:16-22)
    W-->>C: WS topic "UserUpdated" {eventType, occurredAt, payload}
    C-->>C: prepend entry (dedupe by _id/correlationId; respect active filters)
    C-->>U: banner "1 new entry" → tap to expand
  • WS auth: bearer token in handshake (ws.gateway.ts:37-40); client joins tenant:{tenantId} automatically (ws.gateway.ts:50).
  • Filter-aware: realtime entries are appended only if they match the active filter set; otherwise the banner shows "New activity — refresh filters".
  • Safety: broadcast payload is unmasked (OQ-8) — the client never renders broadcast payload fields verbatim; it uses the REST snapshot on expand.

6. Export audit trail — (planned)

entry: list overflow menu / detail menu → "Export"
intent: CSV/PDF of the filtered result set for compliance (AUDITING.md:87)
sequenceDiagram
    actor U as Admin
    participant L as AuditListPage
    participant S as ExportSheet
    U->>L: overflow → Export
    L->>S: open ExportSheet (format, scope = current filters + pages, timezone)
    S->>U: confirm → submit (planned endpoint; export itself is an audited action per AUDITING.md:87)
    alt endpoint exists (future)
        S-->>U: job queued (REPORT_GENERATE, queue.constants.ts:10) → status + download
    else today
        S-->>U: disabled state with note "(planned)" — no backend export endpoint
    end
  • Client scope today: none — the export surface is speced but gated until the backend lands (OQ/planned).

Abandonment & exit rules (all): back = restore previous scroll/filters; timeout = WS reconnect triggers silent refetch; permission denial (no audit.read) = route hidden + 403 screen (00-shared/05 §8); offline = last-good cache + AppOfflineBanner, WS buffered → refetch on reconnect.