Commit Graph

63 Commits

Author SHA1 Message Date
julien 02ac44e498 feat(portal-bff): audit log foundation per ADR-0013 (#76)
CI / check (push) Successful in 1m52s
CI / commits (push) Has been skipped
CI / scan (push) Successful in 1m7s
CI / a11y (push) Successful in 52s
CI / perf (push) Successful in 2m17s
## Summary

Lays down the append-only audit log per ADR-0013: schema declaration, first migration with role grants, NestJS `AuditWriter` service. Typed event-family methods, the separate `AUDIT_DATABASE_URL` pool, the retention job, and the live-DB integration tests are explicitly listed as "wired as features land" in the ADR's confirmation block — they ship when the matching feature ADRs do.

## What lands

**Prisma schema** ([`apps/portal-bff/prisma/schema.prisma`](apps/portal-bff/prisma/schema.prisma)):

- `multiSchema` preview enabled; datasource declares `public` + `audit` schemas.
- `AuditEvent` model: `id` (uuid), `createdAt`, `eventType` (free-form in v1), `audience` enum (`workforce | customer`), `actorIdHash`, `traceId`, `subject`, `outcome` enum (`success | failure | denied`), `payload` (jsonb).
- Indexes on `createdAt`, `eventType`, `traceId` — covering the three obvious query shapes.

**Migration** ([`prisma/migrations/*_init_audit_schema/migration.sql`](apps/portal-bff/prisma/migrations/20260510011453_init_audit_schema/migration.sql)):

- Standard Prisma `CREATE TABLE` / enums output, then the **append-only contract** re-applied explicitly:
  - `ALTER TABLE/TYPE OWNER TO audit_owner`.
  - `GRANT INSERT` to `audit_writer`, `SELECT` to `audit_reader`, **`SELECT, DELETE`** to `audit_archiver` (SELECT is needed to evaluate the `created_at` predicate of "delete older than retention" — Postgres requires SELECT on every column referenced in DELETE's WHERE).
  - `GRANT USAGE` on the enum types to all three roles (without it `audit_writer.INSERT` fails 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/`](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.
- `traceId` auto-resolved from the active OTel span (so audit row joins with traces and Pino logs on the same `trace_id`).
- `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".

**Tests** — 8 unit tests on `AuditWriter` (mocked Prisma + CLS): role-locking ordering, input pass-through, `Prisma.JsonNull` for missing payload, CLS-vs-input precedence on `actorIdHash`, OTel trace capture, error propagation.

## End-to-end verification (manual, against local-dev Postgres)

```
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: ok, row removed (after the SELECT-grant fix)
```

## ADR-0013 §Confirmation rewritten

Two-block split: "wired in foundation PR" lists what landed here; "wired as features land" lists the typed event-family methods, AUDIT_DATABASE_URL connection split, startup self-test probe, retention purge job, salt-shared cross-correlation test, and live-DB role-contract integration tests — each anchored to the feature ADR that triggers it.

## Recovery for anyone with a pre-existing local-dev DB

If your local-dev Postgres already had the audit migration applied **before** the SELECT-grant fix, the archiver's DELETE will fail. Two options:

1. Apply the missing grant directly:
   ```bash
   psql "$DATABASE_URL" -c "GRANT SELECT ON audit.events TO audit_archiver;"
   ```
2. Or wipe the volume and re-migrate cleanly:
   ```bash
   ./infra/local/dev.sh down -v
   ./infra/local/dev.sh up
   pnpm --filter @apf-portal/source exec prisma migrate deploy   # or `cd apps/portal-bff && pnpm exec prisma migrate deploy`
   ```

Fresh DBs land with the corrected migration directly.

## Out of scope (separate PRs)

- Typed event-family methods (`signIn`, `signInFailed`, …) — added per matching feature ADR.
- `AUDIT_DATABASE_URL` separate connection pool — defense-in-depth, when production needs it.
- Startup self-test probe (deliberate failing UPDATE asserting rejection) — lands with the connection split.
- Retention purge job (`audit_archiver` daily cron) — phase-3b infra.
- Live-DB integration tests asserting the role contract — Testcontainers-style harness, separate PR.

## Test plan

- [ ] CI green on this PR.
- [ ] `prisma migrate deploy` succeeds on a fresh DB (the recovery instructions cover the SELECT-grant gap for already-migrated dev DBs).
- [ ] `psql -c "\dp audit.events"` shows the expected privilege matrix: `audit_owner=arwdDxtm/audit_owner`, `audit_writer=a/audit_owner`, `audit_reader=r/audit_owner`, `audit_archiver=rd/audit_owner`.
- [ ] BFF boots; calling `AuditWriter.recordEvent` from a controller (manual smoke once a real flow lands) writes to `audit.events` with the expected `trace_id` matching the request's Jaeger span.

---------

Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr>
Reviewed-on: #76
2026-05-10 03:44:01 +02:00
julien e2dd2e4dd8 fix(portal-shell): use .postcssrc.json so tailwind utilities are emitted (#75)
CI / check (push) Successful in 2m16s
CI / commits (push) Has been skipped
CI / scan (push) Successful in 1m23s
CI / a11y (push) Successful in 1m8s
CI / perf (push) Successful in 2m36s
## Summary

The portal-shell scaffold shipped a `postcss.config.js`, but `@angular/build` (Angular 17+ esbuild pipeline) does **not** load that file format — it only reads `.postcssrc.json`. Result: the `@tailwindcss/postcss` plugin was never registered, Tailwind ran with no source files visible, generated only its `@theme` block, and dropped every utility class on the floor.

The page therefore rendered unstyled — black text on white — even though `nx build` reported success and shipped a 23 KB `styles*.css`.

Rename / re-encode the config to `.postcssrc.json`. Same plugin, same options, just the file format Angular's esbuild adapter actually reads.

## Verification

| | Before | After |
| - | - | - |
| Class selectors in `dist/apps/portal-shell/browser/styles*.css` | **0** | **75** (production) |
| `.text-3xl`, `.bg-amber-50`, `.font-semibold` etc. emitted |  | ✓ |
| Pages render with intended Tailwind layout |  | ✓ |

```bash
pnpm exec nx build portal-shell --configuration=production
grep -oE '\.[a-zA-Z][a-zA-Z0-9_-]*' dist/apps/portal-shell/browser/styles*.css | sort -u | wc -l
# 75
```

## Doc update

`docs/development.md` repo-layout walkthrough now reads `.postcssrc.json` and explicitly calls out that `postcss.config.js` is ignored by `@angular/build` — so a future contributor reaching for the legacy filename gets a hint instead of a silent failure.

## Test plan

- [ ] CI green on this PR.
- [ ] Local: bring up the SPA dev server, `localhost:4200` shows the styled layout — header bar with brand link, system-status widget with rounded card, footer with the two language links.
- [ ] Production build: `nx build portal-shell --configuration=production` succeeds; `dist/.../styles*.css` contains tens of utility-class selectors (not just `@theme` tokens).

---------

Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr>
Reviewed-on: #75
2026-05-10 02:55:15 +02:00
julien 0d31937aeb feat(portal-shell): replace nx-welcome with real shell and home + budgets per ADR-0017 (#74)
CI / check (push) Successful in 2m4s
CI / commits (push) Has been skipped
CI / scan (push) Successful in 2m6s
CI / a11y (push) Successful in 1m14s
CI / perf (push) Successful in 3m3s
## Summary

Cuts the Nx scaffold's `NxWelcome` page (938 LOC of placeholder markup) and lays down the actual portal layout — header, main, footer landmarks plus a skip link — with a real home page and the two RGAA-mandated accessibility statement routes. Bundle budgets in `project.json` are tightened to match ADR-0017.

## What lands

**Layout shell** (`apps/portal-shell/src/app/`):

- `components/header/` — brand link + primary nav landmark stub
- `components/footer/` — accessibility statement links (FR + EN) + version badge
- `app.html` / `app.scss` — flex column with sticky footer + skip link to `#main-content` (WCAG 2.4.1)

**Pages**:

- `pages/home/` — welcome heading + "system status" widget that fetches `/api/health` from the BFF. Doubles as a smoke test of the full SPA → BFF stack: CORS, OTel trace propagation, Pino log correlation. After page load, http://localhost:16686 should show one trace whose root span is the SPA `document_load`, with a child `fetch` span that has its own child `HTTP GET /api/health` BFF span.
- `pages/accessibility/` — single component, content selected by route data (`lang: 'fr' | 'en'`). Mounted at `/accessibility` (en) and `/accessibilite` (fr). v1 carries placeholder copy explicitly framed as "awaiting APF user panel review" — RGAA + ADR-0016 require the routes to exist; the substantive statement (audit grid, gaps list, remediation plan) lands separately.

**Routing & wiring**:

- `app.routes.ts` adds the three routes, all lazy-loaded.
- `app.config.ts` adds `provideHttpClient(withFetch())` so HttpClient delegates to the patched browser `fetch` and the W3C `traceparent` header propagates automatically (per ADR-0012 phase 2).
- `nx-welcome.ts` removed; `app.ts` no longer imports it.

**Bundle budgets** (`apps/portal-shell/project.json`):

| Type                | Limit (raw)  | ADR-0017 (gzip) |
| ------------------- | ------------ | --------------- |
| `initial`           | 1 MB         | ≤ 300 KB        |
| `anyScript`         | 300 KB       | ≤ 100 KB / chunk |
| `anyComponentStyle` | 6 KB         | ≤ 6 KB           |
| `bundle "styles"`   | 150 KB       | ≤ 150 KB         |

`maximumWarning == maximumError` so an overshoot fails the build (matching `type: "error"` in spirit). Angular CLI compares **raw** sizes; ADR-0017 §Confirmation is updated to record the gzip→raw translation and the deferred follow-up that will add a CI check on the actual gzipped transfer size (Angular has no native gzip-mode budget).

## Verified locally

- `pnpm exec nx run-many -t lint test build` → 8 projects green; **12 tests pass** for `portal-shell` across 5 specs (App, Header, Footer, Home, AccessibilityStatement).
- `pnpm nx build portal-shell --configuration=production` → initial bundle **326.99 KB raw / 89.82 KB transfer** (gzip) — well under the ADR-0017 300 KB gzip target. Lazy chunks: home 2.66 KB, accessibility 2.90 KB.
- `pnpm audit` clean.

After `./infra/local/dev.sh up observability` + `pnpm nx serve portal-bff` + `pnpm nx serve portal-shell`, opening http://localhost:4200 shows the new layout, the system-status widget connects to the BFF, and the corresponding trace appears in Jaeger.

## Out of scope (separate PRs)

- **Real RGAA audit content** — APF user-panel review.
- **Design tokens system** in `libs/shared/tokens`.
- **Reusable UI primitives** in `libs/shared/ui`.
- **User-preferences panel** (ADR-0016 §"User-preferences panel").
- **i18n machinery** (`@angular/localize`) — for v1 the FR/EN copy is inlined and route-driven; real i18n is a separate ADR.
- **Env-config** (Angular `environment.ts`) — the hard-coded `http://localhost:3000/api/health` will move there alongside the OTLP endpoint when the env-config PR lands.
- **CI gzip-transfer-size assertion** to complement the raw-size Angular budgets.

## Test plan

- [ ] CI green on this PR.
- [ ] Local: home page renders, system-status widget transitions from "Checking the backend…" to the success state when the BFF is up; falls back to "Backend unreachable" with the BFF stopped.
- [ ] `/accessibility` and `/accessibilite` render the matching language with `<article lang="en|fr">`.
- [ ] Skip link reachable via Tab from address bar; clicking it focuses `#main-content`.
- [ ] Production build size summary roughly matches the figures above (initial total ≈ 90 KB transfer).

---------

Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr>
Reviewed-on: #74
2026-05-10 02:38:42 +02:00
julien 8f2cd4e068 feat(portal-shell): wire spa-side opentelemetry tracing (#72)
CI / commits (push) Has been skipped
CI / scan (push) Successful in 1m15s
CI / a11y (push) Successful in 1m6s
CI / perf (push) Successful in 2m0s
CI / check (push) Successful in 43s
## Summary

Phase 2 of ADR-0012 — closes the loop SPA → BFF → DB. After this PR, a single user action (initial page load, click, form submit) produces one trace whose root span is owned by the SPA and whose child spans cover the BFF request, Postgres queries through Prisma, and (eventually) Redis / downstream-API hops.

## What lands

**Browser-side OTel libs** (production deps):

- `@opentelemetry/sdk-trace-web` — browser tracer + provider
- `@opentelemetry/exporter-trace-otlp-http` — OTLP/HTTP+JSON exporter
- `@opentelemetry/instrumentation` — auto-instrumentation runtime
- `@opentelemetry/instrumentation-fetch` — `fetch` + W3C `traceparent` propagation
- `@opentelemetry/instrumentation-document-load` — initial-paint timings
- `@opentelemetry/instrumentation-user-interaction` — click / keypress / submit

No `@opentelemetry/context-zone`: the workspace is zoneless per ADR-0004; the default `StackContextManager` covers the auto-instrumented paths. Custom spans across `await` will need explicit `context.with(...)` plumbing — fine, encountered as code lands.

**Code**:

- [`apps/portal-shell/src/observability/tracing.ts`](apps/portal-shell/src/observability/tracing.ts) — `WebTracerProvider` bootstrap. Documents the load-order constraint inline (same pattern as the BFF: must be the very first import of `main.ts`, otherwise auto-instrumentations miss everything imported above).
- `apps/portal-shell/src/main.ts` now imports the tracing module as line 1.

**CORS plumbing** for end-to-end trace propagation:

- BFF (`apps/portal-bff/src/main.ts`) calls `enableCors` with a minimal dev allowlist (`http://localhost:4200`) and explicit permission for the W3C `traceparent` / `tracestate` headers. The full security-grade CORS (per-environment allowlists, helmet, cookie-session, CSRF) belongs to the future phase-2 security ADR — this PR adds the strict minimum for the SPA→BFF trace context to survive cross-origin pre-flight.
- OTel Collector (`infra/local/otel-collector.yaml`) gains a `cors` block on its OTLP/HTTP receiver so the browser's own OTLP POST clears its pre-flight.

**ADR-0012 §Confirmation** rewritten: a new "Wired in the SPA foundation PR (phase 2)" block enumerates what landed here; the carry-over "Wired as features land" list drops the SPA-side SDK item and adds a follow-up note about the security-grade CORS.

## Verification

```bash
pnpm exec nx run-many -t lint test build           # 8 projects green
pnpm audit                                          # 0 vulns
./infra/local/dev.sh up observability               # bring up Collector + Jaeger
./infra/local/dev.sh                                # (separately, BFF stack — your choice)
pnpm nx serve portal-bff                           # localhost:3000
pnpm nx serve portal-shell                         # localhost:4200
```

Open http://localhost:4200 → a `document_load` trace appears in http://localhost:16686 with `service.name=portal-shell`. From DevTools, run `fetch('http://localhost:3000/api/health').then(r => r.json())` → a fetch span appears with a child BFF span on the same trace.

## Test plan

- [ ] CI green on this PR.
- [ ] After local up, `document_load` span visible in Jaeger UI for the SPA.
- [ ] Cross-origin fetch from SPA carries `traceparent` (visible in Network tab) and produces a single end-to-end trace SPA → BFF in Jaeger.
- [ ] DevTools console shows no CORS warnings about `traceparent`, `tracestate`, or the `localhost:4318/v1/traces` POST.

---------

Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr>
Reviewed-on: #72
2026-05-09 23:23:18 +02:00
julien c3c15585ff style(portal-bff): apply prettier to check-database-url.ts (#71)
CI / check (push) Successful in 1m43s
CI / commits (push) Has been skipped
CI / scan (push) Successful in 1m33s
CI / a11y (push) Successful in 42s
CI / perf (push) Successful in 2m21s
## Summary

Cosmetic-only follow-up to #70. Collapses one over-cautious multi-line `throw new Error(...)` into a single line that fits within the project's 100-char `printWidth`.

The reformat was produced by lint-staged **during** the amend that landed #70, but applied to the working tree only — the modification never made it back into the staged content of the amend. The merged commit therefore carries the un-prettified version.

Spotted locally with `pnpm exec prettier --check apps/portal-bff/src/config/check-database-url.ts` failing. Left unchecked, the next PR's `check` job would break at `format:check` for unrelated reasons.

## Test plan

- [ ] CI green on this PR (`format:check` clean).
- [ ] No behavioural change — only whitespace.

---------

Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr>
Reviewed-on: #71
2026-05-09 22:45:10 +02:00
julien b74d3f1b9b feat(portal-bff): observability foundations (Pino + CLS + OTel) (#70)
CI / commits (push) Has been skipped
CI / scan (push) Successful in 1m33s
CI / check (push) Successful in 2m3s
CI / a11y (push) Successful in 1m11s
CI / perf (push) Successful in 2m14s
## Summary

Implements ADR-0012 phase 1, BFF side. The SPA wiring is a separate phase-2 PR.

The BFF now emits structured JSON logs to stdout, tagged with `trace_id` / `span_id` from the active OTel context, and exports OTLP traces over HTTP/Protobuf to the Collector that already runs in the local-dev compose. Anything Nest, Express, HTTP-out, Prisma (Postgres) or `ioredis` does is auto-spanned. A `GET /api/health` liveness endpoint is added to round things out.

## What lands

**Runtime libs added** (production deps):

- `nestjs-pino`, `pino`, `pino-http` — structured logging
- `nestjs-cls` — request-scoped context
- `@opentelemetry/api` / `sdk-node` / `resources` / `semantic-conventions`
- `@opentelemetry/exporter-trace-otlp-proto` (HTTP/Protobuf, port 4318)
- `@opentelemetry/instrumentation-{http,express,nestjs-core,pg,ioredis,pino}` — curated, **no** `auto-instrumentations-node` mass-import (anti-bricolage)

Dev: `pino-pretty` (gated by `NODE_ENV`).

**Code:**

- `apps/portal-bff/src/observability/tracing.ts` — OTel `NodeSDK` bootstrap. Documents the load-order constraint inline (must be the very first import of `main.ts`). Pure side-effect module.
- `apps/portal-bff/src/observability/observability.module.ts` — composes `ClsModule` (UUID per request stored as `request_id`) and `LoggerModule` (`pino-pretty` in dev, raw JSON in prod, `LOG_LEVEL` env-driven, `/health` excluded from auto-logging, `X-Request-Id` honoured if inbound).
- `apps/portal-bff/src/health/{health.controller,health.module,health.controller.spec}.ts` — `GET /api/health` returning `{status, uptimeSeconds, service, version}`. Cheap liveness only — `/readiness` lands when dependencies have a readiness story.
- `apps/portal-bff/src/config/check-database-url.{ts,spec.ts}` — fail-fast validator called from `main.ts` before NestFactory boots. Catches the same family of bug that bit pgweb in #63: a literal special character in `POSTGRES_PASSWORD` that needs URL-encoding in `DATABASE_URL`. Prisma requires a URL string (no discrete-flag escape hatch), so early validation + a clear error message is the v1 mitigation. Six unit tests cover happy path, missing URL, wrong scheme, encoded special chars, literal `@` in password, malformed URL.

**Wiring:**

- `main.ts` imports `./observability/tracing` as line 1, then uses `app.get(Logger)` from `nestjs-pino` with `bufferLogs: true` so early-bootstrap lines are not lost.
- `app.module.ts` imports `ObservabilityModule` first, then `PrismaModule`, then `HealthModule`.
- `apps/portal-bff/.env.example` promotes `LOG_LEVEL`, `OTEL_SERVICE_NAME`, `OTEL_SERVICE_VERSION`, `OTEL_EXPORTER_OTLP_ENDPOINT`, `OTEL_EXPORTER_OTLP_PROTOCOL`, `OTEL_TRACES_SAMPLER` from the "future" comment to active settings — defaults target the local-dev Collector.
- Both `apps/portal-bff/.env.example` and `infra/local/.env.example` now spell out the URL-encoding constraint on `POSTGRES_PASSWORD` with the char-by-char encoding table (`@` → `%40`, etc.).

**ADR-0012 §Confirmation** rewritten to distinguish what landed in this PR from what is wired as the corresponding feature ADRs ship (CLS keys for `session_id` / `user_id_hash` / `audience`, `LOG_USER_ID_SALT` enforcement, redact list, custom spans, SPA-side SDK, full integration tests, prod Collector config).

## Trace ↔ log correlation

Automatic via `@opentelemetry/instrumentation-pino` — every Pino record gets `trace_id` and `span_id` injected from the active OTel context. No CLS gymnastics needed for that concern.

## Verification

```bash
pnpm exec nx run-many -t lint test build       # 8 projects green
pnpm audit --audit-level=moderate              # 0 vulnerabilities
./infra/local/dev.sh up observability          # start Collector + Jaeger
cp apps/portal-bff/.env.example apps/portal-bff/.env
pnpm nx serve portal-bff
curl http://localhost:3000/api/health
# → {"status":"ok","uptimeSeconds":N,"service":"portal-bff","version":"dev"}
```

Then hit `GET http://localhost:3000/api` once or twice and open http://localhost:16686 — the corresponding spans appear in Jaeger, and Pino logs on stdout carry the matching `trace_id`.

## Test plan

- [ ] `nx run-many -t lint test build` green on this PR's CI run.
- [ ] `pnpm audit` clean.
- [ ] BFF boots, `/api/health` returns the expected JSON.
- [ ] Pino logs in dev are colourised one-liners; in prod they would be raw JSON (toggled by `NODE_ENV=production`).
- [ ] With the local-dev stack's `--profile observability` active, traces are visible in Jaeger UI.
- [ ] Each Pino log line for a request carries the same `trace_id` as the trace span in Jaeger.

---------

Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr>
Reviewed-on: #70
2026-05-09 22:28:17 +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 bd8eefb44a chore: configure Tailwind 4 and align lib tsconfigs
Wire Tailwind CSS 4 in the portal-shell app per ADR-0016 (the future
host of spartan-ng components in libs/shared/ui will read from the
Tailwind tokens via the shared-tokens lib).

- pnpm add -D tailwindcss @tailwindcss/postcss postcss
- apps/portal-shell/postcss.config.js declares @tailwindcss/postcss
- apps/portal-shell/src/styles.scss renamed to styles.css and now
  contains a single @import 'tailwindcss' directive. Plain CSS for the
  global file avoids the Sass @import deprecation warning that fires
  when Tailwind directives sit inside SCSS. Component-level styles can
  still use SCSS.
- apps/portal-shell/project.json styles entry updated accordingly.

Side fix: align libs/shared/tokens and libs/shared/util tsconfigs from
module: commonjs to module: esnext. The Nx @nx/js:library --bundler=
tsc generator emits commonjs by default, but tsconfig.base.json
specifies moduleResolution: bundler, which TS only allows alongside
esnext or es2015+ modules. Without the alignment, those libs failed to
build (TS5095). All apps and libs now build green.

spartan-ng wiring is intentionally NOT in this commit. spartan-ng is
currently at 0.0.1-alpha.681 - clearly pre-1.0, which trips the
project rule against pre-1.0 dependencies. ADR-0016 chose spartan-ng
with the copy-paste mitigation, but the alpha state warrants an
explicit go/no-go decision before committing the workspace to it.
2026-04-30 17:50:35 +02:00
Julien Gautier 8de19320c5 chore: generate shared and feature libs with module boundaries per ADR-0003
Generate the four phase-4 libraries:

- libs/shared/tokens (project name shared-tokens) - plain TS lib via
  @nx/js:library; will host the a11y design tokens (palette,
  contrast tiers, spacing, motion) once Tailwind lands in phase 5;
  consumable by both apps; tagged scope:shared, type:shared.
- libs/shared/util (shared-util) - plain TS lib for cross-cutting
  utility code; tagged scope:shared, type:shared.
- libs/shared/ui (shared-ui) - Angular standalone library that will
  host the spartan-ng components copy-pasted in phase 5; Angular-only
  so tagged scope:portal-shell, type:shared. unitTestRunner=
  vitest-analog because vitest-angular requires a buildable lib.
- libs/feature/auth (feature-auth) - placeholder Angular standalone
  feature lib to demonstrate the type:feature pattern; tagged
  scope:portal-shell, type:feature.

@nx/enforce-module-boundaries depConstraints replaced (root
eslint.config.mjs) with the rules from ADR-0003:

  scope:portal-shell -> scope:portal-shell, scope:shared
  scope:portal-bff   -> scope:portal-bff,   scope:shared
  scope:shared       -> scope:shared
  type:app           -> type:feature, type:shared
  type:feature       -> type:feature, type:shared
  type:shared        -> type:shared

This forbids portal-shell from importing portal-bff code (and vice
versa) and prevents shared libs from depending on feature libs.

Project names follow the convention of ADR-0003 (feature-<name> /
shared-<scope>) by passing --name explicitly to the generator; the Nx
22 default takes only the last directory segment.

Sanity check: pnpm nx run-many -t lint and -t test pass for the 8
projects (4 apps/e2e + 4 libs).

Side effects from the generators: tsconfig.base.json paths populated
with the lib import aliases; nx.json gains vite/playwright plugin
entries; .gitignore picks up vitest.config.*.timestamp* (Vitest temp
files); package.json gains @analogjs/vitest-angular and related
devDeps; pnpm-lock.yaml regenerated. Two eslint-disable comments in
portal-bff-e2e support files were trimmed by lint --fix - those files
already lint clean without the directive.
2026-04-30 17:37:29 +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
Julien Gautier cd1d482aa8 fix(portal-shell): make 'nx test' run once by default, add watch configuration
The Angular 21 unit-test builder (@angular/build:unit-test) defaults
to watch mode. Without an explicit option, 'pnpm nx test portal-shell'
hangs on 'Waiting for task' indefinitely - unsuitable for CI and
surprising for ad-hoc invocations.

Pin watch=false as the default in the target options. Add a 'watch'
configuration so developers who want continuous test running can opt
in with 'pnpm nx test portal-shell --configuration=watch'. portal-bff
uses Jest which defaults to no-watch and needs no change.
2026-04-30 16:44:33 +02:00
Julien Gautier 0774014599 fix(portal-bff): use bracket notation for process.env access
The strict-TS option noPropertyAccessFromIndexSignature: true (set in
tsconfig.base.json per ADR-0004) forbids dot-notation access on index
signatures. process.env is typed as { [key: string]: string | undefined }
so process.env.PORT must be written process.env['PORT']. The Nx
generator wrote the dot form by default; fix to comply with the
project's strict-TS bar.

Touched: portal-bff main.ts and the three portal-bff-e2e support files
(global-setup, global-teardown, test-setup).
2026-04-30 16:34:17 +02:00
Julien Gautier bea5e1954f chore: generate portal-shell and portal-bff apps per ADR-0004 / ADR-0005
Add the @nx/angular, @nx/nest, @nx/vite, @nx/eslint plugins, then
generate the two apps. Adjust the empty-template tsconfig.base.json
to be Angular-compatible (drop project references and customConditions
that the empty-template defaults to but Angular doesn't support; keep
the strict-TS extensions from ADR-0004).

apps/portal-shell (Angular 21):
- standalone APIs, routing, SCSS, esbuild
- vitest-angular as unitTestRunner, playwright for e2e
- strict mode
- tags scope:portal-shell, type:app
- app.config.ts wired with provideZonelessChangeDetection() per
  ADR-0004 (Angular 21 + Nx 22 generates without zone.js by default)

apps/portal-bff (NestJS 11):
- Express adapter (default per ADR-0005)
- Jest as unitTestRunner
- tags scope:portal-bff, type:app
- main.ts wired with a global ValidationPipe configured
  whitelist + forbidNonWhitelisted + transform per ADR-0005
- Phase-2 security additions (helmet, CORS, sessions, CSRF, rate
  limit, auth guards, error filter) deferred to their respective
  ADRs - placeholder comment in main.ts

Workspace dependencies: class-validator + class-transformer added
(required by NestJS ValidationPipe at runtime). Nx-generated
.gitignore additions (.angular, __screenshots__) merged into ours.
.vscode/extensions.json and launch.json added by Nx are kept (do not
override our existing settings.json).
2026-04-30 16:12:42 +02:00