Final piece of the chantier portal-admin per ADR-0020 §"v1 scope"
item 4. The new /audit page consumes GET /api/admin/audit (PR #132),
renders a filter form + paginated results table, and trips the
admin.audit.query deterrent on every fetch.
What lands
- AuditEventsService (apps/portal-admin/src/app/pages/audit/
audit-events.service.ts): thin HttpClient wrapper around
GET /api/admin/audit. Builds HttpParams from the filter shape,
dropping empty strings (Nest's ValidationPipe treats ?foo= as
invalid). providedIn: 'root' — the audit page is the single v1
consumer, no per-page DI overhead worth it.
- AuditPage component (apps/portal-admin/src/app/pages/audit/
audit.ts): signal-driven page composing:
- Filter form: eventType, actorIdHash, audience, outcome,
subjectPrefix, createdAtFrom/To (datetime-local → ISO),
page-size selector (25 / 50 / 100 / 200 matching BFF MAX_LIMIT).
- Result table: timestamp (locale-formatted), event type,
audience+outcome with color-coded badges, actor hash + subject
stacked, trace id, payload disclosure (details/summary).
- Pagination: previous/next disabled at boundaries, resets
offset to 0 on every Apply Filters or Reset.
- States: loading line, empty state, error message (403
surfaces "you do not have access"; 5xx surfaces a generic
retry message — raw status leaks to ops via the network tab,
not to the admin UI).
- Route: /audit, lazy-loaded, title 'Audit log — APF Portal Admin'
+ matching FR translation in messages.fr.xlf
('Journal d'audit — Administration APF Portal').
Notes
- @RequireMfa NOT applied to the BFF endpoint in v1 — the admin
surface already sits behind a freshly-MFA'd session per ADR-0020,
and the per-query audit row is the deterrent. The endpoint will
pick up @RequireMfa({ freshness: 600 }) when the security review
asks for it; the SPA already lives behind the unauthorized
interceptor so it would gracefully refresh on the 401.
- Page-size cap mirrors the BFF MAX_LIMIT (200). A future caller
bypassing the SPA cap is still safe because the BFF DTO clamps
to 200 regardless (PR #132 §AuditReader.findEvents).
- SCSS budget: 4.98 KB / 5 KB warning (1.21 KB transfer). Well
below the 6 KB error ceiling.
- AdminAuditQuery interface uses `?: T | undefined` so callers can
build the query object with `undefined` placeholders for absent
filters under `exactOptionalPropertyTypes: true` — the page does
exactly that in `buildFilters()`.
Tests: +15 specs (AuditEventsService 3, AuditPage 12: initial query,
filter forwarding, offset-reset on Apply, clearFilters, pagination
disabled at boundaries, outcome-badge variants, payload disclosure
visibility, empty / loading / error states).