707ff25ee0
First real consumer of the admin module — paginated audit-log viewer
per ADR-0020's v1 catalogue, gated by @RequireAdmin and emitting
admin.audit.query on every read as the "fishing expedition" deterrent
called out in ADR-0020 §"Audit log captures … Read actions are also
captured … to deter fishing expeditions".
What ships
- AuditReader (apps/portal-bff/src/admin/audit-reader.service.ts):
- SET LOCAL ROLE audit_reader inside the transaction — symmetric
with AuditWriter's SET LOCAL ROLE audit_writer. The runtime
role contract holds even when the BFF connection is otherwise
privileged; UPDATE/INSERT/DELETE on audit.events fail at the
Postgres level.
- Parameterised SELECT only — never concatenates filter values
into SQL. Subject prefix uses LIKE with explicit ESCAPE and
escapes %/_ in the literal so an admin-side wildcard can't
masquerade as a meta-character.
- COUNT(*) + LIMIT/OFFSET pagination. Order: created_at DESC,
id DESC for deterministic page boundaries on identical
timestamps.
- Hard caps limit at MAX_LIMIT (200) even if a caller bypasses
the DTO — defense in depth on the BFF's event loop.
- AdminAuditQueryDto (apps/portal-bff/src/admin/audit-query.dto.ts):
- Filters: eventType, actorIdHash, audience, outcome,
subjectPrefix, createdAtFrom/createdAtTo (ISO-8601).
- Pagination: limit (1..200, default 50), offset (default 0).
- Wired through Nest's global ValidationPipe so unknown query keys
are rejected (forbidNonWhitelisted) and limit is bounded.
- AdminAuditController:
- GET /api/admin/audit, @RequireAdmin at the class level.
- Forwards the validated DTO to AuditReader, then emits
admin.audit.query with { filters, resultCount } as payload so a
reviewer can see what the admin searched for.
- @RequireMfa intentionally NOT applied in v1: the admin surface
already sits behind a freshly-MFA'd session at sign-in, and the
per-query audit row is the deterrent. Adding @RequireMfa later
is a one-line change.
- AuditWriter gains adminAuditQuery() typed method emitting the
event with outcome=success (the read happened; whether it
returned rows is captured separately in resultCount).
Tests: +30 specs (AuditReader 10, AdminAuditController 6, AuditWriter
typed event 2, DTO validation 12). All 308 BFF specs pass.
37 lines
1.5 KiB
TypeScript
37 lines
1.5 KiB
TypeScript
import { Module } from '@nestjs/common';
|
|
import { AuthModule } from '../auth/auth.module';
|
|
import { AdminAuditController } from './admin-audit.controller';
|
|
import { AdminAuthController } from './admin-auth.controller';
|
|
import { AdminController } from './admin.controller';
|
|
import { AdminRoleGuard } from './admin-role.guard';
|
|
import { AuditReader } from './audit-reader.service';
|
|
|
|
/**
|
|
* `AdminModule` — root of the `/api/admin/*` surface per ADR-0020.
|
|
*
|
|
* v1 ships:
|
|
* - `GET /api/admin/me` (self-test, guarded by `@RequireAdmin`).
|
|
* - `/api/admin/auth/{login,callback,me,logout}` — the distinct
|
|
* admin auth flow that establishes the `__Host-portal_admin_session`
|
|
* per ADR-0020 §"Sessions — distinct from `portal-shell`".
|
|
*
|
|
* Imports `AuthModule` to consume `AuthService`, `SessionEstablisher`,
|
|
* and `ENTRA_CONFIG` — the auth flow itself is shared with the
|
|
* user-portal `AuthController`; what differs is the redirect URIs
|
|
* passed in (admin-specific) and the session that gets populated
|
|
* (resolved by the path-routed session middleware in `main.ts`).
|
|
*
|
|
* The functional admin modules (audit log viewer, CMS, menu
|
|
* management, user list) land as separate sub-modules consumed from
|
|
* here in upcoming PRs.
|
|
*
|
|
* `AuditWriter` (required by `AdminRoleGuard`) is provided globally
|
|
* by `AuditModule`, so no extra import is needed for it.
|
|
*/
|
|
@Module({
|
|
imports: [AuthModule],
|
|
controllers: [AdminController, AdminAuthController, AdminAuditController],
|
|
providers: [AdminRoleGuard, AuditReader],
|
|
})
|
|
export class AdminModule {}
|