Commit Graph

3 Commits

Author SHA1 Message Date
Julien Gautier 3a44a1deed feat(portal-bff): audit log foundation per ADR-0013
CI / commits (pull_request) Successful in 1m27s
CI / check (pull_request) Successful in 1m37s
CI / scan (pull_request) Successful in 1m37s
CI / a11y (pull_request) Successful in 1m42s
CI / perf (pull_request) Successful in 3m19s
Lays down the append-only audit log: schema, migration with role
grants, NestJS AuditWriter service. Typed event-family methods,
separate AUDIT_DATABASE_URL pool, retention job, and live-DB
integration tests are explicitly listed as "wired as features
land" in ADR-0013 §Confirmation.

Schema (apps/portal-bff/prisma/schema.prisma):

- multiSchema preview enabled; datasource declares public + audit
  schemas.
- AuditEvent model with: id (uuid), createdAt, eventType (free-form
  in v1, will formalise into a catalogue once we have N stable
  types), audience (workforce|customer enum), actorIdHash, traceId,
  subject, outcome (success|failure|denied enum), payload (jsonb).
- Indexes on createdAt, eventType, traceId — covering the obvious
  query shapes (date-range scan, by event family, by trace for
  log-audit correlation).

Migration (prisma/migrations/*_init_audit_schema/migration.sql):

- CREATE TABLE / enums via Prisma's standard output.
- Append-only contract re-applied explicitly: ALTER TABLE / TYPE
  OWNER TO audit_owner, then GRANT INSERT to audit_writer, SELECT
  to audit_reader, SELECT+DELETE to audit_archiver. SELECT is
  required for archiver because Postgres needs SELECT on every
  column referenced in DELETE's WHERE clause to evaluate "older
  than retention" — granted explicitly in this PR rather than
  silently failing later.
- USAGE on the enum types granted to all three roles so audit_
  writer's INSERT does not fail with "permission denied for type".
- No GRANT for UPDATE / TRUNCATE to anyone, including audit_owner
  at runtime — only fresh schema migrations amend the table.

Service (apps/portal-bff/src/audit/):

- AuditWriter.recordEvent(input) — single entry point. Wraps every
  INSERT in a transaction whose first statement is `SET LOCAL ROLE
  audit_writer`, so the role contract holds at runtime even from
  the otherwise-privileged BFF connection. SET LOCAL is reset on
  COMMIT/ROLLBACK so the pool's next consumer sees the original
  role.
- traceId auto-resolved from the active OTel span context.
- actorIdHash auto-resolved from CLS (key 'actorIdHash') with
  explicit input-side override; null when neither is set
  (placeholder until ADR-0009 / ADR-0010 guards populate CLS).
- Errors propagate (no catch-and-swallow), per ADR-0013's
  "blocking writes: no audit ⇒ no action".
- AuditModule exposes the writer; wired into AppModule so any
  future feature module can inject it.

Tests (apps/portal-bff/src/audit/audit.service.spec.ts, 8 cases):

- Locks the transaction to audit_writer before INSERTing.
- Passes input fields through.
- Records Prisma.JsonNull when no payload is provided.
- Reads actorIdHash from CLS when not passed.
- Prefers explicit actorIdHash over CLS-resolved.
- Stores actorIdHash = null when neither input nor CLS has one.
- Captures the active OTel trace id.
- Stores traceId = null when no span is active.
- Propagates the underlying error (no swallow).

End-to-end smoke against local-dev Postgres (manual, via psql):

- INSERT under audit_writer: ok.
- UPDATE under audit_writer: "permission denied for table events".
- DELETE under audit_writer: "permission denied for table events".
- DELETE under audit_archiver (after the SELECT grant fix): ok,
  row removed.

ADR-0013 §Confirmation rewritten as "wired in foundation PR" /
"wired as features land", with the latter listing the typed
event-family methods, AUDIT_DATABASE_URL split, startup self-test
probe, retention purge job, salt-shared cross-correlation test,
and live-DB role-contract integration tests.
2026-05-10 03:41:32 +02:00
julien a0f6e594ba fix(portal-bff): downgrade Prisma to 6.x for nestjs-prisma compatibility (#3)
CI / commits (push) Has been skipped
CI / check (push) Failing after 4s
CI / scan (push) Failing after 6s
CI / a11y (push) Failing after 3s
CI / perf (push) Failing after 4s
## Summary

- Pin `prisma` and `@prisma/client` to `^6` (resolved 6.19.3) instead of the previous `^7`.
- Switch the generator in `apps/portal-bff/prisma/schema.prisma` from `prisma-client` to `prisma-client-js` (Prisma 6's default).
- Add the explicit `url = env("DATABASE_URL")` in the `datasource db` block (required by Prisma 6; was implicit in Prisma 7 via `prisma.config.ts`).
- Remove `apps/portal-bff/prisma.config.ts` (Prisma 7-only feature).
- Drop the now-irrelevant `/generated/prisma` entry from `apps/portal-bff/.gitignore`.

## Motivation

Two distinct Prisma 7 breaking changes surfaced as runtime errors in `pnpm nx serve portal-bff`:

1. **Generator output path:** Prisma 7's default `prisma-client` generator writes to a custom output dir declared in the schema. `@prisma/client/default.js`'s runtime stub still resolves `.prisma/client/default` in a sibling `node_modules/.prisma/client/`, which only the legacy `prisma-client-js` generator populates. Result: `ESM loader error: Cannot find module '.prisma/client/default'`.

2. **PrismaClientOptions API:** In Prisma 7, `PrismaClientOptions` no longer exposes `datasourceUrl` nor `datasources`. The connection must come through a driver adapter (e.g. `@prisma/adapter-pg`). `nestjs-prisma@0.27.0` calls `super(undefined)` when no `prismaServiceOptions` is passed, which Prisma 7 rejects with `PrismaClientInitializationError: PrismaClient needs to be constructed with a non-empty, valid PrismaClientOptions`.

Both issues are downstream of Prisma 7's "adapter-first" architecture being incompatible with `nestjs-prisma`'s still-Prisma-6-shaped wrapper. Working around each issue separately would lead into bespoke wiring (custom PrismaService, manual `@prisma/adapter-pg` install, hand-rolled DI). That's bricolage on a foundational layer.

Per CLAUDE.md ("default to stable, recognized, battle-tested choices; cutting-edge alternatives only when the trade-off is captured in an ADR"), the cheapest, cleanest fix is to use Prisma 6 — still actively maintained, fully aligned with `nestjs-prisma`'s design, supported by every tutorial and example in the wider ecosystem.

## Implementation notes

- ADR-0006 ("Persistence — PostgreSQL with Prisma") specifies "Prisma" without pinning a version. The version choice is a tactical detail. No ADR amendment.
- `apps/portal-bff/.env.example` is unchanged — the `DATABASE_URL` variable name is the same in Prisma 6 and 7.
- `prisma.config.ts` removed: it was a Prisma 7 file that Prisma 6 ignores; keeping it would only confuse a future contributor.
- The Prisma 7 `prisma-client` generator left a residual output dir at `apps/portal-bff/generated/` from the previous setup; removed locally and excluded from gitignore (no longer needed).
- Re-evaluate when `nestjs-prisma` releases an update aligned with Prisma 7's adapter model. The path back is symmetric: pin Prisma to `^7`, restore the schema's `prisma-client` generator, install `@prisma/adapter-pg`, and update `app.module.ts` to instantiate the adapter.

## Verification

- [x] `pnpm exec prisma generate` populates `node_modules/.../@prisma+client@6.19.3/.prisma/client/default.js` (no more 7.x in the active resolution).
- [x] `pnpm nx build portal-bff` green.
- [X] `pnpm nx serve portal-bff` boots cleanly (no `PrismaClientInitializationError`) — to be confirmed locally before merge. Connection to a real Postgres is out of scope of this PR; if Postgres is not running, an `ECONNREFUSED` is expected and unrelated.
- [ ] `pnpm ci:check` — runs in CI on PR open.

## Related

- [ADR-0006 — Persistence: PostgreSQL with Prisma](docs/decisions/0006-persistence-postgresql-prisma.md). Generator and version are tactical; no amendment.
- Future: revisit Prisma version when `nestjs-prisma` ships an update with first-class driver-adapter support.

---------

Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr>
Reviewed-on: #3
2026-05-04 10:46:12 +02:00
Julien Gautier 2b0e20bd85 chore: wire PostgreSQL + Prisma per ADR-0006
Add Prisma 7 + nestjs-prisma. The schema lives at
apps/portal-bff/prisma/schema.prisma with provider postgresql; the new
prisma-client generator (Prisma 7 default) outputs the typed client to
apps/portal-bff/generated/prisma/ which is gitignored.

apps/portal-bff/src/app/app.module.ts imports PrismaModule.forRoot
({ isGlobal: true }) so PrismaService is injectable across the BFF
without per-module imports.

apps/portal-bff/.env.example documents DATABASE_URL with a local-dev
default, plus a forward list of env vars introduced by upcoming phases
and ADRs (auth, sessions, MFA, observability, audit, downstream APIs)
- catalog reference, not implementation. The actual .env stays
gitignored at both repo root and app levels.

prisma.config.ts (Prisma 7's TypeScript config) is committed; it loads
DATABASE_URL via dotenv. Schema and migrations paths are pinned to
prisma/ relative to the bff app.

PostgreSQL provisioning, RLS policies for the dual-audience design,
the dedicated audit schema with role grants (audit_owner / audit_writer
/ audit_reader / audit_archiver per ADR-0013), and column-level
encryption for L3-scoped data are out of scope of this commit -
they belong with the future on-prem infrastructure ADR.
2026-04-30 17:28:54 +02:00