d592bd325a26ef1487eaf43da6ab0f3ee5eaec09
289 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
d592bd325a |
chore(deps): update dependency vite to v8.0.16
CI / commits (pull_request) Has been skipped
CI / perf (pull_request) Has been skipped
CI / scan (pull_request) Successful in 3m43s
CI / a11y (pull_request) Successful in 3m53s
CI / check (pull_request) Successful in 5m47s
Docs site / build (pull_request) Successful in 2m8s
|
||
|
|
cca3b76771 |
chore(deps): update dependency lint-staged to v17.0.6 (#257)
This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [lint-staged](https://github.com/lint-staged/lint-staged) | devDependencies | patch | [`17.0.5` -> `17.0.6`](https://renovatebot.com/diffs/npm/lint-staged/17.0.5/17.0.6) | --- ### Release Notes <details> <summary>lint-staged/lint-staged (lint-staged)</summary> ### [`v17.0.6`](https://github.com/lint-staged/lint-staged/blob/HEAD/CHANGELOG.md#1706) [Compare Source](https://github.com/lint-staged/lint-staged/compare/v17.0.5...v17.0.6) ##### Patch Changes - [#​1803](https://github.com/lint-staged/lint-staged/pull/1803) [`bdf2770`](https://github.com/lint-staged/lint-staged/commit/bdf27700a6e25b40333672eef4d438984a2d0383) - Run all tests with [Deno](https://deno.com), in addition to Node.js and Bun. - [#​1796](https://github.com/lint-staged/lint-staged/pull/1796) [`7508272`](https://github.com/lint-staged/lint-staged/commit/75082727cdd070adb59d62c9040515da3bbbb2f9) - Fix performance regression of *lint-staged* v17 by going back to using `git add` to stage task modifications. This was changed to `git update-index --again` in v17 for less manual work, but unfortunately the `update-index` command gets slower in very large Git repos. - [#​1797](https://github.com/lint-staged/lint-staged/pull/1797) [`7b2505a`](https://github.com/lint-staged/lint-staged/commit/7b2505a1f8fb8735e6306c7dabdd5295632f8c1a) - This version of *lint-staged* uses the new [staged publishing for npm packages](https://docs.npmjs.com/staged-publishing) feature. Releases are already published from GitHub Actions with [trusted publishing](https://docs.npmjs.com/trusted-publishers), but now an additional approval with two-factor authentication is also required. - [#​1802](https://github.com/lint-staged/lint-staged/pull/1802) [`321b0a9`](https://github.com/lint-staged/lint-staged/commit/321b0a972a434006f5b5fac18867974ef040d037) - Downgrade dependency `tinyexec@1.2.2` to avoid issues in version 1.2.3. </details> --- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Enabled. ♻ **Rebasing**: Whenever PR is behind base branch, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box --- This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate). <!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0MC42Mi4xIiwidXBkYXRlZEluVmVyIjoiNDAuNjIuMSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOlsiZGVwZW5kZW5jaWVzIl19--> Reviewed-on: #257 Co-authored-by: APF Portal Bot <jgautier.webdev+apf-portal-bot@gmail.com> Co-committed-by: APF Portal Bot <jgautier.webdev+apf-portal-bot@gmail.com> |
||
|
|
045ff924a8 |
fix(infra): exclude serve-static from 'dev.sh up all' (port collision with apps) (#261)
## Summary
Fix `./infra/local/dev.sh up all` failing on `port is already allocated` for port 4200: the `apps` profile (ADR-0030 — Angular dev-server on 4200) and the `serve-static` profile (Caddy reverse proxy for the production build, also defaulting to 4200) cannot run together. `up all` expanded to "every profile" without taking that conflict into account.
After this PR, `./infra/local/dev.sh up all` brings up the comprehensive dev stack — infra + `dbtools` + `observability` + `apps` — and `serve-static` stays available via the explicit `./infra/local/dev.sh up serve-static` invocation when a developer actually wants to test a production build.
## Root cause
`dev.compose.yml` publishes port 4200 in two services:
- `portal-shell` (profile `apps`): `${SHELL_PORT:-4200}:4200` — Angular dev-server.
- `serve-static` (profile `serve-static`): `${SERVE_STATIC_PORT:-4200}:4200` — Caddy serving the production build.
`dev.sh`'s `ALL_PROFILES` array was used both as:
1. the teardown / status / logs scope (so a manually-started profile is still reachable for `down` and friends), **and**
2. the expansion target for `up all`.
Conflating the two meant `up all` always tried to start `serve-static` even when `apps` was in scope — and the Docker port-publishing collision aborted the whole `up`.
## Fix
Split the two concerns:
```bash
# Every profile that exists — teardown / status / logs scope.
ALL_PROFILES=(dbtools observability serve-static apps)
# What `up all` expands to — serve-static excluded.
UP_ALL_PROFILES=(dbtools observability apps)
```
`serve-static` stays accessible:
- via explicit `dev.sh up serve-static` (the original way),
- via `dev.sh down` / `status` / `logs` (still in `ALL_PROFILES`).
Why exclude `serve-static` from `up all` rather than `apps`:
- `apps` is the new "no native toolchain" dev mode (ADR-0030) — it _is_ what a comprehensive dev `up` should boot.
- `serve-static` has zero value without a prior `nx build --configuration=production` (Caddy serves an empty `dist/` → 404 everywhere). Auto-starting it in `up all` would put a 404-machine in the stack by default.
- The port number can stay at the Angular convention (4200) for the dev-server, which matches the devcontainer's `forwardPorts`.
## What lands
| File | Change |
| --- | --- |
| `infra/local/dev.sh` | Split `ALL_PROFILES` (teardown scope) and `UP_ALL_PROFILES` (`up all` scope). Inline comment explains the exclusion of `serve-static`. Usage text updated. |
| `infra/README.md` | Cheat-sheet row for `up all` clarifies the new behaviour. |
## Out of scope
The script is **environment-agnostic** — it works identically when invoked on the local workstation or on `vm-dev`. Port publishing is on `0.0.0.0` by default, so services are reachable from a remote browser via the VM IP without any script change. The user-visible difference is only the URL host (`localhost:4200` locally vs `<vm-ip>:4200` from the workstation against the VM). No local-vs-vm mode in the script.
## Test plan
- [x] `bash -n infra/local/dev.sh` passes.
- [x] `./infra/local/dev.sh help` shows the updated `up all` description.
- [ ] **On vm-dev**: `./infra/local/dev.sh up all` brings up postgres / redis / otel-collector / pgweb / jaeger / apps-deps (exits 0) / portal-bff / portal-shell / portal-admin without port conflicts; `serve-static` is **not** started.
- [ ] `./infra/local/dev.sh up serve-static` (explicit) still starts Caddy on 4200, provided `apps` is **not** already up.
- [ ] `./infra/local/dev.sh down` and `status` still see serve-static if it was started manually (ALL_PROFILES retains it).
## Related
- Surfaced by the ADR-0030 dockerised dev mode VM validation — `up all` was added to the user-facing surface in that PR; this PR finishes wiring it.
- The port collision was flagged as a caveat in the ADR-0030 PR body ("don't run `apps` and `serve-static` together"). This PR turns the caveat into a script invariant instead of relying on the user to remember.
---------
Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr>
Reviewed-on: #261
|
||
|
|
f9f5f171eb |
fix(security): normalize IPv6 in rate-limit keyGenerator (ADR-0021) (#260)
## Summary
Fix `ERR_ERL_KEY_GEN_IPV6` raised by `express-rate-limit` at BFF boot: the rate-limit middleware's custom `keyGenerator` was using `req.ip` verbatim, which the library v8 refuses because it lets an IPv6 attacker rotate through the host bits of their own subnet to escape per-IP rate-limiting. Surfaced during the ADR-0030 dockerised dev mode validation (the boot log shows the `ValidationError` immediately after the rate-limit middleware is built).
## Root cause
`apps/portal-bff/src/security/rate-limit.middleware.ts` returned:
```ts
return `ip:${req.ip ?? 'unknown'}`;
```
`req.ip` is the raw address the IP-trust chain hands Express. For IPv4 that's already the right bucket key. For IPv6, every distinct host in an attacker's allocation hashes to a different bucket — even though the same human controls all of them. An attacker on a residential IPv6 assignment (typically `/56`) thus has ~2^72 trivially-rotatable buckets per `/56`, which makes the per-IP rate limit useless against them.
`express-rate-limit` v7+ ships an `ipKeyGenerator` helper that **truncates the address to its `/56` prefix** before keying. The library v8 raises `ERR_ERL_KEY_GEN_IPV6` at boot if a custom `keyGenerator` returns `req.ip` verbatim, precisely to refuse shipping this bypass.
## Fix
| File | Change |
| --- | --- |
| `apps/portal-bff/src/security/rate-limit.middleware.ts` | Import `ipKeyGenerator` from `express-rate-limit`; wrap `req.ip` through it when keying. IPv4 addresses pass through unchanged; IPv6 addresses get truncated to their `/56`. Comment explains the rationale + that the lib's `/56` default matches a typical residential ISP customer allocation. |
| `apps/portal-bff/src/security/rate-limit.middleware.spec.ts` | New test asserting two IPv6 addresses in the same `/56` share a bucket (the bypass would have set both apart), and that distinct `/56`s remain isolated (truncation does not collapse all IPv6 traffic into one global bucket). Existing IPv4 / session / `SKIP_PATHS` tests are unchanged and still pass — `ipKeyGenerator` is a no-op for IPv4. |
The session-keyed branch (`s:${sessionID}`) is untouched: sessions key on the BFF-issued session id, not the address.
## Why the BFF kept booting despite the error
The log shows `bootstrap` reaching `AuthModule` immediately after the `ValidationError` printout. `express-rate-limit` v8's `errorHandler` defaults to logging the error and continuing rather than throwing for this specific validation, so the middleware was effectively running with the unfixed `keyGenerator` until now — i.e. the bypass was live in the dev BFF. Fixed pre-emptively, before any prod consumer.
## Related
- Per [ADR-0021](docs/decisions/0021-phase-2-security-baseline.md) — Phase-2 security baseline, rate-limit section.
- Surfaced by the ADR-0030 dockerised dev mode validation (the SPA `/api/auth/me` proxy errors visible in the same log run were a side effect of the BFF restarting on every config validator until the env was fully populated; unrelated to this fix).
## Test plan
- [x] `pnpm exec nx test portal-bff --testFile=apps/portal-bff/src/security/rate-limit.middleware.spec.ts` — 794 tests pass, including the new `/56` isolation case.
- [ ] Restart the BFF (`./infra/local/dev.sh restart portal-bff`) — `ERR_ERL_KEY_GEN_IPV6` no longer appears in the boot log.
- [ ] IPv4 traffic still rate-limits as before (existing test coverage; no behavioural change since `ipKeyGenerator` is identity for v4).
---------
Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr>
Reviewed-on: #260
|
||
|
|
a84ea2d116 |
feat(spa): proxy /api in dev-server, relative bffApiBaseUrl (#259)
## Summary Make the SPAs reach the BFF as a **same-origin** call via an Angular dev-server `/api` proxy. Solves the "Backend unreachable" error surfaced during the ADR-0030 dockerised dev mode VM validation when the SPA is accessed from a remote browser (e.g. `http://<vm-ip>:4200/`), and bypasses CORS in dev altogether. Follow-up to the just-merged ADR-0030 implementation. ## Root cause it fixes Before this PR, both SPAs hardcoded `bffApiBaseUrl: 'http://localhost:3000/api'` (per ADR-0018) and the BFF's `CORS_ALLOWED_ORIGINS` only allowed `http://localhost:4200,http://localhost:4300`. Both assumptions hold for native `nx serve` (developer on the same machine as the BFF) but break the moment the browser sits on a different host than the BFF — exactly the case for the ADR-0030 `apps` Compose profile: open `http://<vm-ip>:4200/` from your workstation and the SPA's `localhost:3000` call hits your **workstation's** loopback (nothing there), not the VM's BFF. Even if the URL were right, the origin `http://<vm-ip>:4200` is not in the CORS allowlist. ## Fix Switch to a same-origin dev pattern: the Angular dev-server proxies `/api/*` to the BFF, and `environment.ts` uses a relative `'/api'` URL. | File | Change | | --- | --- | | `apps/portal-shell/proxy.conf.js` | **New.** Maps `/api → ${BFF_TARGET:-http://localhost:3000}` (JS form so the env var can swap the target at startup). | | `apps/portal-admin/proxy.conf.js` | **New.** Same shape. | | `apps/portal-shell/project.json`, `apps/portal-admin/project.json` | `serve.options.proxyConfig` points at the new file. | | `apps/portal-shell/src/environments/environment.ts`, `apps/portal-admin/src/environments/environment.ts` | `bffApiBaseUrl: 'http://localhost:3000/api'` → `'/api'`. The comment block explains the rationale + how production siblings can still use an absolute origin if SPA + BFF live on different hosts. | | `apps/portal-shell/src/observability/tracing.ts` | `new URL(environment.bffApiBaseUrl)` would throw on a relative URL — resolved against `window.location.origin` so both relative (dev) and absolute (prod cross-origin) bases work. | | `infra/local/dev.compose.yml` | `BFF_TARGET=http://portal-bff:3000` added to `portal-shell` and `portal-admin` so the proxy hits the BFF **container** by name (Compose DNS) when the `apps` profile is up. Native `nx serve` leaves the var unset and falls back to `localhost:3000`. | ## Why a relative URL is safe - `${bffApiBaseUrl}/health` etc. compose to `/api/health` — relative paths work in `fetch` / `HttpClient`. - `tracing.ts` propagates `traceparent` on requests whose origin matches the BFF origin. Resolving the relative base against `window.location.origin` gives the current page's origin, which is exactly the origin the dev-server proxy serves from — so the regex still matches the right requests in dev. In a future cross-origin production deployment, `environment.prod.ts` can set an absolute `bffApiBaseUrl`; the URL constructor's second arg is ignored when the first is absolute, so the same code path keeps working. - Auth flow (`feature-auth` / `auth.config.ts`): consumes `bffApiBaseUrl` via DI as a string prefix — agnostic to absolute vs relative. ## Scope notes - The OTel HTTP exporter (`environment.otlpEndpoint = 'http://localhost:4318/v1/traces'`) and the cross-SPA links (`adminAppUrl`, `shellAppUrl`) **remain absolute**. They hit the same remote-browser problem on `apps`-profile access, but neither is blocking the user-visible "Backend unreachable" path this PR targets. Out of scope here; a follow-up could either proxy them too or surface them via runtime config. - This pattern is dev-server only — production builds do not use the proxy. Per-environment `bffApiBaseUrl` overrides remain the supported lever (ADR-0018), unchanged. ## Test plan - [x] `docker compose -f dev.compose.yml --profile apps config` still validates. - [ ] **On the VM**, `./infra/local/dev.sh up apps`, then in the workstation browser open `http://<vm-ip>:4200/` → SPA loads, the "Backend unreachable" message is gone, network tab shows `/api/...` calls succeeding (same-origin, no CORS preflight). - [ ] Native `nx serve portal-shell` still works (the proxy falls back to `localhost:3000`). - [ ] Trace headers (`traceparent`) appear on `/api/*` fetches in the browser network tab. - [ ] `pnpm exec nx affected -t test build` green on the two SPAs. ## Related - [ADR-0030](docs/decisions/0030-dockerised-dev-mode.md) — the dockerised dev mode this enables to actually work from a remote browser. - [ADR-0018](docs/decisions/0018-environment-configuration-strategy.md) — the `environment.ts` per-env strategy this complements (does not supersede — production behaviour unchanged). --------- Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr> Reviewed-on: #259 |
||
|
|
c080d1ad89 |
feat(infra): dockerised full-stack dev mode — apps compose profile (ADR-0030) (#258)
## Summary
Implements [ADR-0030](../docs/decisions/0030-dockerised-dev-mode.md) (now `accepted`): a Docker Compose `apps` profile that runs the three Nx dev servers (`portal-bff`, `portal-shell`, `portal-admin`) from a shared `Dockerfile.dev`, so a developer can boot the whole stack with **no native Node/pnpm**:
```bash
./infra/local/dev.sh up apps # infra + portal-bff:3000 + portal-shell:4200 + portal-admin:4300
```
Purely additive and profile-gated — the native `nx serve` flow and the devcontainer are untouched. Dev-only; no production images (those stay with the ADR-0028 Container Registry work).
## What lands
| File | Change |
| --- | --- |
| `docs/decisions/0030-dockerised-dev-mode.md` | Status `proposed` → `accepted`. |
| `docs/decisions/README.md` | Index status → `accepted`. |
| `infra/local/Dockerfile.dev` | **New.** `node:24-bookworm` + corepack (pnpm resolved from `packageManager` at runtime — no pinned version to drift). No COPY/install at build time. `NX_DAEMON=false`, `NODE_OPTIONS=--max-old-space-size=4096`. |
| `infra/local/dev-entrypoint.sh` | **New.** Shared entrypoint: BFF (`APF_ROLE=bff`) runs `prisma generate` + `prisma migrate deploy` then serves; SPA services go straight to `nx serve`. |
| `infra/local/dev.compose.yml` | **New `apps` profile.** A one-shot `apps-deps` service installs into a shared `node_modules` volume once (the 3 servers gate on its `service_completed_successfully`, avoiding a 3-way install race); `portal-bff` / `portal-shell` / `portal-admin` services from the shared image via a `x-app-base` anchor. Repo bind-mounted; `node_modules` + `.nx` in named volumes. |
| `infra/local/dev.sh` | `apps` added to `ALL_PROFILES` (so teardown / status / logs catch it) + usage / examples. |
| `infra/README.md` | New "Dockerised app dev mode" section + cheat-sheet / file-table rows. |
| `docs/setup/01-dev-debian-vm-setup.md` | "Three dev modes — which when" table at the top of Step 5. |
| `CLAUDE.md` | Architecture roll-up bullet + ADR-count line + environment-conventions note. |
## Key design decisions
- **One image, one install.** The monorepo means a single `Dockerfile.dev` + a single `pnpm install` serves all three apps.
- **`node_modules` + `.nx` in named volumes, not bind-mounted.** The container's install (native modules — `esbuild`, `@swc/core`, Prisma engines, `lmdb`, `@parcel/watcher` — built for this image) must never be shadowed by the host's `node_modules`. The repo source is bind-mounted for hot reload; these two directories are overlaid with named volumes.
- **`apps-deps` one-shot avoids the install race.** Three services sharing one `node_modules` volume can't all run `pnpm install` concurrently. A dedicated install service runs first; the three app services `depends_on` its completion.
- **`NX_DAEMON=false`** in the containers — three containers sharing one workspace would otherwise contend on the Nx daemon.
- **Env wiring.** The BFF reuses its own `apps/portal-bff/.env` (Entra / session / jwks secrets) via `env_file: { required: false }`; the host-specific URLs (`DATABASE_URL` / `REDIS_URL` / OTel endpoint) are overridden in `environment:` — rebuilt from `infra/local/.env` creds → Compose service names. Compose `environment` wins over `env_file`, so the localhost values in the BFF `.env` don't leak into the container.
- **BFF still needs its secrets.** "No native toolchain" ≠ "no config". `apps/portal-bff/.env` must exist (same as native dev); `required: false` lets SPA-only devs `up` without it (the BFF then fails its own boot validators with a clear message).
## Validation on the VM
- [x] `docker compose -f dev.compose.yml --profile apps config` validates (YAML, anchors / merge, env interpolation).
- [x] `bash -n` clean on `dev-entrypoint.sh` and `dev.sh`.
- [x] **Full boot on vm-dev** — `./infra/local/dev.sh up apps` brings up postgres / redis / otel + `apps-deps` (one-shot, exit 0) + portal-bff / portal-shell / portal-admin, all containers report healthy or running.
- [x] `apps-deps` populates the shared `node_modules` volume; the three servers reach their `nx serve` step without re-installing.
- [x] Ports published as expected: BFF :3000, portal-shell :4200, portal-admin :4300.
- [x] `./infra/local/dev.sh up` (no `apps`) unchanged for native devs.
## Follow-ups identified during VM validation
- **SPA → BFF reachability from a remote browser.** Opening `http://<vm-ip>:4200/` from the workstation surfaces a "Backend unreachable" message: the SPA's hardcoded `bffApiBaseUrl: 'http://localhost:3000/api'` (ADR-0018 build-time env) plus the BFF's `CORS_ALLOWED_ORIGINS=http://localhost:4200,…` both assume "browser on the same machine as the BFF", which doesn't hold here. Fixed in the **stacked follow-up PR `feat/spa-dev-proxy`** (proxy `/api` in the Angular dev-server + relative `bffApiBaseUrl`), which lands right after this PR.
- The OTel HTTP exporter URL (`environment.otlpEndpoint`) and the cross-SPA links (`adminAppUrl`, `shellAppUrl`) remain absolute and hit the same remote-browser limit; not blocking for v1, can be revisited if needed.
## Related
- [ADR-0030](docs/decisions/0030-dockerised-dev-mode.md) — the decision (accepted in this PR's chain).
- [ADR-0020](docs/decisions/0020-portal-admin-app.md) — the devcontainer this complements.
- [ADR-0028](docs/decisions/0028-migrate-cicd-and-git-hosting-to-gitlab.md) — production images / Container Registry (deferred).
- Follow-up branch `feat/spa-dev-proxy` — the SPA-side proxy fix that makes the dockerised mode usable from a remote browser.
---------
Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr>
Reviewed-on: #258
|
||
|
|
8a2a99c351 |
docs(decisions): add ADR-0030 dockerised full-stack dev mode (proposed) (#256)
## Summary Propose **ADR-0030 — Dockerised full-stack dev mode**: an `apps` Compose profile that runs the three Nx apps (`portal-bff`, `portal-shell`, `portal-admin`) from a shared `Dockerfile.dev`, so a developer can `docker compose up` the whole stack with **no native Node/pnpm** on the host. `proposed` status — this PR is the **decision record only**. The `Dockerfile.dev` + the `apps` profile + the BFF entrypoint land in a follow-up PR once the ADR is accepted. ## Why Two frictions motivate it: 1. Infra is already in Docker (`infra/local/dev.compose.yml`), but the Nx apps run natively — every dev pays the nvm/corepack/pnpm setup cost. The recent fresh-VM `.zshrc` nvm gap is a concrete example of that path breaking. 2. A developer asked to run all servers with a single `docker compose up`, no toolchain, no IDE attach. ## Decision (chosen option) An **`apps` Compose profile** backed by **one shared `Dockerfile.dev`** (node:24 + pinned pnpm), because it is purely additive: - `./infra/local/dev.sh up` stays infra-only (native + devcontainer flows unchanged). - `./infra/local/dev.sh up apps` brings up infra + the three dev servers with hot reload. Key design points captured in the ADR (built in the follow-up): one image / one install for the monorepo; repo bind-mounted but `node_modules` + Nx cache in **named volumes** (native-module arch correctness); `depends_on … service_healthy`; BFF entrypoint does `prisma generate` + `migrate deploy`; services point at the existing `apf-portal-dev` Compose network. The ADR documents a "which mode when" table so the **three** dev modes (native / devcontainer / compose-`apps`) coexist without confusion. ## Scope guardrails - **Dev-only.** No production images here — those are tracked against the ADR-0028 Container Registry follow-up (post-cutover). The ADR is explicit that it produces no deployment artefact. - Complements ADR-0020's devcontainer (interactive/IDE) with a non-interactive services sibling; does not replace it. ## Numbering Takes **0030**, not 0029. `0029` is reserved for the cascade/Pléiades/Acteurs+ syncs ADR (referenced by ADR-0026/0027/0028); numbers are never reused, so the dev-mode ADR takes the next free slot. The index gap at 0029 is intentional until that ADR is written. ## What lands | File | Change | | --- | --- | | `docs/decisions/0030-dockerised-dev-mode.md` | New ADR, `proposed`. | | `docs/decisions/README.md` | Index row for 0030. | ## Test plan - [x] MADR 4.0.0 frontmatter; tags drawn from the README vocabulary (`infrastructure`, `process`). - [x] Index updated in the same commit (per the repo's index-maintenance rule). - [ ] R&D Lead review → accept / revise. On acceptance: update the CLAUDE.md architecture roll-up + add the "which mode when" guidance to `docs/setup/`, then open the implementation PR. ## Related - [ADR-0020](docs/decisions/0020-portal-admin-app.md) — VSCode devcontainer (the interactive no-toolchain path this complements). - [ADR-0028](docs/decisions/0028-migrate-cicd-and-git-hosting-to-gitlab.md) — Container Registry / production images (the deferred prod-image work). - [ADR-0006](docs/decisions/0006-persistence-postgresql-prisma.md) — Prisma; the entrypoint applies (never authors) migrations. --------- Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr> Reviewed-on: #256 |
||
|
|
645f81a443 |
fix(setup): mirror nvm init into .zshrc so zsh sees node + pnpm (#255)
## Summary Fix the dev-VM bootstrap so `node` and `pnpm` are on `PATH` in zsh sessions. On a fresh VM provisioned with `docs/setup/scripts/`, a freshly-cloned checkout fails to run anything (`command not found: pnpm`, and `node` too) because the nvm init never reaches the interactive shell. ## Root cause Two setup scripts interact badly: - `20-zsh.sh` patches `~/.bashrc` with an `exec zsh -l` hand-off on interactive shells (chsh is blocked on the corp VM, so this is how zsh becomes the effective shell), and patches `~/.zshrc` with theme/plugins/fzf only. - `40-node.sh` runs the upstream nvm installer, which appends its init block to **`~/.bashrc`** (its default target). Because `20-zsh.sh` runs first, the `exec zsh -l` guard sits **above** the nvm block in `~/.bashrc`. On every interactive shell, bash execs into zsh before reaching the nvm block — so it never runs — and `~/.zshrc` has no nvm init at all. Net result: zsh sessions have neither `node` nor `pnpm` on `PATH`, even though nvm + Node + corepack installed correctly. This is a procedure bug, not a one-off: every VM built from these scripts hits it. ## Fix `40-node.sh` now mirrors the nvm init block into `~/.zshrc` after enabling corepack — idempotent via a managed marker, same pattern `20-zsh.sh` already uses for its `~/.bashrc` patch: ```sh # Managed by docs/setup/scripts/40-node.sh — nvm init for zsh. export NVM_DIR="$HOME/.nvm" [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" [ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion" ``` `docs/setup/01-dev-debian-vm-setup.md` — the `40-node.sh` row in the bootstrap table now notes the `~/.zshrc` patch and why it is needed. ## What lands | File | Change | | --- | --- | | `docs/setup/scripts/40-node.sh` | Append an idempotent nvm-init block to `~/.zshrc` after corepack enable. | | `docs/setup/01-dev-debian-vm-setup.md` | Document the `~/.zshrc` nvm patch in the `40-node.sh` table row. | ## Manual remediation for already-provisioned VMs VMs built before this fix won't be retroactively patched (the scripts are idempotent and skip already-installed nvm). On those, run once: ```sh cat >> ~/.zshrc <<'EOF' # nvm export NVM_DIR="$HOME/.nvm" [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" [ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion" EOF exec zsh -l ``` (Or re-run `40-node.sh` once this lands — the marker check makes it safe; it will add the block and skip the rest.) ## Test plan - [x] `bash -n docs/setup/scripts/40-node.sh` — syntax valid. - [x] Manual remediation block verified on the affected VM: after appending + `exec zsh -l`, `node --version` (v24) and `pnpm --version` (10.34.1) both resolve. - [ ] Next fresh VM bootstrap: `node` + `pnpm` resolve in the first zsh session with no manual step. - [ ] Re-running `40-node.sh` on an already-set-up VM is a no-op on the nvm install and adds the `~/.zshrc` block exactly once (marker idempotency). ## Related - `docs/setup/01-dev-debian-vm-setup.md` — dev-VM bootstrap procedure. - `20-zsh.sh` — owns the `.bashrc` → zsh hand-off this fix complements. --------- Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr> Reviewed-on: #255 |
||
|
|
741fc07d40 |
fix(deps): update dependency ioredis to v5.11.0 (#254)
This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [ioredis](https://github.com/luin/ioredis) | dependencies | minor | [`5.10.1` -> `5.11.0`](https://renovatebot.com/diffs/npm/ioredis/5.10.1/5.11.0) | --- ### Release Notes <details> <summary>luin/ioredis (ioredis)</summary> ### [`v5.11.0`](https://github.com/luin/ioredis/blob/HEAD/CHANGELOG.md#5110-2026-05-26) [Compare Source](https://github.com/luin/ioredis/compare/v5.10.1...v5.11.0) ##### Bug Fixes - prevent RangeError from string accumulation in pipeline ([#​2088](https://github.com/luin/ioredis/issues/2088)) ([defc077](https://github.com/luin/ioredis/commit/defc07716a9ef10c2077ec4e23ea48cb9ea731fc)) - replace deprecated url.parse() with WHATWG URL API ([#​2081](https://github.com/luin/ioredis/issues/2081)) ([0021a45](https://github.com/luin/ioredis/commit/0021a4590e286aabbf27f4e2fc18f9d2de829ef0)), closes [redis/ioredis#1747](https://github.com/redis/ioredis/issues/1747) ##### Features - add array commands, typings and tests ([#​2114](https://github.com/luin/ioredis/issues/2114)) ([baf68d6](https://github.com/luin/ioredis/commit/baf68d6d89553672cfac3e08543467b910b561c5)) - add increx command ([#​2115](https://github.com/luin/ioredis/issues/2115)) ([37d0695](https://github.com/luin/ioredis/commit/37d0695b212d865ef24132acff85420ae51dde50)) - add Redis MSETEX support ([#​2111](https://github.com/luin/ioredis/issues/2111)) ([04a4615](https://github.com/luin/ioredis/commit/04a4615e8e96b9c58d017e360b5eaafede8973d0)) - add typed GCRA command support and functional tests ([#​2094](https://github.com/luin/ioredis/issues/2094)) ([468a802](https://github.com/luin/ioredis/commit/468a8023cd2c8f342ec7c55a01bf0c8d17e4b877)) - add vector set command support ([#​2116](https://github.com/luin/ioredis/issues/2116)) ([b7b3def](https://github.com/luin/ioredis/commit/b7b3defbd119d07fb86d071d5eefc255db4920c2)) - Add xnack command ([#​2103](https://github.com/luin/ioredis/issues/2103)) ([187d29b](https://github.com/luin/ioredis/commit/187d29b45000ee46a4baa8ce91eacfa04675aee8)) - Add zinter zunion count ([#​2104](https://github.com/luin/ioredis/issues/2104)) ([0d510bb](https://github.com/luin/ioredis/commit/0d510bbc1cfc8b01d862b76c408f6687f6e77809)) - Implement `TracingChannel` support ([#​2089](https://github.com/luin/ioredis/issues/2089)) ([4760e0a](https://github.com/luin/ioredis/commit/4760e0a19c194f29f4feb703003dcf046e4509cd)) #### [5.10.1](https://github.com/luin/ioredis/compare/v5.10.0...v5.10.1) (2026-03-19) ##### Bug Fixes - **cluster:** lazily start sharded subscribers ([#​2090](https://github.com/luin/ioredis/issues/2090)) ([4f167bb](https://github.com/luin/ioredis/commit/4f167bb9f494f0e8200a20dedd8bbdf1810fcd22)) </details> --- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box --- This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate). <!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0MC42Mi4xIiwidXBkYXRlZEluVmVyIjoiNDAuNjIuMSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOlsiZGVwZW5kZW5jaWVzIl19--> Reviewed-on: https://git.unespace.com/julien/apf_portal/pulls/254 Co-authored-by: APF Portal Bot <jgautier.webdev+apf-portal-bot@gmail.com> Co-committed-by: APF Portal Bot <jgautier.webdev+apf-portal-bot@gmail.com> |
||
|
|
3ac408aca9 |
fix(deps): update dependency helmet to v8.2.0 (#253)
This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [helmet](https://helmet.js.org/) ([source](https://github.com/helmetjs/helmet)) | dependencies | minor | [`8.1.0` -> `8.2.0`](https://renovatebot.com/diffs/npm/helmet/8.1.0/8.2.0) | --- ### Release Notes <details> <summary>helmetjs/helmet (helmet)</summary> ### [`v8.2.0`](https://github.com/helmetjs/helmet/blob/HEAD/CHANGELOG.md#820---2026-05-21) [Compare Source](https://github.com/helmetjs/helmet/compare/v8.1.0...v8.2.0) - `Cross-Origin-Opener-Policy`: support `noopener-allow-popups`. See [#​522](https://github.com/helmetjs/helmet/pull/522) - Improve error message when passing duplicate options </details> --- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box --- This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate). <!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0MC42Mi4xIiwidXBkYXRlZEluVmVyIjoiNDAuNjIuMSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOlsiZGVwZW5kZW5jaWVzIl19--> Reviewed-on: #253 Co-authored-by: APF Portal Bot <jgautier.webdev+apf-portal-bot@gmail.com> Co-committed-by: APF Portal Bot <jgautier.webdev+apf-portal-bot@gmail.com> |
||
|
|
377524ce3f |
chore(deps): update typescript tooling to v8.60.0 (#252)
This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [@typescript-eslint/utils](https://typescript-eslint.io/packages/utils) ([source](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/utils)) | devDependencies | minor | [`8.59.4` -> `8.60.0`](https://renovatebot.com/diffs/npm/@typescript-eslint%2futils/8.59.4/8.60.0) | | [typescript-eslint](https://typescript-eslint.io/packages/typescript-eslint) ([source](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint)) | devDependencies | minor | [`8.59.4` -> `8.60.0`](https://renovatebot.com/diffs/npm/typescript-eslint/8.59.4/8.60.0) | --- ### Release Notes <details> <summary>typescript-eslint/typescript-eslint (@​typescript-eslint/utils)</summary> ### [`v8.60.0`](https://github.com/typescript-eslint/typescript-eslint/blob/HEAD/packages/utils/CHANGELOG.md#8600-2026-05-25) [Compare Source](https://github.com/typescript-eslint/typescript-eslint/compare/v8.59.4...v8.60.0) This was a version bump only for utils to align it with other projects, there were no code changes. See [GitHub Releases](https://github.com/typescript-eslint/typescript-eslint/releases/tag/v8.60.0) for more information. You can read about our [versioning strategy](https://typescript-eslint.io/users/versioning) and [releases](https://typescript-eslint.io/users/releases) on our website. </details> <details> <summary>typescript-eslint/typescript-eslint (typescript-eslint)</summary> ### [`v8.60.0`](https://github.com/typescript-eslint/typescript-eslint/blob/HEAD/packages/typescript-eslint/CHANGELOG.md#8600-2026-05-25) [Compare Source](https://github.com/typescript-eslint/typescript-eslint/compare/v8.59.4...v8.60.0) This was a version bump only for typescript-eslint to align it with other projects, there were no code changes. See [GitHub Releases](https://github.com/typescript-eslint/typescript-eslint/releases/tag/v8.60.0) for more information. You can read about our [versioning strategy](https://typescript-eslint.io/users/versioning) and [releases](https://typescript-eslint.io/users/releases) on our website. </details> --- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about these updates again. --- - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box --- This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate). <!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0MC42Mi4xIiwidXBkYXRlZEluVmVyIjoiNDAuNjIuMSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOlsiZGVwZW5kZW5jaWVzIl19--> Reviewed-on: #252 Co-authored-by: APF Portal Bot <jgautier.webdev+apf-portal-bot@gmail.com> Co-committed-by: APF Portal Bot <jgautier.webdev+apf-portal-bot@gmail.com> |
||
|
|
0df3ab9f03 |
chore(deps): update pnpm to v10.34.1 (#251)
This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [pnpm](https://pnpm.io) ([source](https://github.com/pnpm/pnpm/tree/HEAD/pnpm)) | packageManager | minor | [`10.33.4` -> `10.34.1`](https://renovatebot.com/diffs/npm/pnpm/10.33.4/10.34.1) | --- ### Release Notes <details> <summary>pnpm/pnpm (pnpm)</summary> ### [`v10.34.1`](https://github.com/pnpm/pnpm/releases/tag/v10.34.1): pnpm 10.34.1 [Compare Source](https://github.com/pnpm/pnpm/compare/v10.34.0...v10.34.1) #### Patch Changes - Reject `pnpm-lock.yaml` entries whose remote tarball `resolution:` block is missing the `integrity` field. Previously the worker that extracts a downloaded tarball skipped hash verification when no integrity was supplied and minted a fresh one from the unverified bytes, so an attacker who could both alter the lockfile (e.g. via a pull request that strips `integrity:`) and serve modified content at the referenced tarball URL could install a tampered package without any error — including under `--frozen-lockfile`. pnpm now fails closed at lockfile-read time with `ERR_PNPM_MISSING_TARBALL_INTEGRITY`. Git-hosted tarballs (`gitHosted: true` or a URL on codeload.github.com / bitbucket.org / gitlab.com) and `file:` tarballs are exempt — the commit SHA in a git-host URL and the user-controlled local path already anchor the bytes. <!-- sponsors --> #### Platinum Sponsors <table> <tbody> <tr> <td align="center" valign="middle"> <a href="https://bit.cloud/?utm_source=pnpm&utm_medium=release_notes" target="_blank"><img src="https://pnpm.io/img/users/bit.svg" width="80" alt="Bit"></a> </td> </tr> </tbody> </table> #### Gold Sponsors <table> <tbody> <tr> <td align="center" valign="middle"> <a href="https://sanity.io/?utm_source=pnpm&utm_medium=release_notes" target="_blank"> <picture> <source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/sanity.svg" /> <source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/sanity_light.svg" /> <img src="https://pnpm.io/img/users/sanity.svg" width="120" alt="Sanity" /> </picture> </a> </td> <td align="center" valign="middle"> <a href="https://discord.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank"> <picture> <source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/discord.svg" /> <source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/discord_light.svg" /> <img src="https://pnpm.io/img/users/discord.svg" width="220" alt="Discord" /> </picture> </a> </td> <td align="center" valign="middle"> <a href="https://vite.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank"><img src="https://pnpm.io/img/users/vitejs.svg" width="42" alt="Vite"></a> </td> </tr> <tr> <td align="center" valign="middle"> <a href="https://serpapi.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank"> <picture> <source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/serpapi_dark.svg" /> <source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/serpapi_light.svg" /> <img src="https://pnpm.io/img/users/serpapi_dark.svg" width="160" alt="SerpApi" /> </picture> </a> </td> <td align="center" valign="middle"> <a href="https://coderabbit.ai/?utm_source=pnpm&utm_medium=release_notes" target="_blank"> <picture> <source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/coderabbit.svg" /> <source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/coderabbit_light.svg" /> <img src="https://pnpm.io/img/users/coderabbit.svg" width="220" alt="CodeRabbit" /> </picture> </a> </td> <td align="center" valign="middle"> <a href="https://stackblitz.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank"> <picture> <source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/stackblitz.svg" /> <source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/stackblitz_light.svg" /> <img src="https://pnpm.io/img/users/stackblitz.svg" width="190" alt="Stackblitz" /> </picture> </a> </td> </tr> <tr> <td align="center" valign="middle"> <a href="https://workleap.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank"> <picture> <source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/workleap.svg" /> <source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/workleap_light.svg" /> <img src="https://pnpm.io/img/users/workleap.svg" width="190" alt="Workleap" /> </picture> </a> </td> <td align="center" valign="middle"> <a href="https://nx.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank"> <picture> <source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/nx.svg" /> <source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/nx_light.svg" /> <img src="https://pnpm.io/img/users/nx.svg" width="50" alt="Nx" /> </picture> </a> </td> </tr> </tbody> </table> <!-- sponsors end --> ### [`v10.34.0`](https://github.com/pnpm/pnpm/releases/tag/v10.34.0): pnpm 10.34 [Compare Source](https://github.com/pnpm/pnpm/compare/v10.33.4...v10.34.0) #### Minor Changes - Treat tarball-integrity mismatches against the lockfile as a hard failure by default. Previously, `pnpm install` (non-frozen) would log `ERR_PNPM_TARBALL_INTEGRITY`, silently re-resolve from the registry, and overwrite the locked integrity — which meant a compromised registry, proxy, or republished version could substitute attacker-controlled content on a clean machine even though the project shipped a committed lockfile. `pnpm install` now exits with `ERR_PNPM_TARBALL_INTEGRITY` and a hint pointing at the new opt-in flag. The only opt-in is **`pnpm install --update-checksums`** — narrowly scoped to refreshing the locked integrity values from what the registry currently serves. Mirrors yarn's flag of the same name. A warning still prints when the bypass takes effect so the operation is auditable. `--force` and `pnpm update` deliberately do **not** bypass the integrity check. They are routine refresh operations; silently overwriting a locked integrity in those flows would erase the protection a committed lockfile is supposed to provide. `--frozen-lockfile` behavior is unchanged. `--fix-lockfile` keeps its documented purpose (filling in missing lockfile entries) and is also not a bypass. #### Patch Changes - Pin unscoped per-registry settings (`_authToken`, `_auth`, `username`/`_password`, `tokenHelper`, inline `cert`/`key`) to the registry declared in the same config source at load time, so a later layer overriding `registry=` (workspace `.npmrc`, `pnpm-workspace.yaml`, CLI `--registry`) cannot redirect a credential or client certificate authored for a different host. A deprecation warning is emitted whenever an unscoped per-registry setting is encountered, naming the source and the URL it was pinned to. Reported by JUNYI LIU. - Fixed `minimumReleaseAge` handling when cached metadata is abbreviated. The npm registry returns abbreviated package metadata (without the per-version `time` field) by default, which made the maturity check throw `ERR_PNPM_MISSING_TIME` whenever cached abbreviated metadata was reused. pnpm now upgrades cached abbreviated metadata to the full document via a follow-up fetch when `minimumReleaseAge` is active, persists the upgrade to the on-disk cache so subsequent installs skip the extra fetch, and lets `ERR_PNPM_MISSING_TIME` from the cache fast-path fall through to the network fetch even under strict mode. - Reject git resolutions whose `commit` field is not a 40-character hexadecimal SHA before invoking `git`. A malicious lockfile could otherwise smuggle a value such as `--upload-pack=<command>` through `git fetch` / `git checkout`, which on SSH or local-file transports executes the supplied command. - Reject patch files whose `diff --git` headers reference paths outside the patched package directory. Previously a malicious `.patch` file added via a pull request could write, delete, or rename arbitrary files reachable by the user running `pnpm install`. - Fixed `--prefix=<dir>` not being honored when locating the workspace root. The `--prefix → dir` rename was applied after workspace detection, so workspace settings declared in `<dir>/pnpm-workspace.yaml` were not loaded when pnpm was invoked from outside `<dir>` [#​11535](https://github.com/pnpm/pnpm/issues/11535). - Reject dependency aliases that contain path-traversal segments (such as `@x/../../../../../.git/hooks`) when reading them from a package manifest or symlinking them into `node_modules`. A malicious registry package could otherwise use a transitive dependency key to make `pnpm install` create symlinks at attacker-chosen paths outside the intended `node_modules` directory. <!-- sponsors --> #### Platinum Sponsors <table> <tbody> <tr> <td align="center" valign="middle"> <a href="https://bit.cloud/?utm_source=pnpm&utm_medium=release_notes" target="_blank"><img src="https://pnpm.io/img/users/bit.svg" width="80" alt="Bit"></a> </td> </tr> </tbody> </table> #### Gold Sponsors <table> <tbody> <tr> <td align="center" valign="middle"> <a href="https://sanity.io/?utm_source=pnpm&utm_medium=release_notes" target="_blank"> <picture> <source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/sanity.svg" /> <source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/sanity_light.svg" /> <img src="https://pnpm.io/img/users/sanity.svg" width="120" alt="Sanity" /> </picture> </a> </td> <td align="center" valign="middle"> <a href="https://discord.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank"> <picture> <source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/discord.svg" /> <source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/discord_light.svg" /> <img src="https://pnpm.io/img/users/discord.svg" width="220" alt="Discord" /> </picture> </a> </td> <td align="center" valign="middle"> <a href="https://vite.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank"><img src="https://pnpm.io/img/users/vitejs.svg" width="42" alt="Vite"></a> </td> </tr> <tr> <td align="center" valign="middle"> <a href="https://serpapi.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank"> <picture> <source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/serpapi_dark.svg" /> <source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/serpapi_light.svg" /> <img src="https://pnpm.io/img/users/serpapi_dark.svg" width="160" alt="SerpApi" /> </picture> </a> </td> <td align="center" valign="middle"> <a href="https://coderabbit.ai/?utm_source=pnpm&utm_medium=release_notes" target="_blank"> <picture> <source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/coderabbit.svg" /> <source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/coderabbit_light.svg" /> <img src="https://pnpm.io/img/users/coderabbit.svg" width="220" alt="CodeRabbit" /> </picture> </a> </td> <td align="center" valign="middle"> <a href="https://stackblitz.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank"> <picture> <source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/stackblitz.svg" /> <source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/stackblitz_light.svg" /> <img src="https://pnpm.io/img/users/stackblitz.svg" width="190" alt="Stackblitz" /> </picture> </a> </td> </tr> <tr> <td align="center" valign="middle"> <a href="https://workleap.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank"> <picture> <source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/workleap.svg" /> <source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/workleap_light.svg" /> <img src="https://pnpm.io/img/users/workleap.svg" width="190" alt="Workleap" /> </picture> </a> </td> <td align="center" valign="middle"> <a href="https://nx.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank"> <picture> <source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/nx.svg" /> <source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/nx_light.svg" /> <img src="https://pnpm.io/img/users/nx.svg" width="50" alt="Nx" /> </picture> </a> </td> </tr> </tbody> </table> <!-- sponsors end --> </details> --- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box --- This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate). <!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0MC42Mi4xIiwidXBkYXRlZEluVmVyIjoiNDAuNjIuMSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOlsiZGVwZW5kZW5jaWVzIl19--> Reviewed-on: https://git.unespace.com/julien/apf_portal/pulls/251 Co-authored-by: APF Portal Bot <jgautier.webdev+apf-portal-bot@gmail.com> Co-committed-by: APF Portal Bot <jgautier.webdev+apf-portal-bot@gmail.com> |
||
|
|
afbad752fb |
chore(deps): update dependency @oxc-project/runtime to ^0.133.0 (#250)
This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [@oxc-project/runtime](https://oxc.rs) ([source](https://github.com/oxc-project/oxc/tree/HEAD/npm/runtime)) | devDependencies | minor | [`^0.131.0` -> `^0.133.0`](https://renovatebot.com/diffs/npm/@oxc-project%2fruntime/0.131.0/0.133.0) | --- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box --- This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate). <!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0MC42Mi4xIiwidXBkYXRlZEluVmVyIjoiNDAuNjIuMSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOlsiZGVwZW5kZW5jaWVzIl19--> Reviewed-on: #250 Co-authored-by: APF Portal Bot <jgautier.webdev+apf-portal-bot@gmail.com> Co-committed-by: APF Portal Bot <jgautier.webdev+apf-portal-bot@gmail.com> |
||
|
|
33d2257cf5 |
fix(deps): update vitest to v4.1.7 (#249)
This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [@vitest/coverage-v8](https://vitest.dev/guide/coverage) ([source](https://github.com/vitest-dev/vitest/tree/HEAD/packages/coverage-v8)) | devDependencies | patch | [`4.1.6` -> `4.1.7`](https://renovatebot.com/diffs/npm/@vitest%2fcoverage-v8/4.1.6/4.1.7) | | [@vitest/ui](https://vitest.dev/guide/ui) ([source](https://github.com/vitest-dev/vitest/tree/HEAD/packages/ui)) | devDependencies | patch | [`4.1.6` -> `4.1.7`](https://renovatebot.com/diffs/npm/@vitest%2fui/4.1.6/4.1.7) | | [vitest](https://vitest.dev) ([source](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest)) | devDependencies | patch | [`4.1.6` -> `4.1.7`](https://renovatebot.com/diffs/npm/vitest/4.1.6/4.1.7) | | [vitest](https://vitest.dev) ([source](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest)) | dependencies | patch | [`4.1.6` -> `4.1.7`](https://renovatebot.com/diffs/npm/vitest/4.1.6/4.1.7) | --- ### Release Notes <details> <summary>vitest-dev/vitest (@​vitest/coverage-v8)</summary> ### [`v4.1.7`](https://github.com/vitest-dev/vitest/releases/tag/v4.1.7) [Compare Source](https://github.com/vitest-dev/vitest/compare/v4.1.6...v4.1.7) ##### 🐞 Bug Fixes - **runner**: Limit concurrency per task branch in addition to per leaf callbacks (backport) - by [@​hi-ogawa](https://github.com/hi-ogawa) in https://github.com/vitest-dev/vitest/issues/10384 [<samp>(4f0f2)</samp>](https://github.com/vitest-dev/vitest/commit/4f0f2a1ee) ##### [View changes on GitHub](https://github.com/vitest-dev/vitest/compare/v4.1.6...v4.1.7) </details> --- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Enabled. ♻ **Rebasing**: Whenever PR is behind base branch, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about these updates again. --- - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box --- This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate). <!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0MC42Mi4xIiwidXBkYXRlZEluVmVyIjoiNDAuNjIuMSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOlsiZGVwZW5kZW5jaWVzIl19--> Reviewed-on: https://git.unespace.com/julien/apf_portal/pulls/249 Co-authored-by: APF Portal Bot <jgautier.webdev+apf-portal-bot@gmail.com> Co-committed-by: APF Portal Bot <jgautier.webdev+apf-portal-bot@gmail.com> |
||
|
|
17bed9de09 |
fix(deps): update nx to v22.7.5 (#248)
This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [@nx/angular](https://nx.dev) ([source](https://github.com/nrwl/nx/tree/HEAD/packages/angular)) | devDependencies | patch | [`22.7.2` -> `22.7.5`](https://renovatebot.com/diffs/npm/@nx%2fangular/22.7.2/22.7.5) | | [@nx/devkit](https://nx.dev) ([source](https://github.com/nrwl/nx/tree/HEAD/packages/devkit)) | devDependencies | patch | [`22.7.2` -> `22.7.5`](https://renovatebot.com/diffs/npm/@nx%2fdevkit/22.7.2/22.7.5) | | [@nx/eslint](https://nx.dev) ([source](https://github.com/nrwl/nx/tree/HEAD/packages/eslint)) | devDependencies | patch | [`22.7.2` -> `22.7.5`](https://renovatebot.com/diffs/npm/@nx%2feslint/22.7.2/22.7.5) | | [@nx/eslint-plugin](https://nx.dev) ([source](https://github.com/nrwl/nx/tree/HEAD/packages/eslint-plugin)) | devDependencies | patch | [`22.7.2` -> `22.7.5`](https://renovatebot.com/diffs/npm/@nx%2feslint-plugin/22.7.2/22.7.5) | | [@nx/jest](https://nx.dev) ([source](https://github.com/nrwl/nx/tree/HEAD/packages/jest)) | devDependencies | patch | [`22.7.2` -> `22.7.5`](https://renovatebot.com/diffs/npm/@nx%2fjest/22.7.2/22.7.5) | | [@nx/js](https://nx.dev) ([source](https://github.com/nrwl/nx/tree/HEAD/packages/js)) | devDependencies | patch | [`22.7.2` -> `22.7.5`](https://renovatebot.com/diffs/npm/@nx%2fjs/22.7.2/22.7.5) | | [@nx/nest](https://nx.dev) ([source](https://github.com/nrwl/nx/tree/HEAD/packages/nest)) | devDependencies | patch | [`22.7.2` -> `22.7.5`](https://renovatebot.com/diffs/npm/@nx%2fnest/22.7.2/22.7.5) | | [@nx/node](https://nx.dev) ([source](https://github.com/nrwl/nx/tree/HEAD/packages/node)) | devDependencies | patch | [`22.7.2` -> `22.7.5`](https://renovatebot.com/diffs/npm/@nx%2fnode/22.7.2/22.7.5) | | [@nx/playwright](https://nx.dev) ([source](https://github.com/nrwl/nx/tree/HEAD/packages/playwright)) | devDependencies | patch | [`22.7.2` -> `22.7.5`](https://renovatebot.com/diffs/npm/@nx%2fplaywright/22.7.2/22.7.5) | | [@nx/vite](https://nx.dev) ([source](https://github.com/nrwl/nx/tree/HEAD/packages/vite)) | devDependencies | patch | [`22.7.2` -> `22.7.5`](https://renovatebot.com/diffs/npm/@nx%2fvite/22.7.2/22.7.5) | | [@nx/vite](https://nx.dev) ([source](https://github.com/nrwl/nx/tree/HEAD/packages/vite)) | dependencies | patch | [`22.7.2` -> `22.7.5`](https://renovatebot.com/diffs/npm/@nx%2fvite/22.7.2/22.7.5) | | [@nx/vitest](https://nx.dev) ([source](https://github.com/nrwl/nx/tree/HEAD/packages/vitest)) | devDependencies | patch | [`22.7.2` -> `22.7.5`](https://renovatebot.com/diffs/npm/@nx%2fvitest/22.7.2/22.7.5) | | [@nx/web](https://nx.dev) ([source](https://github.com/nrwl/nx/tree/HEAD/packages/web)) | devDependencies | patch | [`22.7.2` -> `22.7.5`](https://renovatebot.com/diffs/npm/@nx%2fweb/22.7.2/22.7.5) | | [@nx/webpack](https://nx.dev) ([source](https://github.com/nrwl/nx/tree/HEAD/packages/webpack)) | devDependencies | patch | [`22.7.2` -> `22.7.5`](https://renovatebot.com/diffs/npm/@nx%2fwebpack/22.7.2/22.7.5) | | [nx](https://nx.dev) ([source](https://github.com/nrwl/nx/tree/HEAD/packages/nx)) | devDependencies | patch | [`22.7.2` -> `22.7.5`](https://renovatebot.com/diffs/npm/nx/22.7.2/22.7.5) | --- ### Release Notes <details> <summary>nrwl/nx (@​nx/angular)</summary> ### [`v22.7.5`](https://github.com/nrwl/nx/releases/tag/22.7.5) [Compare Source](https://github.com/nrwl/nx/compare/22.7.4...22.7.5) ##### 22.7.5 (2026-05-27) ##### 🩹 Fixes - **core:** update tmp to 0.2.6 due to CVE-2026-44705 ([#​35813](https://github.com/nrwl/nx/pull/35813)) ##### ❤️ Thank You - Jack Hsu [@​jaysoo](https://github.com/jaysoo) ### [`v22.7.4`](https://github.com/nrwl/nx/releases/tag/22.7.4) [Compare Source](https://github.com/nrwl/nx/compare/22.7.3...22.7.4) ##### 22.7.4 (2026-05-25) ##### 🩹 Fixes - **core:** update brace-expansion and yaml ([#​35790](https://github.com/nrwl/nx/pull/35790)) ##### ❤️ Thank You - Jack Hsu [@​jaysoo](https://github.com/jaysoo) ### [`v22.7.3`](https://github.com/nrwl/nx/releases/tag/22.7.3) [Compare Source](https://github.com/nrwl/nx/compare/22.7.2...22.7.3) ##### 22.7.3 (2026-05-22) ##### 🚀 Features - **js:** support pnpm 11.2.2 ([#​35772](https://github.com/nrwl/nx/pull/35772)) ##### 🩹 Fixes - **angular:** only add [@​oxc-project/runtime](https://github.com/oxc-project/runtime) on the vitest-analog path ([#​35734](https://github.com/nrwl/nx/pull/35734)) - **angular-rspack:** exclude eslint config from tailwind v4 source scan ([#​35663](https://github.com/nrwl/nx/pull/35663)) - **core:** warn before installing unknown npm packages as preset ([#​35644](https://github.com/nrwl/nx/pull/35644)) - **core:** preserve input order in createNodes plugin results ([#​35595](https://github.com/nrwl/nx/pull/35595)) - **core:** resolve local plugin subpath imports from source ([#​35631](https://github.com/nrwl/nx/pull/35631)) - **core:** treat undefined task parallelism as parallel when scheduling ([#​35736](https://github.com/nrwl/nx/pull/35736)) - **core:** handle object form of bin field in getPrettierPath ([#​35680](https://github.com/nrwl/nx/pull/35680)) - **core:** detect vscode copilot ai agent ([#​35757](https://github.com/nrwl/nx/pull/35757)) - **core:** allow local plugin subpath imports without custom conditions ([#​35751](https://github.com/nrwl/nx/pull/35751), [#​35631](https://github.com/nrwl/nx/issues/35631)) - **dotnet:** include Directory.*.* files in inputs ([#​35738](https://github.com/nrwl/nx/pull/35738)) - **gradle:** add transitive:true to all tasks ([#​35677](https://github.com/nrwl/nx/pull/35677)) - **gradle:** pin generated e2e project toolchain to installed JDK ([#​35703](https://github.com/nrwl/nx/pull/35703)) - **js:** fall back to npm publish when bun publish fails with auth error ([#​35756](https://github.com/nrwl/nx/pull/35756)) - **linter:** improve convert-to-flat-config output fidelity ([#​35330](https://github.com/nrwl/nx/pull/35330)) - **linter:** only rewrite workspace-package peer deps to workspace:\* ([#​35423](https://github.com/nrwl/nx/pull/35423), [#​35318](https://github.com/nrwl/nx/issues/35318), [#​33417](https://github.com/nrwl/nx/issues/33417)) - **misc:** stop inferring `projects: 'self'` in `dependsOn` entries ([#​35686](https://github.com/nrwl/nx/pull/35686)) - **misc:** skip `$` escaping in file paths on windows ([#​35692](https://github.com/nrwl/nx/pull/35692)) - **repo:** run dotnet restore before publish ([#​35771](https://github.com/nrwl/nx/pull/35771)) - **repo:** run dotnet restore before macos e2e job ([#​35774](https://github.com/nrwl/nx/pull/35774)) - **rsbuild:** infer build outputs from distPath.root directly ([#​35707](https://github.com/nrwl/nx/pull/35707)) - **rsbuild:** lazy-require [@​rsbuild/core](https://github.com/rsbuild/core) in plugin so spec mocks work after jest.resetModules ([#​35707](https://github.com/nrwl/nx/issues/35707)) - **testing:** correct yargs-parser import in getJestProjectsAsync ([#​35672](https://github.com/nrwl/nx/pull/35672), [#​35654](https://github.com/nrwl/nx/issues/35654)) ##### ❤️ Thank You - AgentEnder [@​AgentEnder](https://github.com/AgentEnder) - Artur [@​arturovt](https://github.com/arturovt) - Benjamin Staneck [@​Stanzilla](https://github.com/Stanzilla) - Copilot [@​Copilot](https://github.com/Copilot) - Craigory Coppola [@​AgentEnder](https://github.com/AgentEnder) - FrozenPandaz [@​FrozenPandaz](https://github.com/FrozenPandaz) - Jason Jean [@​FrozenPandaz](https://github.com/FrozenPandaz) - Leosvel Pérez Espinosa [@​leosvelperez](https://github.com/leosvelperez) - leosvelperez [@​leosvelperez](https://github.com/leosvelperez) - Louie Weng [@​lourw](https://github.com/lourw) </details> --- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Enabled. ♻ **Rebasing**: Whenever PR is behind base branch, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about these updates again. --- - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box --- This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate). <!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0MC42Mi4xIiwidXBkYXRlZEluVmVyIjoiNDAuNjIuMSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOlsiZGVwZW5kZW5jaWVzIl19--> Reviewed-on: https://git.unespace.com/julien/apf_portal/pulls/248 Co-authored-by: APF Portal Bot <jgautier.webdev+apf-portal-bot@gmail.com> Co-committed-by: APF Portal Bot <jgautier.webdev+apf-portal-bot@gmail.com> |
||
|
|
ffed212cf9 |
fix(deps): update angular to v21.2.15 (#247)
This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [@angular/common](https://github.com/angular/angular) ([source](https://github.com/angular/angular/tree/HEAD/packages/common)) | dependencies | patch | [`21.2.14` -> `21.2.15`](https://renovatebot.com/diffs/npm/@angular%2fcommon/21.2.14/21.2.15) | | [@angular/compiler](https://github.com/angular/angular) ([source](https://github.com/angular/angular/tree/HEAD/packages/compiler)) | dependencies | patch | [`21.2.14` -> `21.2.15`](https://renovatebot.com/diffs/npm/@angular%2fcompiler/21.2.14/21.2.15) | | [@angular/compiler-cli](https://github.com/angular/angular/tree/main/packages/compiler-cli) ([source](https://github.com/angular/angular/tree/HEAD/packages/compiler-cli)) | devDependencies | patch | [`21.2.14` -> `21.2.15`](https://renovatebot.com/diffs/npm/@angular%2fcompiler-cli/21.2.14/21.2.15) | | [@angular/core](https://github.com/angular/angular) ([source](https://github.com/angular/angular/tree/HEAD/packages/core)) | dependencies | patch | [`21.2.14` -> `21.2.15`](https://renovatebot.com/diffs/npm/@angular%2fcore/21.2.14/21.2.15) | | [@angular/forms](https://github.com/angular/angular) ([source](https://github.com/angular/angular/tree/HEAD/packages/forms)) | dependencies | patch | [`21.2.14` -> `21.2.15`](https://renovatebot.com/diffs/npm/@angular%2fforms/21.2.14/21.2.15) | | [@angular/language-service](https://github.com/angular/angular) ([source](https://github.com/angular/angular/tree/HEAD/packages/language-service)) | devDependencies | patch | [`21.2.14` -> `21.2.15`](https://renovatebot.com/diffs/npm/@angular%2flanguage-service/21.2.14/21.2.15) | | [@angular/localize](https://github.com/angular/angular) | dependencies | patch | [`21.2.14` -> `21.2.15`](https://renovatebot.com/diffs/npm/@angular%2flocalize/21.2.14/21.2.15) | | [@angular/platform-browser](https://github.com/angular/angular) ([source](https://github.com/angular/angular/tree/HEAD/packages/platform-browser)) | dependencies | patch | [`21.2.14` -> `21.2.15`](https://renovatebot.com/diffs/npm/@angular%2fplatform-browser/21.2.14/21.2.15) | | [@angular/router](https://github.com/angular/angular/tree/main/packages/router) ([source](https://github.com/angular/angular/tree/HEAD/packages/router)) | dependencies | patch | [`21.2.14` -> `21.2.15`](https://renovatebot.com/diffs/npm/@angular%2frouter/21.2.14/21.2.15) | --- ### Release Notes <details> <summary>angular/angular (@​angular/common)</summary> ### [`v21.2.15`](https://github.com/angular/angular/blob/HEAD/CHANGELOG.md#21215-2026-05-28) [Compare Source](https://github.com/angular/angular/compare/v21.2.14...v21.2.15) ##### common | Commit | Type | Description | | -- | -- | -- | | [7f4ac78994](https://github.com/angular/angular/commit/7f4ac78994bff1576ab33f3ce48f95c17f40b4d8) | fix | add upper bounds for digitsInfo | | [300f61feb3](https://github.com/angular/angular/commit/300f61feb3a534bfddf16fcbd240f97b32249699) | fix | sanitize placeholder | ##### compiler | Commit | Type | Description | | -- | -- | -- | | [0b07f47bd6](https://github.com/angular/angular/commit/0b07f47bd6598ae6bd5b75a375e2c817a3c0f243) | fix | normalize tag names with custom namespaces in DomElementSchemaRegistry ([#​68925](https://github.com/angular/angular/pull/68925)) | | [eb1cbbf2eb](https://github.com/angular/angular/commit/eb1cbbf2eb5833219a367a61c04eb07aaa36cc29) | fix | prevent namespaced SVG <style> elements from being stripped | | [cc1378d54b](https://github.com/angular/angular/commit/cc1378d54bd93f3882d732261be8e66720eb71b2) | fix | sanitize dynamic href and xlink:href bindings on SVG a elements ([#​68925](https://github.com/angular/angular/pull/68925)) | | [782e01594e](https://github.com/angular/angular/commit/782e01594e2ad9134c7385dcf3b518101b23ccab) | fix | strip namespaced SVG script elements during template compilation ([#​68925](https://github.com/angular/angular/pull/68925)) | ##### core | Commit | Type | Description | | -- | -- | -- | | [ff12fe55ac](https://github.com/angular/angular/commit/ff12fe55ace5e861ba261afb4c0480ff3c40a192) | fix | normalize tag names in runtime i18n attribute security context lookup ([#​68925](https://github.com/angular/angular/pull/68925)) | | [e6fe77cc97](https://github.com/angular/angular/commit/e6fe77cc97fd10351687416f938bf754aff4eb9f) | fix | sanitize meta selectors | | [daaf32937f](https://github.com/angular/angular/commit/daaf32937fd5c46e411b26f7c082613716fe9550) | fix | support prefix-insensitive DOM schema lookups and compile-time i18n attribute validation ([#​68925](https://github.com/angular/angular/pull/68925)) | | [dada86e43d](https://github.com/angular/angular/commit/dada86e43d847204f714d1a933084617ab941c0a) | fix | synchronize core sanitization schema with compiler ([#​68925](https://github.com/angular/angular/pull/68925)) | ##### http | Commit | Type | Description | | -- | -- | -- | | [582a417bd2](https://github.com/angular/angular/commit/582a417bd27fdaf989e5065dbcdf1ad752faf70c) | fix | exclude withCredentials requests from transfer cache | | [5c6d6df34b](https://github.com/angular/angular/commit/5c6d6df34bbeff3ce98f3b35875444f925cc8f51) | fix | skip TransferCache for cookie-bearing requests by default | ##### platform-server | Commit | Type | Description | | -- | -- | -- | | [37e8aadf87](https://github.com/angular/angular/commit/37e8aadf87b4facfcaf002a1557f8c393a362d97) | fix | prevent SSRF bypasses via backslash URLs in HttpClient | | [72696e244e](https://github.com/angular/angular/commit/72696e244ed7646cca9ab9afc7769a2163943bda) | fix | secure location and document initialization against SSRF and path hijack | ##### service-worker | Commit | Type | Description | | -- | -- | -- | | [b8bd49341d](https://github.com/angular/angular/commit/b8bd49341ddcee10d119a9d4aa8e5736e4e5da53) | fix | Preserves explicit 'credentials: omit' in asset requests | | [ca32fc1000](https://github.com/angular/angular/commit/ca32fc10001301e6174804f9abcfba62252334f4) | fix | Preserves HTTP cache mode in asset group requests | <!-- CHANGELOG SPLIT MARKER --> </details> --- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Enabled. ♻ **Rebasing**: Whenever PR is behind base branch, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about these updates again. --- - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box --- This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate). <!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0MC42Mi4xIiwidXBkYXRlZEluVmVyIjoiNDAuNjIuMSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOlsiZGVwZW5kZW5jaWVzIl19--> Reviewed-on: #247 Co-authored-by: APF Portal Bot <jgautier.webdev+apf-portal-bot@gmail.com> Co-committed-by: APF Portal Bot <jgautier.webdev+apf-portal-bot@gmail.com> |
||
|
|
67d04b0a78 |
fix(deps): update nestjs (#246)
This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [@nestjs/common](https://nestjs.com) ([source](https://github.com/nestjs/nest/tree/HEAD/packages/common)) | dependencies | patch | [`11.1.21` -> `11.1.24`](https://renovatebot.com/diffs/npm/@nestjs%2fcommon/11.1.21/11.1.24) | | [@nestjs/core](https://nestjs.com) ([source](https://github.com/nestjs/nest/tree/HEAD/packages/core)) | dependencies | patch | [`11.1.21` -> `11.1.24`](https://renovatebot.com/diffs/npm/@nestjs%2fcore/11.1.21/11.1.24) | | [@nestjs/platform-express](https://nestjs.com) ([source](https://github.com/nestjs/nest/tree/HEAD/packages/platform-express)) | dependencies | patch | [`11.1.21` -> `11.1.24`](https://renovatebot.com/diffs/npm/@nestjs%2fplatform-express/11.1.21/11.1.24) | | [@nestjs/swagger](https://github.com/nestjs/swagger) | dependencies | patch | [`11.4.3` -> `11.4.4`](https://renovatebot.com/diffs/npm/@nestjs%2fswagger/11.4.3/11.4.4) | | [@nestjs/testing](https://nestjs.com) ([source](https://github.com/nestjs/nest/tree/HEAD/packages/testing)) | devDependencies | patch | [`11.1.21` -> `11.1.24`](https://renovatebot.com/diffs/npm/@nestjs%2ftesting/11.1.21/11.1.24) | --- ### Release Notes <details> <summary>nestjs/nest (@​nestjs/common)</summary> ### [`v11.1.24`](https://github.com/nestjs/nest/releases/tag/v11.1.24) [Compare Source](https://github.com/nestjs/nest/compare/v11.1.23...v11.1.24) ##### v11.1.24 (2026-05-25) ##### Bug fixes - `core` - [#​17009](https://github.com/nestjs/nest/pull/17009) fix(core): reset dependency-tree cache on metadata changes ([@​puneetdixit200](https://github.com/puneetdixit200)) ##### Enhancements - `core` - [#​16997](https://github.com/nestjs/nest/pull/16997) feat(core): warn on late websocket adapter registration ([@​hbinhng](https://github.com/hbinhng)) ##### Dependencies - `platform-ws` - [#​17011](https://github.com/nestjs/nest/pull/17011) chore(deps): bump ws from 8.20.1 to 8.21.0 ([@​dependabot\[bot\]](https://github.com/apps/dependabot)) ##### Committers: 2 - Nguyễn Hải Bình ([@​hbinhng](https://github.com/hbinhng)) - Puneet Dixit ([@​puneetdixit200](https://github.com/puneetdixit200)) ### [`v11.1.23`](https://github.com/nestjs/nest/releases/tag/v11.1.23) [Compare Source](https://github.com/nestjs/nest/compare/v11.1.22...v11.1.23) ##### v11.1.23 (2026-05-21) ##### Bug fixes - `core` - https://github.com/nestjs/nest/issues/16998 fix snapshot: true eagerly instantiates Terminus transient indicators since 11.1.20 ##### Committers: 1 - Kamil Mysliwiec ([@​kamilmysliwiec](https://github.com/kamilmysliwiec)) ### [`v11.1.22`](https://github.com/nestjs/nest/releases/tag/v11.1.22) [Compare Source](https://github.com/nestjs/nest/compare/v11.1.21...v11.1.22) ##### v11.1.22 (2026-05-21) ##### Bug fixes - `core` - [#​16993](https://github.com/nestjs/nest/pull/16993) fix(core): inflight request injection bug [#​16989](https://github.com/nestjs/nest/issues/16989) ([@​kamilmysliwiec](https://github.com/kamilmysliwiec)) ##### Enhancements - `core` - [#​16967](https://github.com/nestjs/nest/pull/16967) fix(core): identify decorator type in invalid-class-module error ([@​HarrierOnChain](https://github.com/HarrierOnChain)) - ##### Committers: 2 - Harrier ([@​HarrierOnChain](https://github.com/HarrierOnChain)) - Kamil Mysliwiec ([@​kamilmysliwiec](https://github.com/kamilmysliwiec)) </details> <details> <summary>nestjs/swagger (@​nestjs/swagger)</summary> ### [`v11.4.4`](https://github.com/nestjs/swagger/releases/tag/11.4.4) [Compare Source](https://github.com/nestjs/swagger/compare/11.4.3...11.4.4) #### 11.4.4 (2026-05-21) ##### Bug fixes - [#​3930](https://github.com/nestjs/swagger/pull/3930) fix: top-level nullable with discriminator issue ([@​kamilmysliwiec](https://github.com/kamilmysliwiec)) ##### Enhancements - [#​3921](https://github.com/nestjs/swagger/pull/3921) feat(swagger): add summary field to Tag Object (OpenAPI 3.2) ([@​frbuceta](https://github.com/frbuceta)) - [#​3924](https://github.com/nestjs/swagger/pull/3924) feat(swagger): warn when [@​ApiTags](https://github.com/ApiTags) receives hierarchy fields ([@​frbuceta](https://github.com/frbuceta)) - [#​3925](https://github.com/nestjs/swagger/pull/3925) fix(swagger): type Tag Object kind as a free-form string ([@​frbuceta](https://github.com/frbuceta)) ##### Committers: 4 - Alexander Scholz ([@​LucidityDesign](https://github.com/LucidityDesign)) - Francisco Buceta ([@​frbuceta](https://github.com/frbuceta)) - Kamil Mysliwiec ([@​kamilmysliwiec](https://github.com/kamilmysliwiec)) - Natanael dos Santos Feitosa ([@​natanfeitosa](https://github.com/natanfeitosa)) </details> --- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Enabled. ♻ **Rebasing**: Whenever PR is behind base branch, or you tick the rebase/retry checkbox. 👻 **Immortal**: This PR will be recreated if closed unmerged. Get [config help](https://github.com/renovatebot/renovate/discussions) if that's undesired. --- - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box --- This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate). <!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0MC42Mi4xIiwidXBkYXRlZEluVmVyIjoiNDAuNjIuMSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOlsiZGVwZW5kZW5jaWVzIl19--> Reviewed-on: https://git.unespace.com/julien/apf_portal/pulls/246 Co-authored-by: APF Portal Bot <jgautier.webdev+apf-portal-bot@gmail.com> Co-committed-by: APF Portal Bot <jgautier.webdev+apf-portal-bot@gmail.com> |
||
|
|
0bdc065eb4 |
fix(deps): update dependency nestjs-cls to v6.2.1 (#245)
This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [nestjs-cls](https://papooch.github.io/nestjs-cls/) ([source](https://github.com/Papooch/nestjs-cls)) | dependencies | patch | [`6.2.0` -> `6.2.1`](https://renovatebot.com/diffs/npm/nestjs-cls/6.2.0/6.2.1) | --- ### Release Notes <details> <summary>Papooch/nestjs-cls (nestjs-cls)</summary> ### [`v6.2.1`](https://github.com/Papooch/nestjs-cls/releases/tag/nestjs-cls%406.2.1) [Compare Source](https://github.com/Papooch/nestjs-cls/compare/nestjs-cls@6.2.0...nestjs-cls@6.2.1) ##### Bug Fixes - **core**: establish ALS root context at init to mitigate enterWith leak on Node < 24 ([#​577](https://github.com/Papooch/nestjs-cls/issues/577)) ([05b8fb0](https://github.com/Papooch/nestjs-cls/commits/05b8fb0)) - **core**: improve ClsPluginsHooksHost error message and document app.init() requirement ([#​578](https://github.com/Papooch/nestjs-cls/issues/578)) ([2a4e874](https://github.com/Papooch/nestjs-cls/commits/2a4e874)) </details> --- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Enabled. ♻ **Rebasing**: Whenever PR is behind base branch, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box --- This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate). <!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0MC42Mi4xIiwidXBkYXRlZEluVmVyIjoiNDAuNjIuMSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOlsiZGVwZW5kZW5jaWVzIl19--> Reviewed-on: https://git.unespace.com/julien/apf_portal/pulls/245 Co-authored-by: APF Portal Bot <jgautier.webdev+apf-portal-bot@gmail.com> Co-committed-by: APF Portal Bot <jgautier.webdev+apf-portal-bot@gmail.com> |
||
|
|
c41f0a9af9 |
fix(deps): update angular to v21.2.13 (#244)
This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [@angular-devkit/core](https://github.com/angular/angular-cli) | devDependencies | patch | [`21.2.12` -> `21.2.13`](https://renovatebot.com/diffs/npm/@angular-devkit%2fcore/21.2.12/21.2.13) | | [@angular-devkit/schematics](https://github.com/angular/angular-cli) | devDependencies | patch | [`21.2.12` -> `21.2.13`](https://renovatebot.com/diffs/npm/@angular-devkit%2fschematics/21.2.12/21.2.13) | | [@angular/build](https://github.com/angular/angular-cli) | devDependencies | patch | [`21.2.12` -> `21.2.13`](https://renovatebot.com/diffs/npm/@angular%2fbuild/21.2.12/21.2.13) | | [@angular/cdk](https://github.com/angular/components) | dependencies | patch | [`21.2.12` -> `21.2.13`](https://renovatebot.com/diffs/npm/@angular%2fcdk/21.2.12/21.2.13) | | [@angular/cli](https://github.com/angular/angular-cli) | devDependencies | patch | [`21.2.12` -> `21.2.13`](https://renovatebot.com/diffs/npm/@angular%2fcli/21.2.12/21.2.13) | | [@schematics/angular](https://github.com/angular/angular-cli) | devDependencies | patch | [`21.2.12` -> `21.2.13`](https://renovatebot.com/diffs/npm/@schematics%2fangular/21.2.12/21.2.13) | --- ### Release Notes <details> <summary>angular/angular-cli (@​angular-devkit/core)</summary> ### [`v21.2.13`](https://github.com/angular/angular-cli/blob/HEAD/CHANGELOG.md#21213-2026-05-27) [Compare Source](https://github.com/angular/angular-cli/compare/v21.2.12...v21.2.13) ##### [@​angular-devkit/build-angular](https://github.com/angular-devkit/build-angular) | Commit | Type | Description | | --------------------------------------------------------------------------------------------------- | ---- | ---------------------------------------------------------- | | [3c6d26a31](https://github.com/angular/angular-cli/commit/3c6d26a316cd6aea455c19b249dc6852d84a698e) | fix | remove unconditional CORS wildcard from webpack dev-server | ##### [@​angular/build](https://github.com/angular/build) | Commit | Type | Description | | --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------- | | [2b3e95517](https://github.com/angular/angular-cli/commit/2b3e95517358f8ef3482d5319d970f4774e45ad0) | fix | assert that asset input paths are within workspace root | <!-- CHANGELOG SPLIT MARKER --> </details> <details> <summary>angular/components (@​angular/cdk)</summary> ### [`v21.2.13`](https://github.com/angular/components/blob/HEAD/CHANGELOG.md#21213-21-2-13-2026-05-27) [Compare Source](https://github.com/angular/components/compare/v21.2.12...v21.2.13) No user facing changes in this release <!-- CHANGELOG SPLIT MARKER --> </details> --- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Enabled. ♻ **Rebasing**: Whenever PR is behind base branch, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about these updates again. --- - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box --- This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate). <!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0MC42Mi4xIiwidXBkYXRlZEluVmVyIjoiNDAuNjIuMSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOlsiZGVwZW5kZW5jaWVzIl19--> Reviewed-on: #244 Co-authored-by: APF Portal Bot <jgautier.webdev+apf-portal-bot@gmail.com> Co-committed-by: APF Portal Bot <jgautier.webdev+apf-portal-bot@gmail.com> |
||
|
|
c5704ef2af |
chore(deps): update dependency dayjs to v1.11.21 (#242)
This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [dayjs](https://day.js.org) ([source](https://github.com/iamkun/dayjs)) | devDependencies | patch | [`1.11.20` -> `1.11.21`](https://renovatebot.com/diffs/npm/dayjs/1.11.20/1.11.21) | --- ### Release Notes <details> <summary>iamkun/dayjs (dayjs)</summary> ### [`v1.11.21`](https://github.com/iamkun/dayjs/blob/HEAD/CHANGELOG.md#11121-2026-05-26) [Compare Source](https://github.com/iamkun/dayjs/compare/v1.11.20...v1.11.21) ##### Bug Fixes - preserve unsupported year tokens in format ([#​3015](https://github.com/iamkun/dayjs/issues/3015)) ([#​3016](https://github.com/iamkun/dayjs/issues/3016)) ([8fda602](https://github.com/iamkun/dayjs/commit/8fda602beac5abbc64230ddc49085aa532320f26)) </details> --- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Enabled. ♻ **Rebasing**: Whenever PR is behind base branch, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box --- This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate). <!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0MC42Mi4xIiwidXBkYXRlZEluVmVyIjoiNDAuNjIuMSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOlsiZGVwZW5kZW5jaWVzIl19--> Reviewed-on: https://git.unespace.com/julien/apf_portal/pulls/242 Co-authored-by: APF Portal Bot <jgautier.webdev+apf-portal-bot@gmail.com> Co-committed-by: APF Portal Bot <jgautier.webdev+apf-portal-bot@gmail.com> |
||
|
|
e88263a985 |
fix(ci): give gitlab perf job a discoverable chromium (ADR-0028) (#243)
## Summary Fix the GitLab `perf` job, which failed at the Lighthouse CI healthcheck with `Chrome installation not found`. Follow-up on [ADR-0028](../docs/decisions/0028-migrate-cicd-and-git-hosting-to-gitlab.md) Phase 2 (`.gitlab-ci.yml` landed in #241). ## Root cause The `perf` job ran on `mcr.microsoft.com/playwright:v1.55.0-jammy`. That image **does** ship Chromium, but under `/ms-playwright/chromium-<rev>/chrome-linux/chrome` — a non-standard path/name. Lighthouse CI uses `chrome-launcher`, which discovers Chrome by probing the `PATH` (`google-chrome`, `chromium`, `chromium-browser`) or the `CHROME_PATH` env var. It cannot find Playwright's bundled Chromium, so the healthcheck fails before any run starts. The same class of failure ("absence de Chrome pour Lighthouse") was hit and solved on the Gitea side — there the `catthehacker/ubuntu:full-22.04` image happened to carry Chrome at a standard location. ## Fix Decouple the two browser-driving tools instead of forcing one image to serve both: | Aspect | Before | After | | --- | --- | --- | | `image` | `mcr.microsoft.com/playwright:v1.55.0-jammy` | `node:24-bookworm` (same as the `.node-job` base) | | Chrome | bundled, undiscoverable | `apt-get install --no-install-recommends chromium` → `/usr/bin/chromium` | | `CHROME_PATH` | unset | `/usr/bin/chromium` (explicit, removes any PATH ambiguity) | | `before_script` | inherited | `!reference [.node-job, before_script]` + the apt install | Rationale for not pinning Playwright's Chromium via `CHROME_PATH` instead: that couples the perf job to an exact match between the npm `@playwright/test` version and the Docker image tag. A Renovate bump of `@playwright/test` while the image stays at `v1.55.0` would silently break `executablePath()`. The apt-installed Debian `chromium` has no such coupling. The future ADR-0016 axe-core e2e job — which drives **Playwright** directly — will use the Playwright image. Lighthouse wants a standard Chrome; Playwright wants its own browsers. Different tools, different images. `lighthouserc.js` already passes `--no-sandbox` (containerised-CI requirement), so no change needed there — that flag's rationale applies identically on GitLab Runner. ## Trade-off noted The `apt-get install chromium` re-downloads ~100 MB per perf run (the package lands in the job container's ephemeral writable layer, not a cached image layer). Acceptable for now. If perf-job duration or `vm-gitlab` bandwidth becomes a pain, the clean optimisation is a small custom image with Chromium pre-installed, pushed to GitLab's Container Registry — out of scope here, flagged for later. ## Test plan - [ ] GitLab `perf` job reaches the Lighthouse run (no more `Chrome installation not found`) and the ADR-0017 assertions evaluate. - [ ] `chromium` apt install completes on `node:24-bookworm` (Debian bookworm `main` carries the package). - [ ] `.gitlab-ci.yml` passes GitLab CI Lint (Project → Build → Pipeline Editor → Validate) — confirms the `!reference` + added keys parse. - [ ] Other jobs (`check`, `audit`, `commits`, `a11y`, `sast`, `secret_detection`) unaffected. ## Related - [ADR-0028](../docs/decisions/0028-migrate-cicd-and-git-hosting-to-gitlab.md) Phase 2 — `.gitlab-ci.yml` (#241). - [ADR-0017](../docs/decisions/0017-performance-budgets-lighthouse-ci.md) — Lighthouse CI perf budgets. - [ADR-0016](../docs/decisions/0016-accessibility-baseline-wcag-aa-targeted-aaa.md) — future axe-core e2e (will own the Playwright image). --------- Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr> Reviewed-on: #243 |
||
|
|
c24795f1d6 |
ci(gitlab): land .gitlab-ci.yml alongside gitea workflow (ADR-0028 phase 2) (#241)
## Summary Bump the existing `tmp` security override from `>=0.2.4` to `>=0.2.6` to cover [GHSA-ph9p-34f9-6g65](https://github.com/advisories/GHSA-ph9p-34f9-6g65) — a path-traversal vulnerability via unsanitized prefix/postfix in `tmp@<0.2.6`. Surfaced by `pnpm ci:audit` on the next CI run; both Gitea and the in-flight GitLab Phase 2 pipeline fail without this. ## Why this isn't an upstream upgrade `tmp` is a transitive dependency. The two consuming paths today: | Path | Latest available | | --- | --- | | `.>nx>tmp` | `nx@22.7.4` still declares `tmp@^0.2.4` | | `.>@lhci/cli>tmp` | `@lhci/cli@0.15.1` (latest) still declares `tmp@^0.1.0` | Upgrading `nx` or `@lhci/cli` does not move `tmp` to 0.2.6. The pre-existing `pnpm.overrides` entry was already pinning `tmp@<0.2.4` → `>=0.2.4` against the prior advisory; this PR just widens the lower bound to match the new one. Same pattern, one line. ## What lands | File | Change | | --- | --- | | `package.json` | `"tmp@<0.2.4": ">=0.2.4"` → `"tmp@<0.2.6": ">=0.2.6"` (one line). | | `pnpm-lock.yaml` | `tmp@0.2.4` → `tmp@0.2.6` resolved; `@scalar/nestjs-api-reference@1.1.16 → 1.1.19` picked up as a natural transitive resolution during `pnpm install` (no semver-major, both are within the existing `^1.1.14` range and match Renovate's "patch + auto-merge" policy). | ## Risk Patch bump (0.2.4 → 0.2.6) on a tiny library with a stable public API since v0.2.0. The release notes for 0.2.5 and 0.2.6 are sanitization-only fixes — no API surface change. Risk of regression in nx / lhci consumers: negligible. ## Test plan - [x] `pnpm audit --audit-level=moderate` returns `No known vulnerabilities found` (exit 0). - [x] `pnpm why tmp` shows `tmp@0.2.6` as the single resolved version (no duplicate). - [ ] CI green on Gitea (`check` + `scan` jobs). - [ ] Once merged, rebase `ci/gitlab-pipeline-phase2` on top and retry the GitLab `audit` job — expected green. ## Related - [GHSA-ph9p-34f9-6g65](https://github.com/advisories/GHSA-ph9p-34f9-6g65) — the advisory. - Unblocks [ADR-0028](../docs/decisions/0028-migrate-cicd-and-git-hosting-to-gitlab.md) Phase 2 pipeline parity validation (see `ci/gitlab-pipeline-phase2` branch). --------- Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr> Reviewed-on: #241 |
||
|
|
a848e0fabd |
chore(deps): update dependency @swc/helpers to v0.5.23 (#239)
This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [@swc/helpers](https://swc.rs) ([source](https://github.com/swc-project/swc/tree/HEAD/packages/helpers)) | devDependencies | patch | [`0.5.21` -> `0.5.23`](https://renovatebot.com/diffs/npm/@swc%2fhelpers/0.5.21/0.5.23) | --- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Enabled. ♻ **Rebasing**: Whenever PR is behind base branch, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box --- This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate). <!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0MC42Mi4xIiwidXBkYXRlZEluVmVyIjoiNDAuNjIuMSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOlsiZGVwZW5kZW5jaWVzIl19--> Reviewed-on: #239 Co-authored-by: APF Portal Bot <jgautier.webdev+apf-portal-bot@gmail.com> Co-committed-by: APF Portal Bot <jgautier.webdev+apf-portal-bot@gmail.com> |
||
|
|
e2894afc4a |
chore(deps): bump tmp override to >=0.2.6 (GHSA-ph9p-34f9-6g65) (#240)
## Summary Bump the existing `tmp` security override from `>=0.2.4` to `>=0.2.6` to cover [GHSA-ph9p-34f9-6g65](https://github.com/advisories/GHSA-ph9p-34f9-6g65) — a path-traversal vulnerability via unsanitized prefix/postfix in `tmp@<0.2.6`. Surfaced by `pnpm ci:audit` on the next CI run; both Gitea and the in-flight GitLab Phase 2 pipeline fail without this. ## Why this isn't an upstream upgrade `tmp` is a transitive dependency. The two consuming paths today: | Path | Latest available | | --- | --- | | `.>nx>tmp` | `nx@22.7.4` still declares `tmp@^0.2.4` | | `.>@lhci/cli>tmp` | `@lhci/cli@0.15.1` (latest) still declares `tmp@^0.1.0` | Upgrading `nx` or `@lhci/cli` does not move `tmp` to 0.2.6. The pre-existing `pnpm.overrides` entry was already pinning `tmp@<0.2.4` → `>=0.2.4` against the prior advisory; this PR just widens the lower bound to match the new one. Same pattern, one line. ## What lands | File | Change | | --- | --- | | `package.json` | `"tmp@<0.2.4": ">=0.2.4"` → `"tmp@<0.2.6": ">=0.2.6"` (one line). | | `pnpm-lock.yaml` | `tmp@0.2.4` → `tmp@0.2.6` resolved; `@scalar/nestjs-api-reference@1.1.16 → 1.1.19` picked up as a natural transitive resolution during `pnpm install` (no semver-major, both are within the existing `^1.1.14` range and match Renovate's "patch + auto-merge" policy). | ## Risk Patch bump (0.2.4 → 0.2.6) on a tiny library with a stable public API since v0.2.0. The release notes for 0.2.5 and 0.2.6 are sanitization-only fixes — no API surface change. Risk of regression in nx / lhci consumers: negligible. ## Test plan - [x] `pnpm audit --audit-level=moderate` returns `No known vulnerabilities found` (exit 0). - [x] `pnpm why tmp` shows `tmp@0.2.6` as the single resolved version (no duplicate). - [ ] CI green on Gitea (`check` + `scan` jobs). - [ ] Once merged, rebase `ci/gitlab-pipeline-phase2` on top and retry the GitLab `audit` job — expected green. ## Related - [GHSA-ph9p-34f9-6g65](https://github.com/advisories/GHSA-ph9p-34f9-6g65) — the advisory. - Unblocks [ADR-0028](../docs/decisions/0028-migrate-cicd-and-git-hosting-to-gitlab.md) Phase 2 pipeline parity validation (see `ci/gitlab-pipeline-phase2` branch). --------- Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr> Reviewed-on: #240 |
||
|
|
3191040bbc |
chore(gitlab): add MR template (ADR-0028 phase 1) (#238)
## Summary [ADR-0028](../docs/decisions/0028-migrate-cicd-and-git-hosting-to-gitlab.md) Phase 1 (`mirror-and-bootstrap`) — repo-side groundwork for the GitLab CE migration. Phase 1 is mostly ops work on `vm-gitlab` (groups, mirror push, branch protection, bot account); the only artefact that needs to land in the codebase ahead of the cutover is the GitLab MR template, so that the moment Phase 2 lands `.gitlab-ci.yml` and developers start opening MRs, they get the same structured prompt Gitea PRs use today. ## What lands - **New file `.gitlab/merge_request_templates/Default.md`** — port of `.gitea/pull_request_template.md`, content-identical except for two adjustments: - "PR title" → "MR title" in the comment header (GitLab terminology). - **Typo fix:** the section reference to the Conventional Commits convention was `docs/development.md §5` in the original (which is "Observability dev-loop"); the correct section is **§7 "Conventional commit cycle"**. The Gitea template still has the wrong ref — it gets deleted in Phase 3 cutover so fixing it there rather than touching it twice. GitLab discovers `.gitlab/merge_request_templates/*.md` automatically; `Default.md` becomes the default for new MRs without any project-settings step. ## Out of scope (per [ADR-0028 §"Migration sequence"](../docs/decisions/0028-migrate-cicd-and-git-hosting-to-gitlab.md)) | Item | Phase | | --- | --- | | `.gitlab-ci.yml` | 2 (parallel pipelines) | | `renovate.json` `customManagers.fileMatch` swap from `.gitea/workflows/*.yml` → `.gitlab-ci.yml` | 2-3 (after `.gitlab-ci.yml` exists) | | Remote URLs in CLAUDE.md / `docs/setup/01-dev-debian-vm-setup.md` §8 | 3 (cutover) | | Deletion of `.gitea/workflows/` + `infra/ci-runners.compose.yml` + `infra/data/runner-*/` | 3 (cutover) | | Stale-reference sweep + signed-commit policy | 4 (cleanup) | ## Ops checklist (executed on `vm-gitlab` — not part of this PR) For traceability; the following happens outside the repo, on `vm-gitlab` (`10.100.201.10`, LAN-only — VPN for off-site access is sufficient): - [ ] Create group `apf-portal` + project `apf_portal` on GitLab CE. - [ ] `git remote add gitlab git@10.100.201.10:apf-portal/apf_portal.git && git push --mirror gitlab` from a current clone. - [ ] Configure branch protection on `main`: push direct disabled, squash-merge only, status checks list left empty (peuplé en Phase 2 quand `.gitlab-ci.yml` apparaît). - [ ] Create `apf-portal-bot` service account on GitLab, issue a dedicated SSH/GPG signing key (cf. ADR-0028 §"Signed commits"), generate a Renovate platform token. - [ ] Reconfigure the existing Renovate runtime (host config, not in this repo) to add the GitLab platform endpoint pointed at `10.100.201.10`. Renovate's `config:recommended` + the `renovate.json` in-repo are platform-agnostic; only the bot's host config needs the new endpoint + token. ## Test plan - [x] `.gitlab/merge_request_templates/Default.md` markdown renders identically to the Gitea template in a local preview. - [x] No CI gates touched — `pnpm ci:check` / `ci:audit` unchanged. - [ ] On first MR opened post-mirror, the Default template appears auto-selected. ## Related - [ADR-0028](../docs/decisions/0028-migrate-cicd-and-git-hosting-to-gitlab.md) — migration ADR (accepted in #227). - Next: Phase 2 (`gitlab-ci-pipeline`) once `vm-gitlab` ops bootstrap is complete and the runner is registered. --------- Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr> Reviewed-on: #238 |
||
|
|
8bc36b460a |
fix(admin): unbreak ci:check on PR2b (TS2454 + missing fr translation) (#237)
## Summary Follow-up fix on [#236](#236) (ADR-0026 PR 2b). Two issues that `nx run-many -t build` exposed but `nx run-many -t test` masked: 1. **TS2454 in `user-scopes.service.ts`** — webpack's strict-mode build flagged `let exists: boolean` as "used before assigned". jest's `--transpile-only` doesn't enforce definite-assignment so it slipped through local checks. 2. **Missing FR translation** for `route.user-scopes.title` — Angular's `@angular/localize` build target requires every `$localize` ID to have a target in `messages.fr.xlf`. The route I added in #236 referenced the ID but I didn't add the trans-unit. ## What lands | File | Change | | --- | --- | | `apps/portal-bff/src/admin/user-scopes.service.ts` | Initialise `let exists = false` instead of `let exists: boolean`. The `VALUE_BEARING_KINDS.has(kind)` check earlier in the function does narrow `kind` semantically, but `Set.has` doesn't propagate type narrowing the way `Array.includes` or a custom type guard would. Initialising to `false` is the smallest fix; the next `if (!exists)` still throws the friendly "does not match" message in the (unreachable-by-construction) fallback. | | `apps/portal-admin/src/locale/messages.fr.xlf` | New `<trans-unit id="route.user-scopes.title">` — FR target `Périmètres utilisateur — Administration APF Portal`. | ## Why these failed locally - **TS2454**: jest runs with `--transpile-only` so it skips definite-assignment analysis (and most type-checking). Webpack's production build runs full `tsc` and catches it. - **Missing translation**: the dev server `pnpm nx serve portal-admin` doesn't run the localize pass; only `--configuration=production` does. My local `nx test portal-admin` runs in JIT mode against the source locale, so the missing FR target slid through. For the future: `pnpm nx run-many -t build --projects=portal-bff,portal-admin` would have caught both pre-PR. Worth a `pnpm ci:check:fast` shorthand that runs a subset of the CI gates locally before push — but that's a separate PR. ## Test plan - [x] `pnpm exec nx run-many -t build --projects=portal-bff,portal-admin` — both pass. - [x] No application logic changed in either fix (initialise vs declare; add a missing translation row). - [ ] CI green on this PR. --------- Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr> Reviewed-on: #237 |
||
|
|
928ed0cdc2 |
feat(admin): add /admin/users/:oid/scopes screen + endpoints (ADR-0026 PR 2b) (#236)
## Summary ADR-0026 PR 2b (second half of PR 2). Ships the operator surface for the `@RequireScope` stack — the admin Angular screen at `/admin/users/:oid/scopes` lets an admin list / grant / revoke scopes for an existing portal User. Both write paths emit blocking audit rows per ADR-0013 (`admin.scope_granted` / `admin.scope_revoked`). Closes the ADR-0026 trilogy: | PR | Status | Effect | | --- | --- | --- | | ADR-0026 PR 1 (#232) | merged | Person + User + UserScope schema + provisioner + drift gate Person.source + PrincipalBuilder real UUIDs. | | ADR-0026 PR 2a (#233) | merged | PrismaScopeResolver replaces stub + prisma/seed.ts populates 19 personas. | | **ADR-0026 PR 2b (this)** | proposed | Admin scope-management screen — operator surface. | ## What lands ### BFF (under `apps/portal-bff/src/`) | File | Change | | --- | --- | | `admin/user-scopes.dto.ts` | **New**. `GrantUserScopeDto` (class-validator on kind + value + ISO expiresAt) + response shapes (`UserScopeDto`, `UserScopesPageDto`). | | `admin/user-scopes.service.ts` | **New**. `resolveUserByOid` (404 when no portal User), `list` (`UserScope` rows joined to User+Person), `grant` (validates value against ADR-0027 tables for value-bearing kinds, rejects non-empty for valueless, P2002 → 400), `revoke` (404-protected — won't leak scope-belongs-to-other-user). | | `admin/user-scopes.controller.ts` | **New** REST surface at `/api/admin/users/:oid/scopes` with `@RequireAdmin`. `GET` (no audit, read), `POST` (audit `admin.scope_granted`), `DELETE :scopeId` (audit `admin.scope_revoked`, 204 on success). | | `admin/user-scopes.service.spec.ts` + `admin/user-scopes.controller.spec.ts` | **New** specs — 15 cases across resolve / list / grant (value-bearing + valueless + duplicate) / revoke / audit semantics. | | `admin/admin.module.ts` | Registers `UserScopesController` + `UserScopesService`. | | `audit/audit.types.ts` + `audit/audit.service.ts` | New `AdminScopeGrantedInput` / `AdminScopeRevokedInput` types + `adminScopeGranted` / `adminScopeRevoked` methods on `AuditWriter`. `subject = 'user:<oid>'` so an auditor pivots on the target of the change; payload carries the resolved tuple at the moment of the write (the revoke payload survives the row's deletion). | ### SPA (under `apps/portal-admin/src/app/`) | File | Change | | --- | --- | | `app.routes.ts` | New route `/users/:oid/scopes` (lazy-loaded). | | `pages/user-scopes/user-scopes.service.ts` + `.service.spec.ts` | **New** thin HttpClient wrapper (`list` / `grant` / `revoke`). Same shape convention as `AdminUsersService`. | | `pages/user-scopes/user-scopes.ts` + `.html` + `.scss` + `.spec.ts` | **New** page component. Signals-based + OnPush. Lists current scopes in a table with `Revoke` per row; "Grant a new scope" form with kind dropdown + conditional value input + optional `expiresAt`. Friendly error mapping (403 → "no admin access", 404 → "User not found, has this persona ever signed in or been seeded?", server messages forwarded). | | `pages/users/users.html` | New `Manage scopes` link per row (RouterLink to the new page), with `aria-label` carrying the user's displayName. | | `pages/users/users.ts` + `users.spec.ts` | Adds `RouterLink` import + `provideRouter([])` in the spec fixture (was missing — the route addition exposed it). | ## Key choices - **`:oid` in the URL, not `User.id`.** The existing `/admin/users` list (ADR-0020) keys on Entra `oid` and the new screen is its child — linking from the list to the scopes page without an intermediate UUID lookup is the simplest UX. The BFF translates `oid → User.id` internally via `resolveUserByOid`. - **404 carries a hint.** When `resolveUserByOid` misses, the BFF throws `NotFoundException("No portal User found for Entra oid ${oid}.")` and the SPA translates the 403/404 status to friendly French-English text. The hint mentions "has this persona ever signed in or been seeded?" — a common operator confusion (the user-directory list shows oids that signed in but the `User` row only exists post-sign-in OR post-seed). - **Audit before write commit, write after audit success.** Standard ADR-0013 posture. Order in the controller: service call (which writes the DB row) → audit (blocking). If audit fails, the DB row exists but no audit — a known v1 cost for non-rollback flows. The simpler alternative (audit before write) would emit ghost audit rows for failed writes; we picked the cleaner direction. - **Value-bearing validation against ADR-0027 tables, not catalogue.** `etablissement:0330800013` must match an existing `Structure.code`; `delegation:33` must match `Delegation.code`; `region:75` must match `Region.code`. The lookup is a single `findUnique` per kind; rejection raises 400 with the offending value in the message. Valueless kinds (`self` / `siege` / `unrestricted`) reject any non-empty value as a defensive check (the DTO type already constrains this but the service runs the assertion). - **`UserScope.source = 'admin-ui'`** for every row created by this surface. Distinct from `'seed'` (set by `prisma/seed.ts`) and `'self-signin'` (Person source only). ADR-0029's syncs will add `'pleiades'` / `'acteurs-plus'`. - **A11y per [ADR-0016](https://git.unespace.com/julien/apf_portal/src/branch/main/docs/decisions/0016-accessibility-baseline-wcag-aa-targeted-aaa.md)**: `aria-labelledby` on the form sections, `role="status"`/`aria-live="polite"` for load + error feedback, `role="alert"` for submit errors, `min-height: 44px` on every input/button per the touch-target rule, `aria-label` on the Manage-scopes link carrying the displayName for screen readers, conditional `aria-required` + `aria-describedby` on the value input. - **`window.confirm` before revoke.** Cheap protection against accidental keyboard-activation. v1 OK; a future polish PR could replace with the spartan-ng dialog when it ships. ## Local verification - [x] `node scripts/check-catalogue-drift.mjs` — clean (4 / 24 / 7 / 3). - [x] `pnpm exec nx lint portal-bff` — **0 errors** (13 warnings, all pre-existing). - [x] `pnpm exec nx lint portal-admin` — **0 errors** (3 warnings, all pre-existing). - [x] `pnpm exec nx test portal-bff` — full suite passes (now includes the 2 new `user-scopes` spec files). - [x] `pnpm exec nx test portal-admin` — **71 tests passing** (added 8 for `user-scopes`; updated `users.spec.ts` to provide a router after the RouterLink import). ## Test plan — remaining (manual on dev DB) - [ ] **Sign in as `admin@apfrd...`** (the only persona with `Portal.Admin`) on `portal-admin`. Navigate to `/admin/users`. Click `Manage scopes` for `directeur-bordeaux`. The page shows one row `etablissement:0330800013` (from the seed). - [ ] **Grant a new scope**: kind `delegation`, value `33`. Submit. New row appears; audit row `admin.scope_granted` lands in `audit.events`. - [ ] **Try a bogus value** (kind `etablissement`, value `xyz`): server returns 400, form shows the message. - [ ] **Try a duplicate** (`etablissement:0330800013` again on `directeur-bordeaux`): server returns 400 with "already granted". - [ ] **Revoke a scope**: row disappears; audit row `admin.scope_revoked` lands. - [ ] **Sign in as a non-admin persona** (e.g. `collaborateur-simple`) and try `/admin/users/.../scopes` → 403 + friendly error. - [ ] **Review focus** — the BFF's `validateValueForKind` switch, the SPA's `translateError` mapping, the a11y attributes on the form, the audit ordering in the controller methods. ## Notes for the reviewer - **No scope-vocabulary endpoint.** The form expects the operator to type the `Structure.code` / `Delegation.code` / `Region.code` by hand (with a hint string under the field). Defensive validation catches typos. A future polish PR could add `GET /api/admin/scope-vocabulary` returning all three lists at once + replace the value input with a typeahead dropdown. - **Renamed the service's `UserScopesPage` interface to `UserScopesPayload`.** Discovered the collision with the component class during the first test run. The component keeps the `UserScopesPage` name per Angular convention; the payload type now reads as what it is. - **`UserScope.source` is not under the drift gate today.** The schema comment in PR 1 said "same catalogue posture as Person.source"; in practice the seed writes `'seed'` and this PR writes `'admin-ui'`. Extending the drift gate to cover `UserScope.source` is a small follow-up — defensible to do once ADR-0029's upstream-sync values are landing. ## What's next ADR-0025's `@RequireScope` stack is now end-to-end live for the test tenant: persona → Entra → BFF → PrismaScopeResolver → DB → Principal → guard. The trilogy is done. Per the prior conversation: **GitLab migration Phase 1** (`mirror-and-bootstrap`) is the next item — mostly ops work on `vm-gitlab` (groups, branch protection, Renovate reconfig, deploy keys). The first PR-shaped output from that phase is the Renovate config update. --------- Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr> Reviewed-on: #236 |
||
|
|
55338b83c0 |
fix(seed): use tsconfig.app.json instead of inline --compiler-options (#235)
## Summary Follow-up to [#234](#234). The `db:seed` script's inline `--compiler-options '{"module":"CommonJS",…}'` was getting its quotes stripped by the shell when `pnpm run` re-spawned `ts-node`: ``` > ts-node --compiler-options {module:CommonJS,esModuleInterop:true,target:ES2020} apps/portal-bff/prisma/seed.ts SyntaxError: Expected property name or '}' in JSON at position 1 ``` Cross-platform JSON-on-cmdline quoting through nested `package.json → shell → npm-script → shell → ts-node` is a notorious pain. `apps/portal-bff/tsconfig.app.json` already declares everything `ts-node` needs to transpile the seed (module=commonjs, esModuleInterop=true, target=es2021, moduleResolution=node, types=[node]). Point at it with `-P` and drop the inline JSON entirely. ## What lands | File | Change | | --- | --- | | `package.json` (root) | `db:seed` script: `--compiler-options '{...}'` → `-P apps/portal-bff/tsconfig.app.json`. One-line diff. | That's it. ## Why `tsconfig.app.json` works for the seed even though seed.ts isn't under `src/` `ts-node --transpile-only` reads `compilerOptions` from the `-P` tsconfig but **ignores the `include` / `exclude` fields** for the file passed explicitly on the command line. So the fact that `tsconfig.app.json` has `"include": ["src/**/*.ts"]` and the seed lives under `prisma/` doesn't matter — `ts-node` happily transpiles whatever file you point it at. ## Test plan - [ ] **Locally**: `cd apps/portal-bff && pnpm exec prisma db seed` — no more `SyntaxError` on the compiler-options, seed runs and reports `[seed] test-tenant complete …`. - [ ] **Idempotency** — re-run, expect `created 0 / 0 / 0`. - [x] No application code changed — drift gate / lint / tests untouched. ## Notes for the reviewer - Could have created a dedicated `apps/portal-bff/prisma/tsconfig.json` extending the app config. Less coupling but more files; pointing at the existing `tsconfig.app.json` is the minimum-surface fix. - The Prisma 7 deprecation warning about `package.json#prisma` is unchanged; migrating to `prisma.config.ts` is a separate future PR (would also let us inline the seed config without shell quoting at all). --------- Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr> Reviewed-on: #235 |
||
|
|
827d69594c |
fix(seed): route prisma db seed through pnpm run for cwd resolution (#234)
## Summary Follow-up fix on [#233](#233). The `prisma db seed` command failed with `Cannot find module './seed.ts'` because the seed command's path was resolved against the user's cwd (`apps/portal-bff/`) and doubled into `apps/portal-bff/apps/portal-bff/prisma/seed.ts`. Root cause: Prisma CLI spawns the seed command with cwd = wherever the user invoked `prisma db seed` from. The path `apps/portal-bff/prisma/seed.ts` only works if cwd is the workspace root, but the user's habit (matching `prisma migrate dev`) is to run prisma commands from `apps/portal-bff/`. Fix: indirect through `pnpm run`. `pnpm run` for a root-level script always sets cwd = workspace root regardless of where invoked from. The seed path then resolves predictably. ## What lands | File | Change | | --- | --- | | `package.json` (root) | New `db:seed` npm script in `scripts` carrying the ts-node invocation. `prisma.seed` becomes `pnpm run db:seed` — the indirection forces cwd = workspace root before ts-node resolves the seed path. | That's it. Two-line diff. ## Why `pnpm run` indirection works | Step | cwd | Path resolution | | --- | --- | --- | | User invokes `pnpm exec prisma db seed` from `apps/portal-bff/` | `apps/portal-bff/` | — | | Prisma CLI walks up, finds the workspace `package.json`, reads `prisma.seed` | `apps/portal-bff/` | — | | Prisma spawns the seed command | `apps/portal-bff/` (inherited) | — | | Seed command is `pnpm run db:seed` | `apps/portal-bff/` | — | | **`pnpm run` resets cwd to the package's root (workspace root)** | **workspace root** | — | | ts-node sees `apps/portal-bff/prisma/seed.ts` | workspace root | resolves correctly ✓ | The same flow works if the user invokes from the workspace root — `pnpm run` still ends up at workspace root, idempotent. ## Test plan - [ ] **Locally**: `cd apps/portal-bff && pnpm exec prisma db seed` — should report `[seed] test-tenant complete — created … Person + … User + … UserScope rows; …`. No `MODULE_NOT_FOUND`. - [ ] **Also locally**: same command from the workspace root (`pnpm exec prisma db seed`) — same result. - [ ] **Idempotency** — re-run, expect `created 0 Person + 0 User + 0 UserScope rows`. - [x] No application code changed — drift gate, lint, tests untouched. - [ ] **Review focus** — the `pnpm run` indirection trick + the cwd table above. ## Notes for the reviewer - The deprecation warning about `package.json#prisma` (Prisma 7 migration to `prisma.config.ts`) is unchanged — not in this fix's scope. - Could have also fixed by changing the path to be relative-to-bff (`prisma/seed.ts`) and forcing the user to run from `apps/portal-bff/`. The `pnpm run` approach is cwd-invariant — more robust to where the user invokes from. - The new `db:seed` script is callable directly (`pnpm db:seed`) which is a small ergonomic bonus. --------- Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr> Reviewed-on: #234 |
||
|
|
9b4b4b90d0 |
feat(auth): swap stub for PrismaScopeResolver + add test-tenant seed (ADR-0026 PR 2a) (#233)
## Summary
ADR-0026 PR 2 (first half — backend). Ships:
- **`PrismaScopeResolver`** replacing `StubScopeResolver` in `AuthModule`. Queries `user_scopes WHERE userId = ? AND (expiresAt IS NULL OR expiresAt > NOW())` and maps each row to a typed `Scope` from `shared-auth`.
- **`prisma/seed.ts`** populating Person + User + UserScope rows for the 19 test-tenant personas per `notes/test-tenant-role-assignments.md`. Idempotent — re-running preserves existing rows.
- **`infra/test-tenant.personas.example.json`** — schema template for the gitignored per-persona Entra `oid` map the seed reads.
The admin UI scope-seeding screen `/admin/users/:id/scopes` is split into **PR 2b** (Angular work, follows separately).
## What lands
| File | Change |
| --- | --- |
| `apps/portal-bff/src/auth/prisma-scope-resolver.ts` + `.spec.ts` | **New** Prisma-backed resolver. Server-side `expiresAt` filter (`null OR > NOW()`). Maps `(kind, value)` rows to the discriminated `Scope` union via a pure `toScope` helper. Off-catalogue `kind` values are skipped + WARN-logged (defense in depth — the drift gate is the canonical write-side guard). 10 spec tests covering query shape, valueless / value-bearing kinds, defensive skip on off-catalogue, edge cases on `toScope`. |
| `apps/portal-bff/src/auth/auth.module.ts` | `{ provide: ScopeResolver, useClass: StubScopeResolver }` → `useClass: PrismaScopeResolver`. The `StubScopeResolver` class stays exported from `scope-resolver.ts` (handy for spec fixtures / future "force-unrestricted" dev modes) but is no longer the default wiring. |
| `apps/portal-bff/prisma/seed.ts` | **New** seed script. Hardcoded `PERSONAS` array (19 entries, slug + email + displayName + scope tuples — transcribed from `notes/test-tenant-role-assignments.md`). For each persona: reads its Entra `oid` from `TEST_TENANT_PERSONAS_PATH` (env var, default `infra/test-tenant.personas.json`); if missing → skip with WARN; otherwise idempotent upsert (findUnique by `entraOid` → reuse, or create Person + User in nested-create transaction with `Person.source = 'seed'`). Then idempotent upserts for each scope (findUnique by `(userId, kind, value)` → skip, or create with `UserScope.source = 'seed'`). Final log: counts of rows created + personas skipped. |
| `infra/test-tenant.personas.example.json` | **New** schema template — flat `{ slug: entra-oid }` map for the 19 personas + a `_README` field documenting the shape. Distinct from `test-tenant.entra.json` (the 24-entry GROUP-guid map for `EntraGroupToRoleResolver`). Real file is gitignored. |
| `apps/portal-bff/.env.example` | New `TEST_TENANT_PERSONAS_PATH` block with the same documentation pattern as `ENTRA_GROUP_MAP_PATH`. |
| `package.json` | `prisma.seed` field added: `ts-node --transpile-only --compiler-options '...' apps/portal-bff/prisma/seed.ts`. Lets `pnpm exec prisma db seed` run the script without a project-specific tsconfig dance. |
## Key choices
- **`PrismaScopeResolver` swap is the default for AuthModule.** No "fallback to stub when DB is unreachable" — a Postgres outage that prevents reading `user_scopes` should fail the sign-in (same posture as `PersonAndUserProvisioner.ensureUser`, which is also blocking). The `StubScopeResolver` class remains in `scope-resolver.ts` for spec fixtures + as a hint of how to wire a "force-everyone-unrestricted" dev override if we ever want one.
- **Defensive `toScope` mapping.** The drift gate enforces catalogue membership at the write site (the future admin scope-seeding UI from PR 2b). The Prisma read side defends in depth by skipping off-catalogue rows — a row with `kind = 'something-bogus'` (e.g. a future migration mistake) is dropped from the resolved scopes + logged, rather than throwing on every sign-in for that user. The unit test `'skips + warns on off-catalogue kinds'` pins the behaviour.
- **Seed reads Entra `oid`s from a gitignored file.** Same pattern as `EntraGroupToRoleResolver` (path via env var, file is gitignored, schema template in `infra/*.example.json`). The `oid`s themselves are tenant-private; the 19 persona slugs + their scope tuples are project-wide and live in `seed.ts`.
- **Seed is idempotent on every level.** The unique constraint on `User.entraOid` lets us "create if missing" without locks; the unique on `(userId, kind, value)` does the same for `UserScope`. Re-running the seed after a partial run picks up where it stopped — no `--reset` needed.
- **No spec for `seed.ts` itself.** It's an integration data loader; meaningful test would require a real Prisma client + DB (or a heavy mock fixture). The persona matrix is hand-verified at PR review; the seed's correctness is exercised by running it on the dev DB (test-plan checkbox below).
## Verification path
- [x] `node scripts/check-catalogue-drift.mjs` — clean (`4 / 24 / 7 / 3`).
- [x] `node --test scripts/check-catalogue-drift.spec.mjs` — 29 tests passing.
- [x] `pnpm exec nx lint portal-bff` — **0 errors**, 13 warnings (all pre-existing).
- [x] `pnpm exec nx test portal-bff` (affected specs filtered) — **774 tests passing** (was 766; +8 for `prisma-scope-resolver.spec.ts`).
- [ ] **Locally / on dev DB**:
- `cp infra/test-tenant.personas.example.json infra/test-tenant.personas.json` + fill in real `oid`s from the Entra admin centre.
- `cd apps/portal-bff && pnpm exec prisma db seed` — should report `created 19 Person + 19 User + N UserScope rows; skipped 0 personas` on a fresh DB.
- Re-run the seed → second invocation reports `0 / 0 / 0 created; skipped 0` (idempotent).
- Sign in via the user portal as one of the 19 personas → `principal.scopes` on the session contains the seeded scopes (e.g. `directeur-bordeaux` sees `[{ kind: 'etablissement', value: '0330800013' }]`).
- [ ] **Review focus** — the `toScope` mapping (especially the defensive `null` branch on off-catalogue kinds), the seed's idempotency invariants, the `prisma.seed` command in `package.json`, the comments-only fields in `test-tenant.personas.example.json`.
## What's next
**PR 2b — Admin `/admin/users/:id/scopes` screen.** Angular admin-app SPA screen + BFF read/write controllers, against the schema this PR + ADR-0026 PR 1 + ADR-0027 PR 1 have now stood up. A11y review per ADR-0016 §"Manual testing cadence". Operator workflow only at that point — the seed in this PR is the bootstrap path for the test tenant.
Once both PR 2a + PR 2b ship: **the `@RequireScope` stack is end-to-end live** for the first time. ADR-0025's stubs (`StubScopeResolver`, the entraOid placeholder on `Principal.user.{id, personId}`) are then fully retired.
---------
Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr>
Reviewed-on: #233
|
||
|
|
24071a63ec |
feat(users): add Person + User + UserScope + lazy provisioner (ADR-0026 PR 1) (#232)
## Summary
First implementation PR for [ADR-0026](docs/decisions/0026-person-user-portal-data-model.md) (portal-side identity model). Ships:
- `Person` golden record + `User` portal-account overlay + `UserScope` Prisma models;
- `PersonAndUserProvisioner` service called from `SessionEstablisher.establish` (blocking — lazy-creates `Person` + `User` at first OIDC callback);
- `PERSON_SOURCES` closed-set catalogue + drift-gate extension;
- `PrincipalBuilder.build(user, identity)` signature update + `ScopeResolver.resolve({ userId })` seam change — `Principal.user.{id, personId}` now carry real portal UUIDs, no longer the `entraOid` placeholder.
No consumer for `UserScope` yet — `PrismaScopeResolver` lands in ADR-0026 PR 2. The `StubScopeResolver` continues to return `[{ kind: 'unrestricted' }]` for everyone.
## What lands
| File | Change |
| --- | --- |
| `apps/portal-bff/prisma/schema.prisma` | **+3 models** in the `public` schema: `Person` (UUID PK + PII + nullable email indexed-not-unique + source catalogue + nullable externalId + back-ref to User), `User` (UUID PK + 1-to-1 personId FK + unique entraOid + tenantId + lastSignInAt + scopes[]), `UserScope` (UUID PK + userId FK with `onDelete: Cascade` + kind + opaque value with `@default("")` + source + nullable expiresAt + `@@unique([userId, kind, value])`). All comments explain the cross-references to ADR-0025 / ADR-0027 / ADR-0029. |
| `apps/portal-bff/prisma/migrations/20260526210000_add_person_user_userscope/migration.sql` | **New** hand-written migration. DDL for the 3 tables, indexes (Person: source/externalId/email; User: unique personId + unique entraOid; UserScope: unique composite + plain userId), FKs with the correct `ON DELETE` actions (User.personId → RESTRICT; UserScope.userId → CASCADE — both Prisma defaults given the schema's `Delegation?` / explicit `onDelete: Cascade`). |
| `apps/portal-bff/src/users/person-source.ts` + `.spec.ts` | **New** catalogue. `PERSON_SOURCES = ['self-signin', 'admin-ui', 'seed'] as const`, `PersonSource` type union, `isPersonSource` type guard. Spec follows the structure-kind shape (catalogue content, no duplicates, type guard true/false, narrowing test via `String()` to widen). |
| `apps/portal-bff/src/users/person-and-user-provisioner.service.ts` + `.spec.ts` | **New** blocking provisioner. Fast path: `findUnique by entraOid` + `update lastSignInAt`. Cold path: nested `create` of Person + linked User in a single transaction. Race-condition handling: catches P2002 on `User.entraOid` and re-runs `ensureUser` (loser of a concurrent first-sign-in falls through to the now-warm fast path). Defense-in-depth `isPersonSource` check on the constant. Spec covers cold/warm/race/non-P2002-propagation paths + `splitDisplayName` cases (single token / trailing whitespace / empty / multi-word). |
| `scripts/check-catalogue-drift.mjs` | Property-literal scanner generalised. Adds `extractPersonSources` + `findPersonSourceViolationsInFile` (property `source` instead of `kind`, otherwise identical to the structure-kind scanner). The two extractors share `extractAsConstArray(path, constName)` and the two property scanners share `findPropertyLiteralViolations(path, validValues, sourceText, opts)`. Error-message formatter unchanged. Closing hint extended to reference all three catalogue locations. |
| `scripts/check-catalogue-drift.spec.mjs` | Fixture now writes a `person-source.ts` alongside `structure-kind.ts`. 7 new tests: extractPersonSources (2), findPersonSourceViolationsInFile (5: skip-no-import, no-violation, flag, non-literal, line/col). Aggregation test updated to assert decorator + Structure.kind + Person.source violations all surface together. **29 tests total, all passing.** |
| `apps/portal-bff/src/auth/scope-resolver.ts` | `resolve(input: { entraOid })` → `resolve(input: { userId })`. Stub still ignores its argument; PR 2's `PrismaScopeResolver` will key queries on `User.id`. Comment block updated to reflect that ADR-0026 PR 1 is now landed (no longer "proposed"). |
| `apps/portal-bff/src/auth/principal-builder.ts` | Signature: `build(user)` → `build(user, identity: { userId, personId })`. `Principal.user.id` / `Principal.user.personId` populated from `identity` instead of `user.oid`. Scope resolver called with `{ userId: identity.userId }`. Doc comment block updated to remove the "placeholder" caveat and document the new wiring. |
| `apps/portal-bff/src/auth/principal-builder.spec.ts` | New `TEST_IDENTITY` constant. Every `builder.build(...)` call gets the second arg. New assertions on `principal.user.id` / `principal.user.personId` carrying the test identity. Edge-case test "asks the scope resolver to resolve by entraOid" renamed + retargeted to `{ userId }`. **All 19 persona tests + 5 edge cases pass.** |
| `apps/portal-bff/src/auth/session-establisher.service.ts` | New constructor arg `personUserProvisioner: PersonAndUserProvisioner`. `establish()` calls `ensureUser({ oid, tenantId, displayName, email: user.username })` **before** `principalBuilder.build` so the identity is available. Comment block explains the Entra `preferred_username` → `Person.email` mapping and the blocking-vs-best-effort distinction with `UserDirectoryService.recordSignIn`. |
| `apps/portal-bff/src/auth/session-establisher.service.spec.ts` | Fixture extended with `provisioner` mock + `PROVISIONED_IDENTITY` constant. `STUB_PRINCIPAL` now carries those UUIDs instead of `user.oid`. New tests: (1) provisioner is called with the right input shape; (2) provisioner runs **before** `principalBuilder.build` (`mock.invocationCallOrder` assertion); (3) provisioner failure propagates and nothing downstream runs (build / audit / directory). |
| `apps/portal-bff/src/auth/auth.controller.spec.ts` | Provisioner mock added to the controller fixture (the controller's specs don't exercise provisioning themselves but `SessionEstablisher`'s constructor needs the arg). Principal stub's UUIDs aligned with the new provisioned shape. |
| `apps/portal-bff/src/users/users.module.ts` | `PersonAndUserProvisioner` registered + exported alongside `UserDirectoryService`. `@Global` so the auth module's `SessionEstablisher` can inject both without re-routing the module graph. Comment block updated to document the blocking/best-effort distinction between the two. |
## Key choices
- **Provisioner is blocking**, not best-effort. Distinct from `UserDirectoryService.recordSignIn` which is still best-effort (ADR-0020 admin-list cache, swallows its own errors). Both run from `SessionEstablisher.establish` per the new ordering: (1) provisioner — blocking, (2) build principal — uses identity, (3) save session, (4) cookie, (5) user-session index (best-effort), (6) audit (blocking ADR-0013), (7) UserDirectoryService.recordSignIn (best-effort), (8) log. A provisioner failure short-circuits the whole flow before any of (3)..(8) — the spec asserts this explicitly.
- **No email-based dedup in v1.** `Person.email` is indexed (not unique). The provisioner keys ONLY on `entraOid`. ADR-0026 §"Why no email-based merging in v1" — two distinct humans genuinely share emails; ADR-0029's sync flow handles operator-confirmed reconciliation.
- **Race-condition handling on first sign-in.** Two concurrent first-sign-ins for the same `entraOid` both fall through to `create`. The unique constraint on `User.entraOid` rejects the loser with P2002; the provisioner catches that specific code and re-runs `ensureUser` — fast path now warm. Pathological infinite-loop guarded by the warm-path behaviour on the second attempt. Spec covers the success retry + the non-P2002 propagation paths.
- **ScopeResolver seam moved from `{ entraOid }` to `{ userId }`.** The stub doesn't care about its argument either way; the change is the seam for ADR-0026 PR 2's `PrismaScopeResolver`, which keys `userScope` queries on `User.id` (UUID). Doing the rename now keeps PR 2 to "swap the implementation" only.
- **`PersonAndUserProvisioner.SELF_SIGNIN_SOURCE`** is a typed constant inside the class, not a magic string. Defense in depth: TS type union (compile-time) + drift gate (`source: 'self-signin'` literal is in a file importing `person-source.ts` — flagged if it ever drifts off-catalogue) + runtime `isPersonSource` check (error log + throw if the constant is mutated to an off-catalogue value).
- **UserDirectoryService stays.** ADR-0020's admin-list cache is functionally redundant with Person + User now, but folding it would extend the PR scope (DTO + reader + admin UI + migration of existing rows). Out of scope here — flagged as a follow-up.
## Local verification
- [x] `node scripts/check-catalogue-drift.mjs` — clean (`4 privileges, 24 roles, 7 structure kinds, 3 person sources`).
- [x] `node --test scripts/check-catalogue-drift.spec.mjs` — **29 tests passing** (was 22 — +7 for PERSON_SOURCES).
- [x] `pnpm exec nx lint portal-bff` — **0 errors**, 13 warnings (all pre-existing, none from this PR).
- [x] `pnpm exec nx test portal-bff` (filtered to the 6 affected spec files) — **766 tests passing**.
- [x] `pnpm exec prisma generate` — client regenerated with the 3 new models.
## Test plan — remaining (on a fresh dev DB)
- [ ] `./infra/local/dev.sh down -v && ./infra/local/dev.sh up` (wipe + reboot) then `cd apps/portal-bff && pnpm exec prisma migrate dev` — applies the new migration cleanly, no drift prompt.
- [ ] `pnpm exec prisma studio` — `persons` / `users` / `user_scopes` tables visible, all empty (no seed in this PR).
- [ ] Sign in via `apf-portal` against the test tenant — `users` table gets one row, `persons` table gets one row, `user_scopes` stays empty. Principal in the session carries the provisioned UUIDs (not the `entraOid`).
- [ ] **Review focus** — the provisioner's race-handling logic; the `Entra preferred_username → Person.email` mapping in `SessionEstablisher`; the ScopeResolver seam change rationale; the FK actions in the migration SQL (especially CASCADE on UserScope.userId vs RESTRICT on User.personId).
## What's next
- **ADR-0026 PR 2** — `PrismaScopeResolver` replacing `StubScopeResolver` + `/admin/users/:id/scopes` admin-app screen + `prisma/seed.ts` populating the 19 test personas' `user_scopes` per `notes/test-tenant-role-assignments.md` (referencing this PR's `User.id` and ADR-0027 PR 1's `Structure.code` values).
- **Future PR (out of this scope)** — fold `UserDirectoryEntry` into Person + User now that the latter exists. Touches AdminUsersReader, the DTO, the admin SPA, and a data migration for existing rows. Defer until ADR-0026 PR 2 stabilises.
---------
Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr>
Reviewed-on: #232
|
||
|
|
cba36394c9 |
fix(structures): widen via String() in narrowing test to satisfy lint (#231)
## Summary
One-line lint fix in [apps/portal-bff/src/structures/structure-kind.spec.ts](apps/portal-bff/src/structures/structure-kind.spec.ts) — the narrowing test failed CI lint with `@typescript-eslint/no-inferrable-types` on `const candidate: string = 'medico_social'`.
The naive fix (drop the annotation) would make the test vacuous: TypeScript would infer the literal type `'medico_social'`, which is already a `StructureKind` subtype, so the runtime guard `isStructureKind` would have nothing to prove. Fix instead: widen via `String('medico_social')` — the inferred return type is `string`, the narrowing check stays meaningful, the linter is happy.
## What lands
| File | Change |
| --- | --- |
| `apps/portal-bff/src/structures/structure-kind.spec.ts` | `const candidate: string = 'medico_social'` → `const candidate = String('medico_social')`. 3-line comment explains why `String()` rather than dropping the annotation. |
## Test plan
- [x] `pnpm exec nx lint portal-bff` — `0 errors, 13 warnings` (warnings all pre-existing in unrelated files).
- [x] `pnpm exec prettier --check` clean.
- [ ] **CI** — `pnpm ci:check` passes on this PR.
- [ ] **Review focus** — the comment block explaining the round-trip rationale (otherwise the `String('literal')` pattern reads like over-engineering).
## Notes for the reviewer
- **Pre-existing warnings are NOT addressed here.** The 13 remaining lint warnings (non-null assertions in `principal-extractor.spec.ts`, unused underscored params in `rate-limit.middleware.ts`, one stale eslint-disable in `downstream-token-cache.service.spec.ts`) are outside the scope of this PR — none of them block CI today, and folding them in would muddle the diff. Worth a separate `chore(bff): sweep lint warnings` PR if/when those become noisy.
- **Why `String()` over an `as string` cast?** Both would work, but `String()` is a real runtime operation (returns a fresh `string`) — `as string` is type-system-only. The runtime call has a tiny side-effect (and the inferred return type really IS `string`, not the literal), so the narrowing test stays semantically real.
- **No new error class.** The lint rule `@typescript-eslint/no-inferrable-types` is set to `error` severity in the workspace config; my narrowing test was just the first case to trip it.
---------
Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr>
Reviewed-on: #231
|
||
|
|
8d43717d25 |
fix(deps): update dependency @scalar/nestjs-api-reference to v1.1.19 (#225)
This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [@scalar/nestjs-api-reference](https://github.com/scalar/scalar) ([source](https://github.com/scalar/scalar/tree/HEAD/integrations/nestjs)) | dependencies | patch | [`1.1.16` -> `1.1.19`](https://renovatebot.com/diffs/npm/@scalar%2fnestjs-api-reference/1.1.16/1.1.19) | --- ### Release Notes <details> <summary>scalar/scalar (@​scalar/nestjs-api-reference)</summary> ### [`v1.1.19`](https://github.com/scalar/scalar/blob/HEAD/integrations/nestjs/CHANGELOG.md#1119) ### [`v1.1.18`](https://github.com/scalar/scalar/blob/HEAD/integrations/nestjs/CHANGELOG.md#1118) ### [`v1.1.17`](https://github.com/scalar/scalar/blob/HEAD/integrations/nestjs/CHANGELOG.md#1117) </details> --- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Enabled. ♻ **Rebasing**: Whenever PR is behind base branch, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box --- This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate). <!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0MC42Mi4xIiwidXBkYXRlZEluVmVyIjoiNDAuNjIuMSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOlsiZGVwZW5kZW5jaWVzIl19--> Reviewed-on: #225 Co-authored-by: APF Portal Bot <jgautier.webdev+apf-portal-bot@gmail.com> Co-committed-by: APF Portal Bot <jgautier.webdev+apf-portal-bot@gmail.com> |
||
|
|
b84b58068a |
refactor(users): rename Prisma User model to UserDirectoryEntry (#230)
## Summary
**Mechanical refactor only**, prerequisite to ADR-0026 PR 1. Renames the existing Prisma `User` model (the ADR-0020 user-directory cache, `oid` PK, written by `UserDirectoryService.recordSignIn`) to `UserDirectoryEntry`. Frees the `User` name for the upcoming ADR-0026 model (UUID PK, FK to `Person`, `lastSignInAt` — different semantics).
Zero behavioural change. Same columns, same indexes, same constraints, same call sites — just a different identifier on the model and the table.
## What lands
| File | Change |
| --- | --- |
| `apps/portal-bff/prisma/schema.prisma` | `model User` → `model UserDirectoryEntry`. `@@map("users")` → `@@map("user_directory_entries")`. Comment block extended to flag the upcoming distinction from ADR-0026's new `User`. |
| `apps/portal-bff/prisma/migrations/20260526200000_rename_users_to_user_directory_entries/migration.sql` | **New**. `ALTER TABLE "users" RENAME TO "user_directory_entries"` + `ALTER INDEX` renames for the PK constraint and the two named indexes (Postgres doesn't auto-rename these on table rename). |
| `apps/portal-bff/src/users/user-directory.service.ts` | Two renames: the **TS input interface** `UserDirectoryEntry` → `RecordSignInInput` (the existing name was a misnomer — it's the input to `recordSignIn`, not the entry itself, and would collide with the Prisma-generated `UserDirectoryEntry` type after the model rename). The **Prisma client ref** `this.prisma.user.upsert` → `this.prisma.userDirectoryEntry.upsert`. |
| `apps/portal-bff/src/users/user-directory.service.spec.ts` | Imports / mock setup / fixture type updated to track the two renames. |
| `apps/portal-bff/src/admin/admin-users-reader.service.ts` | `this.prisma.user.{count,findMany}` → `this.prisma.userDirectoryEntry.{count,findMany}`. `Prisma.UserWhereInput` → `Prisma.UserDirectoryEntryWhereInput`. Doc comments mentioning `public.users` updated to `public.user_directory_entries`. The class name `AdminUsersReader`, the endpoint URL `/api/admin/users`, the DTO `AdminUserDto`, and the local `interface UserRow` are unchanged — these are SPA/HTTP-facing identifiers, where the URL semantics ("admin user list") still hold regardless of the backing table name. |
| `apps/portal-bff/src/admin/admin-users-reader.service.spec.ts` | Mock setup updated to track the Prisma client field rename. |
## Why two renames in one file
`apps/portal-bff/src/users/user-directory.service.ts` had a TypeScript `interface UserDirectoryEntry` carrying the **input shape** of `recordSignIn(entry: UserDirectoryEntry)`. After the Prisma model rename to `UserDirectoryEntry`, that name would clash with the Prisma-generated row type. The fix is to rename the TS interface to its proper role — `RecordSignInInput` — at the same time. Net effect: clearer naming on both sides (the call-input name now describes the call, the persisted-row type name now describes the row).
## Recovery procedure (for anyone with the old migration applied locally)
The new migration `20260526200000_rename_users_to_user_directory_entries` is a pure `ALTER TABLE ... RENAME` + index renames — Prisma's `migrate dev` runs it forward without prompting:
```bash
cd apps/portal-bff && pnpm exec prisma migrate dev
# Should report: "Applied migration `20260526200000_rename_users_to_user_directory_entries`"
```
No `down -v` needed — existing data carries over.
## Test plan
- [x] `node scripts/check-catalogue-drift.mjs` — clean (4 / 24 / 7).
- [x] `node --test scripts/check-catalogue-drift.spec.mjs` — 22 tests passing.
- [x] `pnpm exec prettier --check` clean on the touched files.
- [x] Sweep grep — zero leftover `model User`, `prisma.user.`, `Prisma.UserWhereInput`, `interface UserDirectoryEntry`, or `public.users` references anywhere under `apps/portal-bff/src/` or `apps/portal-bff/prisma/`.
- [ ] **Locally**: `pnpm exec prisma migrate dev` applies the rename migration cleanly; `pnpm exec nx test portal-bff` runs the updated specs green.
- [ ] **Review focus** — the two-rename rationale in `user-directory.service.ts`, the migration's `ALTER INDEX` clauses (don't forget those — `ALTER TABLE ... RENAME` does NOT cascade to index names in Postgres), the unchanged class/URL/DTO/local-interface identifiers in `admin-users-reader.service.ts`.
## Why ship as a separate PR
ADR-0026 PR 1's nominal scope is `Person` + `User` + `UserScope` + provisioner + drift gate + PrincipalBuilder — already ~15+ files touched. Folding the rename into that PR would mix mechanical refactor with new design. Splitting keeps both PRs reviewable for what they actually do.
## What's next (post-merge)
**ADR-0026 PR 1** — `Person` + new `User` + `UserScope` schema + `PersonAndUserProvisioner` wired into `SessionEstablisher` + `Person.source` catalogue + drift-gate extension + `PrincipalBuilder` populating `Principal.user.{id, personId}` from real rows. Now unblocked.
---------
Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr>
Reviewed-on: #230
|
||
|
|
ff1713eb6d |
fix(structures): align Structure.delegation_code FK action with Prisma default (#229)
## Summary Single-line fix on the `add_org_hierarchy` migration shipped in [#228](#228) (ADR-0027 PR 1). The FK action on `Structure.delegation_code` was `ON DELETE RESTRICT` (Prisma's default for **required** relations); the field is **optional** (`Structure.delegation Delegation?`), so Prisma's default is `ON DELETE SET NULL`. Mismatch caused `prisma migrate dev` to detect drift and prompt for a corrective migration on every run. Caught locally on first VM-side validation (the workspace dev DB). No production deployment of #228 yet, so the fix is **edit-in-place** rather than a corrective sibling migration — keeps the repo's migration history clean. ## What lands | File | Change | | --- | --- | | `apps/portal-bff/prisma/migrations/20260526143000_add_org_hierarchy/migration.sql` | `ON DELETE RESTRICT` → `ON DELETE SET NULL` on the `structures_delegation_code_fkey` FK. Inline comment explains the rule (nullable Prisma relation → SET NULL is the matching default; not matching it generates drift on every `migrate dev`). | That's it. One line of SQL, one comment block. No schema.prisma change, no test change, no other file touched. ## Why edit-in-place vs new corrective migration Edit-in-place is safe here because: - The broken migration only exists in **dev local DBs** (Julien's WSL postgres). No staging, no preview env, no prod has applied it. - Every dev who pulls this PR's fix wipes their local DB (`./infra/local/dev.sh down -v`) and reapplies — clean migration history, no "modified after applied" warning. - The repo's migration list stays minimal — adding a corrective migration would carry the wart forever in `prisma/migrations/`. Once a non-dev environment has applied a migration, the rule reverses: corrective migration mandatory, never edit in place. ADR-0015's "trunk-based + squash-merge" and the absence of a deployed environment at this stage gives us this one-time window. ## Recovery procedure for anyone who applied #228 ```bash # From the workspace root ./infra/local/dev.sh down -v # wipe the local postgres volume ./infra/local/dev.sh up # Pull this fix git pull # or git switch fix/adr-0027-pr1-fk-action-on-delete depending on local state # Reapply migrations — no prompt this time cd apps/portal-bff && pnpm exec prisma migrate dev # Should report: "Applied migration `20260526143000_add_org_hierarchy`" and exit cleanly. ``` If anyone has a stray `drift_inspection/` folder under `prisma/migrations/` (artifact of the `--create-only` debugging step), delete it before reapplying — it's not in the repo, it was just diagnostic output. ## Test plan - [x] `node scripts/check-catalogue-drift.mjs` — clean (unchanged by this fix). - [x] `node --test scripts/check-catalogue-drift.spec.mjs` — 22 tests passing (unchanged). - [x] `pnpm exec prettier --check` clean (SQL file unaffected by prettier but verified). - [ ] **Locally on WSL** — wipe DB → pull fix → `prisma migrate dev` exits clean, no drift prompt. - [ ] **Review focus** — the comment block in the migration explaining the rule (future-proof against the same mistake when ADR-0026 PR 1 adds optional relations). ## Notes for the reviewer - **Why a comment block on the FK line, not on every nullable FK in future migrations?** This was caught the first time we hit it; a short note at the offending site is cheaper than amending `CLAUDE.md` with a "remember to match Prisma default actions" rule. If we hit the same mistake on ADR-0026 PR 1's `Person`/`User`/`UserScope` migration, we'll consider promoting it. For now: one inline comment at the place where the rule is non-obvious. - **The drift gate doesn't catch this kind of mistake.** Catalogue drift gate scans string literals against TypeScript catalogues — it doesn't look at SQL referential actions. Postgres CHECK constraints (introduced for `Structure.kind` in this same migration) are not the same surface as FK referential actions. Catching this required actually running `prisma migrate dev`. A possible future improvement: a CI gate that runs `prisma migrate diff --from-migrations --to-schema-datamodel --script --shadow-database-url …` and fails if the diff is non-empty (zero-drift gate). Worth an ADR amendment if it becomes a recurring class of bug. --------- Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr> Reviewed-on: #229 |
||
|
|
ba4cdcee7a |
feat(structures): add Region/Delegation/Structure schema + seed (ADR-0027 PR 1) (#228)
## Summary
First implementation PR for [ADR-0027](docs/decisions/0027-portal-side-organisational-hierarchy.md) (`Region` / `Delegation` / `Structure` portal-side organisational hierarchy). **Schema + seed + drift-gate extension only** — no consumer code yet (the `PrismaScopeResolver` that dereferences `Structure.code` from `UserScope.value` lives in ADR-0026 PR 2, which depends on this PR landing first).
Independent of ADR-0026 PR 1 at the schema level — both can ship in parallel; ADR-0026 PR 2 depends on both.
## What lands
| File | Change |
| --- | --- |
| `apps/portal-bff/prisma/schema.prisma` | **+3 models**. `Region` (INSEE code PK + name + delegations[]). `Delegation` (dept code PK + name + regionCode FK + structures[]). `Structure` (portal code PK + name + kind discriminator + nullable unique finess/siret/codePaie + nullable delegationCode FK). All in the `public` schema; matches the ADR-0027 schema sketch verbatim. |
| `apps/portal-bff/prisma/migrations/20260526143000_add_org_hierarchy/migration.sql` | **New** hand-written migration. DDL for the 3 tables. `CHECK ("kind" IN (...))` constraint mirroring `STRUCTURE_KINDS`. Indexes (FK columns + kind + uniques). Inline INSERT seed: Région Nouvelle-Aquitaine (75), Délégation Gironde (33), structures `0330800013` + `0330800021` (médico-social, FINESS = code) + `siege` (APF national, no delegation/finess). |
| `apps/portal-bff/src/structures/structure-kind.ts` | **New**. Closed-set catalogue: `STRUCTURE_KINDS = [medico_social, antenne, dispositif, entreprise_adaptee, mouvement, administratif, siege] as const`. `type StructureKind` derived from the union, `isStructureKind` type guard. |
| `apps/portal-bff/src/structures/structure-kind.spec.ts` | **New**. Jest spec — catalogue content, no duplicates, type guard true for catalogue values + false for typos / cascade-only values / empty, type narrowing at call site. |
| `scripts/check-catalogue-drift.mjs` | **Extend**. New `extractStructureKinds(path)` + `findStructureKindViolationsInFile(path, validKinds, sourceText?)`. Property-literal scanner: detects `kind: 'X'` in object literals, restricted to files that import from `structure-kind.ts` (cheap text pre-filter — `kind` is a common property name on unrelated objects and we'd false-positive everywhere otherwise). Integrated into `scanWorkspace`. Error-message formatter switched on callee shape (`@Foo('x')` for decorators, `kind: 'x'` for property literals). Closing hint updated to reference both ADR-0025 and ADR-0027 catalogue locations. |
| `scripts/check-catalogue-drift.spec.mjs` | **Extend**. Fixture writes a synthesised `structure-kind.ts` alongside `authorization.types.ts`. New tests: extract STRUCTURE_KINDS, throw on missing constant, skip files without the import, no-violation on catalogue values, flag off-catalogue values, skip non-literal initialisers, line/column tracking, scanWorkspace aggregation (decorator + Structure.kind together), scanWorkspace exposes the structureKinds set. **22 tests total, all passing.** |
## Defense in depth
Three layers stack for `Structure.kind`, deliberately:
1. **TypeScript type union** `StructureKind` — compile-time check at every typed assignment.
2. **Postgres `CHECK` constraint** in the migration — runtime enforcement at INSERT / UPDATE, defends raw SQL / casts / untrusted API input.
3. **`scripts/check-catalogue-drift.mjs`** — CI gate asserting every `kind: 'X'` literal in structure-context files is in the catalogue.
The `Privilege` / `FunctionalRole` catalogues from ADR-0025 only have layer 1 + layer 3 (no DB enforcement — those values aren't persisted as schema-checked columns). ADR-0027's `Structure.kind` is persisted, so layer 2 was practical to add — bulletproof against any code path that bypasses the type system.
## Seed scope (and what's deliberately NOT in it)
Just what the 19 test-tenant personas reference per `notes/test-tenant-role-assignments.md`:
- `Region` 75 Nouvelle-Aquitaine (only region the personas exercise)
- `Delegation` 33 Gironde (only delegation)
- `Structure` 0330800013 (APF Bordeaux, medico-social, FINESS = code)
- `Structure` 0330800021 (Complexe Mérignac, medico-social)
- `Structure` `siege` (APF national, kind=siege, no FK to a delegation, no FINESS)
**No** placeholder `entreprise_adaptee`, `antenne`, or `dispositif` row — those kinds are valid per the catalogue but the test tenant doesn't exercise them, and adding gold-plate seed data would be misleading ("what is this row used for?"). The full APF inventory lands with [ADR-0029](#)'s cascade sync; this seed is **superseded** (not extended) by that sync.
## Notes for the reviewer
- **Naming conflict with the existing `User` model.** The current `schema.prisma` already has a `User` model — but it's the ADR-0020 user-directory cache (Entra `oid` as PK, written by `UserDirectoryService.recordSignIn`). ADR-0026 PR 1 introduces a different `User` (UUID PK, FK to `Person`, `lastSignInAt`). **Out of scope here** — ADR-0027 PR 1 doesn't touch `User`. Flagging now so ADR-0026 PR 1 can plan the migration path (likely: rename the existing `User` to `UserDirectoryEntry` or fold it into the new Person + User pair).
- **Migration is hand-written**, matching the style of the two existing migrations (`init_audit_schema`, `users_directory`). Timestamp `20260526143000` chosen so it sorts after the existing `20260514192014_users_directory`.
- **`@@schema("public")`** required on every new model because the audit log uses `multiSchema` (per ADR-0013) — the public/audit split is configured at the datasource.
- **Drift gate error-message formatter** now switches on callee shape — decorator violations still print as `@Foo('x')`, property-literal violations print as `kind: 'x'` to match the offending code shape.
- **No `pnpm ci:check` impact expected** at the bff level beyond the new spec; `pnpm ci:catalogue-drift` continues to report clean (`catalogues: 4 privileges, 24 roles, 7 structure kinds`).
## Test plan
- [x] `node scripts/check-catalogue-drift.mjs` — clean (4 / 24 / 7).
- [x] `node --test scripts/check-catalogue-drift.spec.mjs` — 22 tests passing.
- [x] `pnpm exec prettier --check` clean on the touched files.
- [ ] **On the dev VM**: `pnpm prisma migrate dev` applies the migration cleanly against a fresh `infra/local/dev.compose.yml` postgres. `pnpm prisma studio` shows the seeded Region / Delegation / Structure rows.
- [ ] **On the dev VM**: `pnpm exec nx test portal-bff` runs the new `structure-kind.spec.ts` green.
- [ ] **Review focus** — the `Structure` model shape (kind discriminator, nullable unique columns, FK to Delegation), the inline seed values vs `notes/test-tenant-role-assignments.md`, the drift-gate property-literal scanner's restriction to files importing from `structure-kind.ts`.
## What's next
Per [ADR-0027 §"Phasing"](docs/decisions/0027-portal-side-organisational-hierarchy.md):
1. **This PR** — Region / Delegation / Structure schema + seed + drift gate. ✅
2. **ADR-0026 PR 1** — `Person` / `User` / `UserScope` schema + `PersonAndUserProvisioner` + drift gate extension for `Person.source` + updated `PrincipalBuilder`. Independent of (1), can ship in parallel. **Needs to resolve the existing-`User`-name collision.**
3. **ADR-0026 PR 2** — `PrismaScopeResolver` replacing `StubScopeResolver` + `/admin/users/:id/scopes` admin screen + `prisma/seed.ts` populating the 19 personas' `user_scopes` rows pointing at this PR's `Structure.code` values. **Depends on both (1) and (2).**
4. **ADR-0029** (future) — Pléiades + Acteurs+ + cascade syncs + facet schemas + `Pole` / `Service` / per-source enrichment extensions to this PR's hierarchy.
---------
Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr>
Reviewed-on: #228
|
||
|
|
670f6303fe |
docs(adr-0028): accept CI/CD + git hosting migration to GitLab (#227)
## Summary Promotes [ADR-0028](docs/decisions/0028-migrate-cicd-and-git-hosting-to-gitlab.md) from `proposed` to `accepted`. Shipped as `proposed` in [#226](#226); no open question left on either drivers, considered options, or the 4-phase migration sequence. Same shape as [#219](#219) (ADR-0026 + ADR-0027 acceptance). Once merged, **Phase 1** of the migration (`mirror-and-bootstrap` — repos pushed to GitLab, branch protection / MR templates / deploy keys / Renovate reconfig) is unblocked. ## What lands | File | Change | | --- | --- | | `docs/decisions/0028-migrate-cicd-and-git-hosting-to-gitlab.md` | Frontmatter `status: proposed → accepted`. | | `docs/decisions/0015-cicd-gitea-actions.md` | **Status-update note** added at the top, right under the title — calls out that the platform choice "Gitea Actions" is superseded by ADR-0028, while the rest of ADR-0015's architectural principles (trunk-based + squash, all-gates-blocking, thin YAML, on-prem runners, signed commits, Conventional Commits) carry over unchanged. ADR-0015 stays `accepted`. | | `docs/decisions/README.md` | ADR-0028 row: `proposed → accepted`. | | `CLAUDE.md` | Roll-up bumped to `ADRs 0001 → 0028 accepted` with a one-sentence explanation that ADR-0028 supersedes only ADR-0015's platform choice. **CI/CD architecture bullet rewritten** to reflect the accepted-but-not-yet-implemented state — "Gitea Actions today, migrating to GitLab CE on `vm-gitlab` per ADR-0028 (4-phase rollout in follow-up PRs)". The architectural detail (gates list, thin YAML, signed commits) was preserved; only the platform headline and the act_runner→GitLab Runner line are touched. Also corrected: `ci:scan` (which doesn't exist as a script) → the real script names (`ci:catalogue-drift`, `ci:audit`, `ci:perf`, `ci:gzip-budgets`). | ## Notes for the reviewer - **No new Architecture bullet for ADR-0028 itself.** The decision is a *platform shift* for the existing CI/CD architecture, not a new architectural concern — so it folds into the existing ADR-0015 bullet rather than adding a sibling. - **The annotation pattern on ADR-0015** (status-update blockquote at the top) is the canonical MADR way to handle partial supersession without changing the frontmatter status. The architectural principles are still accepted; only the platform implementation moves. A future reader hitting ADR-0015 first sees the redirect immediately. - **The CLAUDE.md script-list correction** is a side-fix — `ci:scan` is not a real script name; the actual gates are `ci:check`, `ci:catalogue-drift`, `ci:audit`, `ci:commits`, `ci:perf`, `ci:gzip-budgets`. Updated in the same touch since the bullet was being rewritten anyway. - **No code changes**, so no `pnpm ci:check` impact. `pnpm exec prettier --check` clean on the four touched files. ## Test plan - [x] `pnpm exec prettier --check` — clean on the four touched files. - [x] ADR-0015 → ADR-0028 cross-reference resolves (the new blockquote link). - [x] `ADRs 0001 → 0028 accepted` matches reality (`grep '^status: ' docs/decisions/*.md` shows everything below 0029 as `accepted`). - [ ] **Review focus** — the ADR-0015 status-update note phrasing, the CLAUDE.md CI/CD bullet rewrite (especially the "carry over" wording), the roll-up sentence about partial supersession. ## What's next (post-merge) 1. **Phase 1 — `mirror-and-bootstrap`** — `git push --mirror gitlab` for `apf_portal` and the proto vendoring in `apf-ai-service`. GitLab side: groups, projects, branch protection (mirror Gitea's), MR templates, deploy keys, Renovate reconfigured for GitLab. **No `.gitlab-ci.yml` yet** — Gitea pipelines continue to gate. Ops work primarily; the only PR-shaped output is a Renovate config update on the apf-portal repo if its host detection changes. 2. **Phase 2 — `gitlab-ci-pipeline`** — `.gitlab-ci.yml` lands alongside `.gitea/workflows/ci.yml`. Both pipelines run in parallel for ~1 calendar week. GitLab Runner registered on `vm-gitlab`. 3. **Phase 3 — `cutover`** — remotes flip in CLAUDE.md, READMEs, `docs/setup/01-dev-debian-vm-setup.md` §8.3. `.gitea/workflows/` + `infra/ci-runners.compose.yml` deleted, Gitea read-only / archive. 4. **Phase 4 — `cleanup`** — stale references sweep, required signed-commits on `main` enabled. In parallel — once you've finished walking through the dev VM bootstrap — the paused **ADR-0027 PR 1** (Region / Delegation / Structure Prisma schema + inline seed) and **ADR-0026 PR 1** (Person / User / UserScope schema + provisioner) can ship on Gitea; they're decoupled from the migration and don't need to wait for it. --------- Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr> Reviewed-on: #227 |
||
|
|
04ee535de9 |
docs(adr-0028): propose CI/CD + git hosting migration Gitea -> GitLab (#226)
## Summary
Drafts [ADR-0028](docs/decisions/0028-migrate-cicd-and-git-hosting-to-gitlab.md) as `proposed`: migrate CI/CD + git hosting from Gitea (`git.unespace.com`) to GitLab CE self-hosted on `vm-gitlab` (`10.100.201.10`). The migration was anticipated by [ADR-0015](docs/decisions/0015-cicd-gitea-actions.md) ("level-2 implementation; will be superseded by a GitLab migration ADR within 6-18 months") — that window opens now. **Decision-only PR** — the actual 4-phase migration ships across follow-up PRs after acceptance.
ADR-0028's number was previously a placeholder reference in ADR-0026 and ADR-0027 for the Pléiades + Acteurs+ sync ADR. **Renumbering**: that future sync ADR shifts to `ADR-0029`, and the placeholder links in ADR-0026, ADR-0027 and `CLAUDE.md` update to match — included in the same PR so the chain stays consistent.
## What lands
| File | Change |
| --- | --- |
| `docs/decisions/0028-migrate-cicd-and-git-hosting-to-gitlab.md` | **New.** MADR 4.0.0 ADR, `proposed`. Decision = Option B (GitLab CE on `vm-gitlab`). Considered options A (status quo Gitea), C (Forgejo), D (cloud SaaS). Documents what carries over from ADR-0015 (architectural principles unchanged — thin YAML, trunk-based, all-gates-blocking, on-prem runners), what changes (host, pipeline file, runner type, scan tooling), the 4-phase migration sequence, and the signed-commits revisit. |
| `docs/decisions/0026-person-user-portal-data-model.md` | All 11 `ADR-0028` references → `ADR-0029` (sync + facets shifts to 0029). |
| `docs/decisions/0027-portal-side-organisational-hierarchy.md` | All 14 `ADR-0028` references → `ADR-0029`. |
| `docs/decisions/README.md` | New row for ADR-0028 (`proposed`, tags `infrastructure`, `process`, 2026-05-26). |
| `CLAUDE.md` | Roll-up updated: `ADRs 0001 → 0027 accepted; ADR-0028 + ADR-0029 proposed`. ADR-0028's relationship to ADR-0015 spelled out inline ("supersedes ADR-0015's Gitea Actions platform choice — the rest of ADR-0015's architectural principles carry over unchanged"). ADR-0026 + ADR-0027 architecture bullets renumbered 0028 → 0029 to track. |
## Key choices in the ADR
- **What carries over from ADR-0015 vs what changes** — explicit table so future readers see immediately that the migration is **platform-only**, not a re-litigation of CI principles. Trunk-based + squash, all-gates-blocking, thin YAML over portable scripts (`pnpm ci:check` etc. — unchanged), on-prem runners, Conventional Commits in CI + hook (defense in depth) — all carried over. Host, pipeline-file grammar, runner type, and scan tooling are the only things that move.
- **Native security scanning replaces the manual Trivy + gitleaks setup.** GitLab CE's built-in `Dependency-Scanning.gitlab-ci.yml` + `Secret-Detection.gitlab-ci.yml` includes consolidate the ~30 lines of inline `curl + tar` install dance currently in `.gitea/workflows/ci.yml`. Same blocking thresholds (CRITICAL+HIGH dependency vulns, any secret).
- **`vm-gitlab` is already provisioned.** No infra wait — the only sequencing constraint is operator-driven, not infrastructure-driven.
- **4-phase migration, parallel pipelines for ~1 week before cutover.** Phase 1 mirrors repos and bootstraps GitLab side (no `.gitlab-ci.yml` yet — Gitea still gates). Phase 2 lands `.gitlab-ci.yml` alongside `.gitea/workflows/ci.yml` so both pipelines run per PR until parity is confirmed. Phase 3 flips the remote URLs and deletes `.gitea/workflows/` + `infra/ci-runners.compose.yml`. Phase 4 sweeps stale references.
- **Gitea moves to read-only / archive, not decommissioned** at cutover. Existing references to Gitea PRs (`#213`, `#217`, `#219`, …) in commit messages and ADR bodies stay resolvable as historical artefacts. One VM at idle is a low long-term cost.
- **Signed commits revisit.** ADR-0015 noted "signed commits recommended, revisited at GitLab migration". ADR-0028 makes the recommendation: enable required signed commits on `main` once GitLab is live, paired with GnuPG agent forwarding (already documented in `docs/setup/01-dev-debian-vm-setup.md` §8.5). `apf-portal-bot` (Renovate) gets a dedicated signing key at PR 1.
## Renumbering — what moved and why
Before this PR, ADR-0026 and ADR-0027 used `ADR-0028` as a placeholder link for the Pléiades + Acteurs+ sync ADR. That sync ADR hasn't been drafted yet — the number was reserved.
This PR claims `ADR-0028` for the GitLab migration (the immediately-actionable decision), and shifts the sync placeholder to **ADR-0029**. All 25 link references across ADR-0026 (11) and ADR-0027 (14) update in lockstep — replace_all is safe here because in those files `ADR-0028` consistently meant "the sync ADR".
Content of the sync ADR is unchanged — only the number. When that ADR is eventually drafted as `0029-…md`, it gets the existing content reserved for it in the placeholder text.
## Test plan
- [x] `pnpm exec prettier --check` clean on the touched files.
- [x] All `ADR-0028` references in `0026-…md` / `0027-…md` / `CLAUDE.md` now read `ADR-0029` (grep confirms zero remaining references to the old number in those files).
- [x] The new `0028-…md` self-references (status frontmatter, title, internal anchors) are consistent — no leftover `0029`.
- [ ] **Review focus** — drivers / consequences / migration sequence in the ADR; the "what carries over from ADR-0015" table; the renumbering rationale.
## What's next
Per ADR-0028 §"Migration sequence", post-acceptance:
1. **ADR-0028 acceptance PR** — small status-flip, same pattern as #219 (ADR-0026 + ADR-0027 acceptance).
2. **`mirror-and-bootstrap` PR** — `git push --mirror` Gitea → GitLab; GitLab side groups / projects / branch protection / MR templates / deploy keys / Renovate reconfig. No `.gitlab-ci.yml` yet, Gitea pipelines still gate.
3. **`gitlab-ci-pipeline` PR** — `.gitlab-ci.yml` alongside the existing `.gitea/workflows/ci.yml`. Parallel runs ~1 week for parity. GitLab Runner registered on `vm-gitlab`.
4. **`cutover` PR** — remotes flip across docs, `.gitea/workflows/` + `infra/ci-runners.compose.yml` deleted, Gitea read-only.
5. **`cleanup` PR** — stale references sweep, signed-commit policy finalised on `main`.
In parallel — once the dev VM (#220 / #221 / #222 / #223 / #224) is fully bootstrapped — the paused **ADR-0027 PR 1** (Region / Delegation / Structure Prisma schema + inline seed) and **ADR-0026 PR 1** (Person / User / UserScope schema + provisioner) can ship on Gitea; they have no dependency on the GitLab migration and don't need to wait.
---------
Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr>
Reviewed-on: #226
|
||
|
|
72e1182308 |
docs(setup): version aliases.zsh + gitconfig.txt under docs/setup/dotfiles (#224)
## Summary Follow-up on [#220](#220). [`80-dotfiles.sh`](docs/setup/scripts/80-dotfiles.sh) was reading `notes/aliases.zsh` and `notes/gitconfig.txt` — but `notes/` is the project lead's personal scratchpad and is gitignored, so a fresh clone on the dev VM has nothing to read: ``` ✗ /home/APF/dev-jugautier/Works/apf_portal/notes/aliases.zsh not found. ``` Move both templates to a versioned location and update the script + docs to match. ## What lands | File | Change | | --- | --- | | `docs/setup/dotfiles/aliases.zsh` | **New.** Project-wide zsh aliases (navigation / files / search / git / dev), previously living in `notes/aliases.zsh`. Header explains scope (project-wide, all devs pick it up on next zsh restart) and where per-dev customizations go (private dotfiles repo). | | `docs/setup/dotfiles/gitconfig.txt` | **New.** Base `~/.gitconfig` template — init / core / aliases / colour. `[user]` block carries placeholder identity (`name = your name` / `email = your.email@example.com`) that the script overwrites at install via `git config --global user.{name,email}`. | | `docs/setup/scripts/80-dotfiles.sh` | Source paths flipped from `$REPO_ROOT/notes/…` to `$REPO_ROOT/docs/setup/dotfiles/…`. Header doc-comment updated. | | `docs/setup/01-dev-debian-vm-setup.md` | Step-3 table row and §8.4 (dotfiles repo) reference the new path. The §8.4 wording is also tightened — the script no longer "falls back" anywhere, it has one source of truth. | | `docs/setup/README.md` | New `dotfiles/` section in the folder index. Step-7 row in the scripts table also reflects the new paths. | ## Why this lives in `docs/setup/dotfiles/` and not `infra/` or a top-level `dotfiles/` - The directory is **consumed exclusively by [`80-dotfiles.sh`](docs/setup/scripts/80-dotfiles.sh)** — co-locating it under `docs/setup/` keeps the setup family self-contained (one folder to read, one folder to clone). - The name `dotfiles/` reads correctly for a future per-dev dotfiles repo migration (§8.4) — the templates here become the seed of that repo, and the install script will then check `~/.dotfiles/` first and fall back to `docs/setup/dotfiles/` second. ## Unblock path (if you're stuck mid-bootstrap) Either pull this PR, or manually create the two files at the **new** location on the VM: ```bash mkdir -p ~/Works/apf_portal/docs/setup/dotfiles # paste the aliases.zsh + gitconfig.txt content there ./docs/setup/scripts/80-dotfiles.sh ``` ## Test plan - [ ] On a fresh Trixie VM, `./docs/setup/scripts/80-dotfiles.sh` succeeds: aliases.zsh symlink lands at `~/.oh-my-zsh/custom/aliases.zsh` pointing at `docs/setup/dotfiles/aliases.zsh`, `~/.gitconfig` written with the prompted identity. - [ ] Re-running the script reports `↪ skip aliases.zsh symlink already correct` (idempotency). - [ ] `git config --global user.name` returns the prompted value (i.e. placeholder `your name` is overwritten). - [x] `pnpm exec prettier --check` clean on the touched files. - [x] `bash -n docs/setup/scripts/80-dotfiles.sh` clean. ## Notes - `notes/aliases.zsh` and `notes/gitconfig.txt` on existing dev workstations are unaffected — those files live outside the repo (gitignored), nothing here touches them. They can be deleted or kept as personal scratch at the dev's discretion. - A future PR creates the **private dotfiles repo** (`apf/dotfiles`) and teaches `80-dotfiles.sh` to prefer `~/.dotfiles/` over `docs/setup/dotfiles/` when both exist. --------- Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr> Reviewed-on: #224 |
||
|
|
1d11db9f36 |
docs(setup): drop deprecated apt packages on Debian 13 trixie (#223)
## Summary Follow-up on [#220](#220). [`10-base-packages.sh`](docs/setup/scripts/10-base-packages.sh) fails on a fresh Debian 13 Trixie VM with `E: Impossible de trouver le paquet software-properties-common`. The package is no longer shipped on Trixie — and it was never used by any of our downstream scripts in the first place. Drop three deprecated / unused packages from the install list. Add an inline comment explaining each kept package's purpose so a drive-by addition doesn't reintroduce the dropped ones. ## What lands | File | Change | | --- | --- | | `docs/setup/scripts/10-base-packages.sh` | Drop `software-properties-common`, `apt-transport-https`, `lsb-release`. Remaining minimal set: `curl wget git ca-certificates gnupg build-essential pkg-config`. Inline comment lists which downstream script consumes each kept package + which three were deliberately removed and why. | | `docs/setup/01-dev-debian-vm-setup.md` | Step-3 table row for `10-base-packages.sh` updated to match the new package list. | ## Why each removed package wasn't needed | Package | Why removed | | --- | --- | | `software-properties-common` | Drops `add-apt-repository`. We don't use it — [`50-docker.sh`](docs/setup/scripts/50-docker.sh) writes `/etc/apt/sources.list.d/docker.list` manually with the keyring-pinned `signed-by=` form. Also no longer in Trixie. | | `apt-transport-https` | Transitional package since apt 1.5 (2018) — apt has native HTTPS. Intermittently absent on Trixie. | | `lsb-release` | Shell scripts read `/etc/os-release` directly (see `50-docker.sh`'s `. /etc/os-release && echo "${VERSION_CODENAME}"`). | ## Unblock path (manual, if you're stuck mid-bootstrap) The user can either pull this PR and re-run, or shortcut manually: ```bash sudo apt-get install -y curl wget git ca-certificates gnupg build-essential pkg-config ./docs/setup/scripts/20-zsh.sh # continue from the next step ``` Scripts are idempotent — re-running `bootstrap.sh` after pulling this PR is also safe. ## Test plan - [ ] On a fresh Trixie VM, `./docs/setup/scripts/10-base-packages.sh` exits successfully. - [ ] Verify each downstream script still has the binaries it expects (curl in 40-node.sh, gpg in 50-docker.sh, build-essential in pnpm install, …). - [x] `pnpm exec prettier --check` clean on the touched files. - [x] `bash -n docs/setup/scripts/10-base-packages.sh` clean. --------- Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr> Reviewed-on: #223 |
||
|
|
d99254a280 |
docs(setup): make fail2ban opt-in in 70-hardening.sh (#222)
## Summary Follow-up on [#220](#220) / [#221](#221). Makes fail2ban **opt-in** in [`70-hardening.sh`](docs/setup/scripts/70-hardening.sh) instead of installing it unconditionally. Reasoning: some corp environments already ship brute-force protection at the network layer (ACL / corp firewall / appliance) — fail2ban on the host then becomes redundant and can be the wrong layer to debug from when a rule misfires. The other three hardening steps (UFW enable, sshd lockdown) were already prompt-gated; fail2ban was the odd one out. ## What lands | File | Change | | --- | --- | | `docs/setup/scripts/70-hardening.sh` | fail2ban block restructured into three branches: (1) already running → skip; (2) installed but stopped → prompt to enable+start; (3) not installed → prompt to install+enable+start. Each "no" path logs `↪ skip (user choice)` so re-runs don't repeatedly nag if the dev has already declined. | | `docs/setup/01-dev-debian-vm-setup.md` | Table row for `70-hardening.sh` clarified — each sub-step's prompt posture is now visible: UFW prompts before enabling, fail2ban prompts before installing, sshd hardening prompts before applying. `unattended-upgrades` is the only one applied unconditionally. | | `docs/setup/README.md` | Same descriptor adjustment. | ## Test plan - [ ] On a fresh Debian VM with no fail2ban installed, run `70-hardening.sh`, decline the fail2ban prompt → script continues, fail2ban not installed, no service started. - [ ] On the same VM, re-run `70-hardening.sh` → the fail2ban branch prompts again (the dev may have changed their mind); declining again produces the same `↪ skip (user choice)` result. - [ ] On a VM where fail2ban is pre-installed but stopped (rare, but possible if infra rolled it back), the script offers to start it without re-installing. - [x] `pnpm exec prettier --check` clean on the touched files. - [x] `bash -n docs/setup/scripts/70-hardening.sh` (syntax check) clean. --------- Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr> Reviewed-on: #222 |
||
|
|
c77313c693 |
docs(setup): use ~/.bashrc hand-off instead of chsh for zsh switch (#221)
## Summary Follow-up fix on [#220](#220). The corp infra locks the default shell at user-provisioning time on the dev VM (`10.100.201.21`) — `chsh` is denied at the PAM level, so the `sudo chsh -s` block in [`20-zsh.sh`](docs/setup/scripts/20-zsh.sh) fails on every fresh VM. Switch to the `~/.bashrc` exec-zsh hand-off — the same proven pattern used in the legacy [`02-wsl-terminal-setup.md`](docs/setup/02-wsl-terminal-setup.md). UX-identical for the dev, zero infra escalation required. ## What lands | File | Change | | --- | --- | | `docs/setup/scripts/20-zsh.sh` | Drop the `sudo chsh -s` block. Append a guarded `exec zsh -l` block to `~/.bashrc` instead, marked with a managed-by comment for idempotency on re-runs. | | `docs/setup/01-dev-debian-vm-setup.md` | Table row for `20-zsh.sh` updated — "set as default shell" → "`~/.bashrc` (exec zsh on interactive shells — `chsh` is blocked on the corp VM)". | | `docs/setup/README.md` | Same descriptor adjustment. | ## The hand-off block ```bash # Managed by docs/setup/scripts/20-zsh.sh — apf-portal zsh hand-off. # Hand off to zsh on interactive shells. The `case $-` guard avoids # breaking non-interactive bash (scp, rsync, cron, …), and the # ZSH_VERSION check prevents an infinite re-exec loop. case $- in *i*) if command -v zsh >/dev/null 2>&1 && [ -z "${ZSH_VERSION-}" ]; then exec zsh -l fi ;; esac ``` Two safety nets in the block: - **`case $- in *i*)`** — only runs the hand-off when the shell flag set contains `i` (interactive). `scp` / `rsync` / non-interactive ssh executions go through bash and are not hijacked. - **`[ -z "${ZSH_VERSION-}" ]`** — `ZSH_VERSION` is set by zsh itself; if it's already set we're already in zsh and a re-exec would loop. ## Test plan - [ ] On the dev VM, run `20-zsh.sh` on a fresh state: bash, no existing `~/.bashrc` zsh hand-off → script exits without `chsh` error, `~/.bashrc` gets the block appended, `exit` + reconnect lands in zsh. - [ ] Re-run `20-zsh.sh` → reports `↪ skip zsh hand-off already in ~/.bashrc` (idempotency). - [ ] `scp some-file vm-dev:/tmp/` from the workstation still works (non-interactive bash not hijacked). - [x] `pnpm exec prettier --check` clean on the touched markdown files. - [x] `bash -n docs/setup/scripts/20-zsh.sh` (syntax check) clean. ## Why not amend #220 #220 has already merged. Squash-merge collapsed it to `8a04540` on main; a force-push to amend would rewrite that commit and break anyone who's pulled it. The fix lands as a separate commit on the same setup-doc family. --------- Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr> Reviewed-on: #221 |
||
|
|
8a04540410 |
docs(setup): add Debian 13 dev-VM setup procedure + scripts + devcontainer (#220)
## Summary Adds a full Debian 13 dev-VM setup procedure ([docs/setup/01-dev-debian-vm-setup.md](docs/setup/01-dev-debian-vm-setup.md)) + 10 modular idempotent setup scripts + a systemd template + a `.devcontainer/` spec, in preparation for the new dev VM (`10.100.201.21`) replacing the WSL-based workflow. Both IDE flows (VSCode Remote-SSH + Devcontainer) and both Node toolchains (nvm on host + devcontainer image) are available — devs pick per task. Adjacent context (does not ship here, planned follow-up): - **Preview infra on the GitLab VM (`10.100.201.10`)** — same `dev.compose.yml`, deployed by CI on `main`. Doc placeholder in §8.6. - **GitLab Runner migration** (act_runner Gitea → GitLab Runner Docker executor) — bundled with the Gitea → GitLab cutover. - **Private dotfiles repo** (`apf/dotfiles`) — `~/.zshrc`, `~/.p10k.zsh`, `~/.tmux.conf` versioned. `80-dotfiles.sh` is already structured to fall back to a `~/.dotfiles/` clone when present. No application-code changes. No CI gate impact (doc + scripts + devcontainer spec only). ## What lands | Path | Change | | --- | --- | | `docs/setup/01-dev-debian-vm-setup.md` | **New.** Step-by-step doc: workstation prep (SSH agent + VSCode Remote-SSH + fonts), bootstrap orchestrator, per-script effects, project clone, infra boot, IDE flow A / B / C, apf-ai-service .NET appendix, troubleshooting. | | `docs/setup/02-wsl-terminal-setup.md` | Renamed from `01-wsl-terminal-setup.md`. No content change. | | `docs/setup/03-dev-web-stack.md` | Renamed from `02-dev-web-stack.md`. No content change. | | `docs/setup/04-angular-nx-monorepo.md` | Renamed from `03-angular-nx-monorepo.md`. No content change. | | `docs/setup/README.md` | **New.** Index of the `docs/setup/` folder. | | `docs/setup/scripts/lib.sh` | **New.** Shared helpers — colour-coded log/ok/warn/err/skip, `apt_install` skipping already-installed, `ensure_line` idempotent append, `confirm` prompt. | | `docs/setup/scripts/bootstrap.sh` | **New.** Orchestrator running scripts 10..80 in order with confirmation prompts. | | `docs/setup/scripts/10-base-packages.sh` | **New.** apt update + base packages (curl, wget, git, build-essential, …). | | `docs/setup/scripts/20-zsh.sh` | **New.** zsh + Oh My Zsh (RUNZSH=no, no shell hijack) + Powerlevel10k + `zsh-autosuggestions` + `zsh-syntax-highlighting`. Patches `~/.zshrc` (theme, plugins, fzf hook). | | `docs/setup/scripts/30-cli-tools.sh` | **New.** `bat eza fd-find ripgrep fzf zoxide ncdu keychain` + `jq yq httpie make tree htop tmux direnv dnsutils unzip rsync`. Symlinks Debian-renamed binaries (`batcat`→`bat`, `fdfind`→`fd`) into `~/.local/bin` so notes/aliases.zsh works as-is. | | `docs/setup/scripts/40-node.sh` | **New.** nvm v0.40.1 + Node from `.nvmrc` (currently 24) + corepack enable + pnpm warmed from `package.json#packageManager`. | | `docs/setup/scripts/50-docker.sh` | **New.** Docker CE + compose plugin from docker.com apt repo, user added to `docker` group, `docker.service` enabled at boot. Pinned GPG key + repo line for Debian Trixie. | | `docs/setup/scripts/60-tuning.sh` | **New.** `fs.inotify.max_user_watches=524288` (Vite/Nx watch ceiling), optional 4 GB swapfile, optional hostname rename (only prompts on generic hostnames like `debian13`). | | `docs/setup/scripts/70-hardening.sh` | **New.** Best-effort UFW (`allow OpenSSH` only) + unattended-upgrades on security channel + fail2ban + sshd drop-in (`PermitRootLogin no`, `PasswordAuthentication no`, `AllowAgentForwarding yes`). Probes before applying, validates `sshd -t` before reloading, skips cleanly if infra already locked the box down. | | `docs/setup/scripts/80-dotfiles.sh` | **New.** Symlinks `notes/aliases.zsh` → `~/.oh-my-zsh/custom/aliases.zsh` (backs up an existing target). Copies `notes/gitconfig.txt` to `~/.gitconfig`, prompts for identity, applies via `git config --global user.name/email`. | | `docs/setup/systemd/apf-portal-infra@.service` | **New.** Template systemd unit auto-starting `./infra/local/dev.sh up` at boot. Install: `enable apf-portal-infra@$USER.service`. | | `.devcontainer/devcontainer.json` | **New.** VSCode Dev Container spec on `mcr.microsoft.com/devcontainers/typescript-node:1-24-bookworm`. `docker-outside-of-docker` feature + `--network=apf-portal-dev` so DNS to `postgres`/`redis`/`otel-collector` works. `initializeCommand` fails fast if `dev.sh up` hasn't been run yet. Forwarded ports labelled. Six dev extensions pre-installed. | | `.devcontainer/post-create.sh` | **New.** `corepack enable && pnpm install --frozen-lockfile`. | | `CLAUDE.md` | "Environment conventions" rewritten: documents the two envs (`local` / `development`) + hybrid sub-mode + the two IDE flows (Remote-SSH / Devcontainer), points at the new VM setup doc and the legacy WSL doc. | ## Key choices - **Idempotent scripts, individually runnable.** Each script probes before doing anything (apt package already installed? plugin already cloned? UFW already active? sshd drop-in already present?). Bootstrap is the orchestrator; each script also runs standalone. Re-running after a partial setup is safe and reports `↪ skip` for the no-op cases. - **No private key on the VM — SSH agent forwarding instead.** `~/.ssh/config` on the workstation carries `ForwardAgent yes`; the VM never holds long-lived secrets. Same future pattern for GPG signing (covered in §8.5 as appendix). `keychain` is installed by `30-cli-tools.sh` as a fallback for scenarios where agent forwarding is not available (CI runners, scripts). - **Hardening is "best-effort, probe-first".** Some infra teams ship pre-hardened VMs; this script doesn't fight that. UFW already active? Print rules and skip. unattended-upgrades already on? Skip. SSH already locked down? Skip. Each section validates before reloading so a misconfig can't take SSH offline. - **Devcontainer assumes infra-on-host.** The container runs on the VM but talks to postgres / redis / otel **on the same VM's host docker daemon** through the shared `apf-portal-dev` Compose network. `initializeCommand` fails fast with a clear message if `./infra/local/dev.sh up` hasn't been run yet — better than puzzling `ECONNREFUSED` errors at runtime. - **`60-tuning.sh` raises inotify to 524288.** Default Debian limit is 8K; Vite/Nx in this monorepo blow past that. The setting is persisted in `/etc/sysctl.d/99-apf-portal.conf` so it survives reboot. - **Hybrid mode (workstation IDE + VM infra) is a documented sub-mode.** SSH `LocalForward` directives on 5432/6379/4317/4318 expose the VM's infra services as `localhost:*` on the workstation. Latency cost: 5-15 ms per query, fine for daily work; for long-running flows, wrap in `tmux` or use `autossh`. ## Notes for the reviewer - **Renaming the existing setup docs (01→02, 02→03, 03→04) is the only "destructive" change.** `git log --follow` still works because of `git mv`. Diff shows up as renames, not delete-and-add. - **The brief asked for `bat eza fd-find ripgrep fzf zoxide docker keychain ncdu git`.** `git` is installed by `10-base-packages.sh` (every other script needs it). Everything else lives in `30-cli-tools.sh` + `50-docker.sh`. The "fullstack-dev extras" (`jq yq httpie make tree htop tmux direnv dnsutils unzip rsync`) are additions I proposed in the lock-in question and that you greenlit (full scope) — easy to trim if any of them turn out to be unwanted. - **Both Node toolchains in parallel** — nvm on the VM (via `40-node.sh`) **and** devcontainer in the repo. Devs can use either; both read the same `.nvmrc` + `packageManager` pin so the version stays consistent. - **The systemd unit is a TEMPLATE** (`apf-portal-infra@.service`) — install once, enable per-user (`enable apf-portal-infra@$USER.service`). This is the right shape for a shared VM with multiple devs eventually, even if today only one user uses it. - **No PR-body Co-Authored-By trailer, no Generated-with-Claude footer**, per the project rule. ## Test plan Manual (no automated test exists for this kind of setup work): - [ ] On a fresh Debian 13 VM: `git clone …`, `./docs/setup/scripts/bootstrap.sh`, answer prompts, end up with zsh + Powerlevel10k + all the requested CLI tools + Node 24 + pnpm 10.33.4 + Docker on PATH. - [ ] `./infra/local/dev.sh up` boots successfully against the VM's local docker daemon. - [ ] `pnpm install` + `pnpm exec nx run-many -t lint test --parallel=3` passes on the VM. - [ ] VSCode Remote-SSH from a workstation: connect, open `~/Works/apf_portal`, run `pnpm exec nx serve portal-bff`, confirm reaches `postgres:5432`. - [ ] VSCode Dev Containers from the same workstation: `Reopen in Container`, image builds, `postCreateCommand` runs `pnpm install`, dev server reaches postgres through the `apf-portal-dev` network. - [ ] Hybrid mode: SSH tunnel from workstation, `pnpm exec nx serve portal-bff` locally, confirm postgres reachable via `localhost:5432`. - [x] `pnpm exec prettier --check` clean on the touched markdown files. - [x] Scripts pass `bash -n` (syntax check) — verified during writing. ## What's next - Validate by walking through this doc on the actual VM `10.100.201.21`. Any friction surfaced becomes a follow-up PR (`docs(setup): ...`). - Once the dev VM is operational, return to **ADR-0027 Implementation PR 1** (Region / Delegation / Structure Prisma schema + inline reference-data migration) — paused since the start of this PR. - Set up the **private dotfiles repo** (`apf/dotfiles`) as a small follow-up, then teach `80-dotfiles.sh` to prefer the dotfiles repo over `notes/`. - When migrating to GitLab: PR pair — (a) `git remote set-url` doc updates here, (b) `infra/gitlab-runners/` replacing `infra/ci-runners.compose.yml`. - Future: deploy the **shared preview infra** on `vm-gitlab` (10.100.201.10) — CI-driven, separate PR. --------- Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr> Reviewed-on: #220 |
||
|
|
0d8b3712fb |
docs(adr): accept ADR-0026 + ADR-0027 (#219)
## Summary Promotes [ADR-0026](docs/decisions/0026-person-user-portal-data-model.md) and [ADR-0027](docs/decisions/0027-portal-side-organisational-hierarchy.md) from `proposed` to `accepted`. Both shipped as `proposed` in #217 after the cascade / acteurs_plus source-of-truth audit reshaped the org-hierarchy model. No open questions left on either. No code changes — same shape as #205 (ADR-0025 acceptance). ## What lands | File | Change | | --- | --- | | `docs/decisions/0026-person-user-portal-data-model.md` | Frontmatter `status: proposed → accepted`. | | `docs/decisions/0027-portal-side-organisational-hierarchy.md` | Frontmatter `status: proposed → accepted`. | | `docs/decisions/README.md` | Rows for 0026 and 0027 flip to `accepted`. | | `CLAUDE.md` | Roll-up bumped to `0001 → 0027 accepted`; two new Architecture bullets ("Portal-side identity model" and "Portal-side organisational hierarchy") added after the ADR-0025 bullet; ADR-0025's bullet adjusted to clarify the scope literal `etablissement:<structure-code>` (with a pointer to ADR-0027 for the `Structure.code` semantics); the `@RequireScope` Prisma-resolver roadmap entry now references both ADRs as accepted with the two-schema-then-resolver phasing. | ## Notes for the reviewer - **ADR-0025's bullet got a small touch-up, not a rewrite.** The original copy listed scope kinds as `etablissement:<finess>`, which was accurate when ADR-0025 shipped but is now superseded by ADR-0027's `Structure.code` semantics (FINESS for medico-social rows, internal slugs otherwise). The change is `<finess>` → `<structure-code>` + a `see ADR-0027` parenthetical. No actual decision in ADR-0025 changes. - **Two new Architecture bullets, mirrored on the ADR-0024 / ADR-0025 pair of bullets that precede them in tone + length.** The "Portal-side identity model" bullet calls out the `entraOid`-only v1 dedup and the non-unique `Person.email` (both decisions made during the split rework); the "Portal-side organisational hierarchy" bullet calls out the `Structure.code` round-trip semantics and the deferred-to-ADR-0028 items (`Pole`, `Service`, arbitrary nesting, per-source enrichment, full cascade sync). - **No code changes**, so no `pnpm ci:check` impact. `pnpm exec prettier --check` clean on the four touched files. ## Test plan - [x] `pnpm exec prettier --check` — clean on the four touched files. - [x] Internal links resolve (ADR-0026 ↔ ADR-0027 mutual references, plus the `ADR-0028` dangling marker left intentional for the future sync ADR). - [ ] **Review focus** — the two new Architecture bullets phrasing; the ADR-0025 bullet's small scope-literal touch-up; CLAUDE.md roll-up wording. ## What's next Now unblocked — per ADR-0026 §"Phasing" and ADR-0027 §"Phasing": 1. **ADR-0027 Implementation PR 1** — `Region` / `Delegation` / `Structure` Prisma schema + inline reference-data migration (Région Nouvelle-Aquitaine + Délégation 33 + a handful of test-tenant structures) + `Structure.kind` catalogue + drift-gate extension. Independent of (2) at the schema level. 2. **ADR-0026 Implementation PR 1** — `Person` / `User` / `UserScope` Prisma schema + `PersonAndUserProvisioner` called from `SessionEstablisher` + `Person.source` catalogue + drift-gate extension + updated `PrincipalBuilder` populating `Principal.user.{id, personId}` from the real rows. Can ship in parallel with (1). 3. **ADR-0026 Implementation PR 2** — `PrismaScopeResolver` replacing `StubScopeResolver` + `/admin/users/:id/scopes` admin-app screen + `prisma/seed.ts` populating the 19 test personas' `user_scopes` per `notes/test-tenant-role-assignments.md`. Depends on both (1) and (2). 4. **ADR-0028 (proposed)** — Pléiades + Acteurs+ + cascade syncs + facet schemas (Salarié / Élu / Adhérent / Bénévole / Bénéficiaire / PartenaireExterne) + operator-confirmed Person-reconciliation flow + the deferred org-hierarchy extensions (`Pole`, `Service`, per-source enrichment). --------- Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr> Reviewed-on: #219 |
||
|
|
7d89591f9a |
fix(deps): update dependency @grpc/grpc-js to v1.14.4 (#218)
This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [@grpc/grpc-js](https://grpc.io/) ([source](https://github.com/grpc/grpc-node)) | dependencies | patch | [`1.14.3` -> `1.14.4`](https://renovatebot.com/diffs/npm/@grpc%2fgrpc-js/1.14.3/1.14.4) | --- ### Release Notes <details> <summary>grpc/grpc-node (@​grpc/grpc-js)</summary> ### [`v1.14.4`](https://github.com/grpc/grpc-node/releases/tag/%40grpc/grpc-js%401.14.4): @​grpc/grpc-js 1.14.4 [Compare Source](https://github.com/grpc/grpc-node/compare/@grpc/grpc-js@1.14.3...@grpc/grpc-js@1.14.4) - Fix a bug that could cause servers to crash when handling malformed requests ([advisory GHSA-5375-pq7m-f5r2](https://github.com/grpc/grpc-node/security/advisories/GHSA-5375-pq7m-f5r2)) - Fix a bug that could cause clients and servers to crash when handling malformed compressed messages ([advisory GHSA-99f4-grh7-6pcq](https://github.com/grpc/grpc-node/security/advisories/GHSA-99f4-grh7-6pcq)) </details> --- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Enabled. ♻ **Rebasing**: Whenever PR is behind base branch, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box --- This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate). <!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0MC42Mi4xIiwidXBkYXRlZEluVmVyIjoiNDAuNjIuMSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOlsiZGVwZW5kZW5jaWVzIl19--> Reviewed-on: #218 Co-authored-by: APF Portal Bot <jgautier.webdev+apf-portal-bot@gmail.com> Co-committed-by: APF Portal Bot <jgautier.webdev+apf-portal-bot@gmail.com> |
||
|
|
30cefc4488 |
docs(adr): split ADR-0026 + propose ADR-0027 (Structure hierarchy) (#217)
## Summary
Splits [ADR-0026](docs/decisions/0026-person-user-portal-data-model.md) into two sibling ADRs after a cascade / acteurs_plus source-of-truth audit caught a design break: the first draft pinned `Etablissement.finess` as primary key, but ≥ 30 % of APF's real structure inventory has no FINESS (antennes, dispositifs, entreprises adaptées, mouvement, administratif, siège).
| ADR | Status | Scope |
| --- | --- | --- |
| [ADR-0026](docs/decisions/0026-person-user-portal-data-model.md) — narrowed | `proposed` | `Person` + `User` + `UserScope` only (identity model) |
| [ADR-0027](docs/decisions/0027-portal-side-organisational-hierarchy.md) — new | `proposed` | `Region` + `Delegation` + `Structure` (cascade-aligned: `kind` discriminator + nullable FINESS / SIRET / `codePaie`) |
| `ADR-0028` (future) | — | Pléiades + Acteurs+ + cascade syncs + facet schemas (renumbered from the old `ADR-0027` placeholder) |
No code changes — all three artefacts moving in this PR are markdown.
## What lands
| File | Change |
| --- | --- |
| `docs/decisions/0026-person-user-portal-data-model.md` | Title trimmed (drop `+ organisational hierarchy`); `Region` / `Delegation` / `Etablissement` schema removed; `Person.email` unique constraint dropped (two distinct humans can share an email — see Lifecycle); Lifecycle rewritten without email-based dedup; `UserScope.value` documented as opaque string referencing ADR-0027 codes; Confirmation drops `Etablissement.kind` bullet; ADR-0027 sync references renumbered to ADR-0028; "What ADR-0026 ships vs adjacent ADRs" rewritten to three columns. |
| `docs/decisions/0027-portal-side-organisational-hierarchy.md` | **New.** Decision = Option B (`Structure` with `kind` discriminator + nullable FINESS / SIRET / `codePaie`, internal `code` PK that doubles as FINESS for medico-social structures). Considered options A (FINESS-only — original ADR-0026 draft), B (chosen), C (full cascade replication), D (remote read against cascade). Inline-migration seed for the test tenant (Region 75 + Delegation 33 + a handful of medico-social structures + `siege`); full inventory deferred to ADR-0028's cascade sync. `Structure.kind` enum-as-string drift-gated. |
| `docs/decisions/README.md` | ADR-0026 row: title updated, status stays `proposed`. New ADR-0027 row: `proposed`, tags `data, backend`. |
| `CLAUDE.md` | Roll-up clarifies: `0001 → 0025 accepted`; `ADR-0026 + ADR-0027 proposed`. `@RequireScope` Prisma-resolver roadmap entry references both ADRs + the ADR-0028 follow-up. No new Architecture bullet (entries land when their ADRs ship). |
## Why split
The cascade audit was the trigger. Cascade — APF's medico-social structure registry + Pléiades/Talentia HR integration — models `Structure` with a **seven-value type discriminator**: `medico_social`, `antenne`, `dispositif`, `entreprise_adaptee`, `mouvement`, `administratif`, `sanitaire`. Three of those (`antenne`, `dispositif`, part of `entreprise_adaptee`) **do not have a FINESS** by construction. Cascade carries FINESS / SIRET / SIREN / Pléiades `codePaie` / Talentia `codeCompta` on **separate per-source enrichment rows** (`StructureSourceFiness`, `StructureSourceSirene`, `StructureSourcePleiades`, `StructureSourceTalentia`), nullable and many-to-one against `Structure`.
The acteurs_plus audit confirmed: acteurs_plus does not store FINESS / SIRET / SIREN on its hierarchy entities at all — it uses a portal-internal `code` (unique string) + an `externalId` pointer.
The first ADR-0026 draft's `Etablissement.finess` PK excluded all non-medico-social structures by construction. The fix is **not** to make FINESS nullable on `Etablissement` (that smuggles the discriminator into absence-of-value semantics) — it is to adopt cascade's `Structure` + `kind` discriminator directly. Doing that inside ADR-0026 would have ballooned its scope; splitting is the cleaner shape:
- **ADR-0026** keeps a tight focus on identity (`Person` + `User` + `UserScope`). The Person model is unchanged from the first draft except for the email-dedup rewrite (already discussed before the audit landed).
- **ADR-0027** owns the org hierarchy with the cascade-aligned schema, the seeding posture, and the deferred parts (`Pole`, `Service`, arbitrary nesting, per-source enrichment) called out explicitly as ADR-0028's territory.
## ADR-0027 schema highlights
```prisma
model Structure {
// Portal-internal stable code. For medico-social structures we set
// code = FINESS (round-trips through scope literals + URLs cleanly).
// For non-medico-social structures: APF-internal slug ('siege',
// 'apf-bdx-merignac', 'ea-toulouse', 'mvt-national', …).
code String @id
name String
// Aligned with cascade's Structure.type discriminator. Drift-gated.
kind String // 'medico_social' | 'antenne' | 'dispositif'
// | 'entreprise_adaptee' | 'mouvement'
// | 'administratif' | 'siege'
finess String? @unique // 9 digits, NULL for non-medico-social
siret String? @unique // 14 chars, NULL when not SIRENE-registered
codePaie String? @unique // Pléiades 6-char, NULL in v1
delegationCode String? // NULL for siège, mouvement national
delegation Delegation? @relation(fields: [delegationCode], references: [code])
@@index([kind])
@@index([delegationCode])
}
```
The vocabulary mismatch — ADR-0025's scope kind name is `etablissement` but the value is now a `Structure.code` of any kind — is documented as a known wart, with a possible ADR-0025 amendment as the rename path if a maintainer trips over it.
## Notes for the reviewer
- **`Person.email` unique constraint dropped.** Two distinct humans genuinely can share an email (shared family alias, generic `info@` mailbox, error in an upstream feed). The first draft had `email String? @unique` carried over from a "let's use it as a v1 dedup key" line of thinking that the audit reshaped. The lifecycle now treats `entraOid` as the only natural key the v1 provisioner trusts; email is an attribute, indexed for operator-driven lookup (admin UI search, ADR-0028 reconciliation flow), not a constraint.
- **`UserScope.value` has no FK to ADR-0027 tables.** Deliberate: a scope can outlive its target (a structure decommissioned mid-quarter still has historical UserScope rows pointing at its code, which the audit log needs to read). Admin UI write path validates; runtime guard tolerates stale codes (they fail the resource match, not the sign-in).
- **The old open PR `docs/adr-0026-accept-and-tighten-lifecycle` is superseded by this one.** That branch promoted ADR-0026 (full first draft) to `accepted`. The Q1 / Q2 resolutions from that PR are preserved here — Q1 (no email-dedup) is the new ADR-0026 Lifecycle section; Q2 (inline-migration seed) moves to ADR-0027's "Seeding posture" section since it is org-hierarchy-specific. The old PR can be closed without merging.
- **No code changes**, so no `pnpm ci:check` impact. `pnpm exec prettier --check` clean on the four touched files.
## Test plan
- [x] `pnpm exec prettier --check` — clean on the four touched files.
- [x] ADR cross-references resolve (every `ADR-NNNN` link in the two ADRs round-trips; the new ADR-0028 reference is a known dangling marker for the future sync ADR).
- [ ] **Review focus** — cascade / acteurs_plus audit findings as cited in ADR-0027 §"Context"; the schema choices in ADR-0027 (kind enum, nullable FINESS/SIRET, internal `code` PK); the `Person.email` non-unique change in ADR-0026; the scope-kind vocabulary mismatch documented in ADR-0027.
## What's next (post-merge)
Per ADR-0026 §"Phasing" and ADR-0027 §"Phasing" — the two ADR PRs ship in parallel once accepted:
1. **ADR-0027 Implementation PR 1** — `Region` / `Delegation` / `Structure` Prisma schema + inline reference-data migration + `Structure.kind` catalogue + drift-gate extension.
2. **ADR-0026 Implementation PR 1** — `Person` / `User` / `UserScope` Prisma schema + `PersonAndUserProvisioner` called from `SessionEstablisher` + `Person.source` catalogue + drift-gate extension + updated `PrincipalBuilder`. Independent of (1) at the schema level — can ship in parallel.
3. **ADR-0026 Implementation PR 2** — `PrismaScopeResolver` replacing `StubScopeResolver` + `/admin/users/:id/scopes` admin screen + `prisma/seed.ts` populating the 19 test personas' `user_scopes` per `notes/test-tenant-role-assignments.md`. **Depends on both (1) and (2)** — the seed references `Structure.code` values from (1) and writes `UserScope` rows from (2).
4. **ADR-0028 (proposed)** — Pléiades + Acteurs+ + cascade syncs + facet schemas (Salarié / Élu / Adhérent / Bénévole / Bénéficiaire / PartenaireExterne) + operator-confirmed Person-reconciliation flow + schema extensions (`Pole`, `Service`, per-source enrichment) the sync needs.
---------
Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr>
Reviewed-on: #217
|
||
|
|
abd1f91809 |
fix(deps): update angular (#215)
This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [@angular-devkit/core](https://github.com/angular/angular-cli) | devDependencies | patch | [`21.2.11` -> `21.2.12`](https://renovatebot.com/diffs/npm/@angular-devkit%2fcore/21.2.11/21.2.12) | | [@angular-devkit/schematics](https://github.com/angular/angular-cli) | devDependencies | patch | [`21.2.11` -> `21.2.12`](https://renovatebot.com/diffs/npm/@angular-devkit%2fschematics/21.2.11/21.2.12) | | [@angular/build](https://github.com/angular/angular-cli) | devDependencies | patch | [`21.2.11` -> `21.2.12`](https://renovatebot.com/diffs/npm/@angular%2fbuild/21.2.11/21.2.12) | | [@angular/cdk](https://github.com/angular/components) | dependencies | patch | [`21.2.11` -> `21.2.12`](https://renovatebot.com/diffs/npm/@angular%2fcdk/21.2.11/21.2.12) | | [@angular/cli](https://github.com/angular/angular-cli) | devDependencies | patch | [`21.2.11` -> `21.2.12`](https://renovatebot.com/diffs/npm/@angular%2fcli/21.2.11/21.2.12) | | [@angular/common](https://github.com/angular/angular) ([source](https://github.com/angular/angular/tree/HEAD/packages/common)) | dependencies | patch | [`21.2.13` -> `21.2.14`](https://renovatebot.com/diffs/npm/@angular%2fcommon/21.2.13/21.2.14) | | [@angular/compiler](https://github.com/angular/angular) ([source](https://github.com/angular/angular/tree/HEAD/packages/compiler)) | dependencies | patch | [`21.2.13` -> `21.2.14`](https://renovatebot.com/diffs/npm/@angular%2fcompiler/21.2.13/21.2.14) | | [@angular/compiler-cli](https://github.com/angular/angular/tree/main/packages/compiler-cli) ([source](https://github.com/angular/angular/tree/HEAD/packages/compiler-cli)) | devDependencies | patch | [`21.2.13` -> `21.2.14`](https://renovatebot.com/diffs/npm/@angular%2fcompiler-cli/21.2.13/21.2.14) | | [@angular/core](https://github.com/angular/angular) ([source](https://github.com/angular/angular/tree/HEAD/packages/core)) | dependencies | patch | [`21.2.13` -> `21.2.14`](https://renovatebot.com/diffs/npm/@angular%2fcore/21.2.13/21.2.14) | | [@angular/forms](https://github.com/angular/angular) ([source](https://github.com/angular/angular/tree/HEAD/packages/forms)) | dependencies | patch | [`21.2.13` -> `21.2.14`](https://renovatebot.com/diffs/npm/@angular%2fforms/21.2.13/21.2.14) | | [@angular/language-service](https://github.com/angular/angular) ([source](https://github.com/angular/angular/tree/HEAD/packages/language-service)) | devDependencies | patch | [`21.2.13` -> `21.2.14`](https://renovatebot.com/diffs/npm/@angular%2flanguage-service/21.2.13/21.2.14) | | [@angular/localize](https://github.com/angular/angular) | dependencies | patch | [`21.2.13` -> `21.2.14`](https://renovatebot.com/diffs/npm/@angular%2flocalize/21.2.13/21.2.14) | | [@angular/platform-browser](https://github.com/angular/angular) ([source](https://github.com/angular/angular/tree/HEAD/packages/platform-browser)) | dependencies | patch | [`21.2.13` -> `21.2.14`](https://renovatebot.com/diffs/npm/@angular%2fplatform-browser/21.2.13/21.2.14) | | [@angular/router](https://github.com/angular/angular/tree/main/packages/router) ([source](https://github.com/angular/angular/tree/HEAD/packages/router)) | dependencies | patch | [`21.2.13` -> `21.2.14`](https://renovatebot.com/diffs/npm/@angular%2frouter/21.2.13/21.2.14) | | [@schematics/angular](https://github.com/angular/angular-cli) | devDependencies | patch | [`21.2.11` -> `21.2.12`](https://renovatebot.com/diffs/npm/@schematics%2fangular/21.2.11/21.2.12) | --- ### Release Notes <details> <summary>angular/angular-cli (@​angular-devkit/core)</summary> ### [`v21.2.12`](https://github.com/angular/angular-cli/blob/HEAD/CHANGELOG.md#21212-2026-05-20) [Compare Source](https://github.com/angular/angular-cli/compare/v21.2.11...v21.2.12) ##### [@​angular/build](https://github.com/angular/build) | Commit | Type | Description | | --------------------------------------------------------------------------------------------------- | ---- | --------------------------------------------- | | [cbad57579](https://github.com/angular/angular-cli/commit/cbad57579adb5de7887985afbb2bf1f40adf3cb2) | fix | ignore virtual esbuild paths with (disabled): | <!-- CHANGELOG SPLIT MARKER --> </details> <details> <summary>angular/components (@​angular/cdk)</summary> ### [`v21.2.12`](https://github.com/angular/components/blob/HEAD/CHANGELOG.md#21212-plastic-moose-2026-05-20) [Compare Source](https://github.com/angular/components/compare/v21.2.11...v21.2.12) ##### material | Commit | Type | Description | | -- | -- | -- | | [da87be7646](https://github.com/angular/components/commit/da87be76464d76ec11ae922abd5f4c72c5b4ea3e) | fix | **datepicker:** ensure dates don't overflow on a small screen ([#​33281](https://github.com/angular/components/pull/33281)) | <!-- CHANGELOG SPLIT MARKER --> </details> <details> <summary>angular/angular (@​angular/common)</summary> ### [`v21.2.14`](https://github.com/angular/angular/blob/HEAD/CHANGELOG.md#21214-2026-05-20) [Compare Source](https://github.com/angular/angular/compare/v21.2.13...v21.2.14) ##### compiler | Commit | Type | Description | | -- | -- | -- | | [68282dff9f](https://github.com/angular/angular/commit/68282dff9f9ef46540cca4bd38fc1ab739c8a783) | fix | strip namespaced SVG script elements during template compilation | ##### core | Commit | Type | Description | | -- | -- | -- | | [c0f52272ed](https://github.com/angular/angular/commit/c0f52272ed337d4776bd4178cbbdc7f32037500f) | fix | do not insert todo when migrating void [@​Output](https://github.com/Output) | | [938a7f3edd](https://github.com/angular/angular/commit/938a7f3eddda97a39edb9edcc8b4dd970858b3a2) | fix | makes resource URL sanitizer lookup case-insensitive | | [0fb2724194](https://github.com/angular/angular/commit/0fb272419407a64a0a47096b03a911f4e7e83d79) | fix | reject script element as a dynamic component host | | [49113ac0ef](https://github.com/angular/angular/commit/49113ac0eff852d987b5acb28a9bbda0242842cd) | fix | visit ICU expressions in signal migration schematics | ##### router | Commit | Type | Description | | -- | -- | -- | | [099bf577ee](https://github.com/angular/angular/commit/099bf577ee8f0bab60593a8fd2a1de7d298e3cd6) | fix | skip scroll-to-top on initial navigation when hydrating | <!-- CHANGELOG SPLIT MARKER --> </details> --- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Enabled. ♻ **Rebasing**: Whenever PR is behind base branch, or you tick the rebase/retry checkbox. 👻 **Immortal**: This PR will be recreated if closed unmerged. Get [config help](https://github.com/renovatebot/renovate/discussions) if that's undesired. --- - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box --- This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate). <!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0MC42Mi4xIiwidXBkYXRlZEluVmVyIjoiNDAuNjIuMSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOlsiZGVwZW5kZW5jaWVzIl19--> Reviewed-on: #215 Co-authored-by: APF Portal Bot <jgautier.webdev+apf-portal-bot@gmail.com> Co-committed-by: APF Portal Bot <jgautier.webdev+apf-portal-bot@gmail.com> |
||
|
|
e8796e94f4 |
chore(deps): update dependency ts-jest to v29.4.11 (#214)
This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [ts-jest](https://kulshekhar.github.io/ts-jest) ([source](https://github.com/kulshekhar/ts-jest)) | devDependencies | patch | [`29.4.10` -> `29.4.11`](https://renovatebot.com/diffs/npm/ts-jest/29.4.10/29.4.11) | --- ### Release Notes <details> <summary>kulshekhar/ts-jest (ts-jest)</summary> ### [`v29.4.11`](https://github.com/kulshekhar/ts-jest/blob/HEAD/CHANGELOG.md#29411-2026-05-21) [Compare Source](https://github.com/kulshekhar/ts-jest/compare/v29.4.10...v29.4.11) ##### Bug Fixes - preserve Bundler on the CJS path under TypeScript >= 6 ([3941818](https://github.com/kulshekhar/ts-jest/commit/39418187515f11b6584d35a4e3ddf50231f74936)), closes [#​4198](https://github.com/kulshekhar/ts-jest/issues/4198) </details> --- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Enabled. ♻ **Rebasing**: Whenever PR is behind base branch, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box --- This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate). <!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0MC42Mi4xIiwidXBkYXRlZEluVmVyIjoiNDAuNjIuMSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOlsiZGVwZW5kZW5jaWVzIl19--> Reviewed-on: https://git.unespace.com/julien/apf_portal/pulls/214 Co-authored-by: APF Portal Bot <jgautier.webdev+apf-portal-bot@gmail.com> Co-committed-by: APF Portal Bot <jgautier.webdev+apf-portal-bot@gmail.com> |
||
|
|
8266e5b172 |
docs(adr-0026): person + user portal-side data model (proposed) (#213)
## Summary
[ADR-0025](docs/decisions/0025-authorization-model-privileges-roles-scopes.md) shipped the authorization model with three stubs explicitly deferred to a follow-up ADR:
- `Principal.user.id` and `Principal.user.personId` carry the Entra `oid` as a placeholder — `apps/portal-bff/src/auth/principal-builder.ts` documents the seam.
- `StubScopeResolver` returns `[{ kind: 'unrestricted' }]` for every signed-in user; the per-persona scope values documented in `notes/test-tenant-role-assignments.md` have nowhere to live.
- `@RequireScope`'s `ScopableResource` shape references an `Etablissement` / `Delegation` / `Region` chain that has no Prisma table.
ADR-0026 specifies the portal-side data model that closes those stubs. **Decision-only, status `proposed`** — implementation lands in two PRs once accepted.
## What lands
| File | Change |
| --- | --- |
| `docs/decisions/0026-person-user-portal-data-model.md` | New ADR. MADR 4.0.0 format. Tags: `data`, `backend`, `security`. ~310 lines. |
| `docs/decisions/README.md` | One new index row for 0026 (status `proposed`, 2026-05-24). |
## ADR scope
The ADR commits the **schema** for:
- **`Person`** golden record — stable identity, can exist without a portal account (Pléiades pre-provisioning, dossier bénéficiaires, alumni). `source` field tracks provenance (`self-signin` in v1; `pleiades` / `acteurs-plus` join the catalogue with ADR-0027).
- **`User`** portal-account overlay — one-to-zero-or-one with Person, lazy-created on first OIDC callback. Portal-only state (`lastSignInAt`, future a11y preferences) rides here, not on the shared Person row.
- **`UserScope`** — confirms the migration whose shape ADR-0025 §"Sources of truth — apf_portal-side `user_scopes` table" already specified.
- **`Region` / `Delegation` / `Etablissement`** — organisational hierarchy with externally-meaningful codes (INSEE / French dept / FINESS) as primary keys, matching the on-the-wire shape of the scope literals (`etablissement:0330800013`, `delegation:33`).
The ADR **explicitly defers**:
- Facet schemas (`Salarie`, `Adherent`, `Benevole`, `Elu`, `Beneficiaire`, `PartenaireExterne`) — they track upstream-system shape and ride alongside their producing sync.
- Pléiades / Acteurs+ sync logic, reconciliation policy between sync-owned and admin-UI-owned fields.
Both deferrals point at **ADR-0027** (Pléiades + Acteurs+ syncs + facet schemas) as the next-up ADR.
## Notes for the reviewer
- **Why `Person` + `User` split and not a single table.** The "Considered Options" section walks through this. Option A (User-only) breaks down the moment a dossier holder who never signs in needs a stable identifier. Option D (Person with embedded facet columns) forces every Pléiades or Acteurs+ schema change through a table that both shapes share, which is exactly the kind of coupling that ADR-0027 needs the freedom to design out.
- **Why externally-meaningful primary keys for the hierarchy.** `Region.code` / `Delegation.code` / `Etablissement.finess` are INSEE / FINESS codes — stable across reorgs and already on every URL the portal will mint. Adding a separate UUID would force every consumer to indirect through it for no gain. The trade-off (no in-place rename — if a délégation merges, delete + re-insert with the new code) is bounded by how rarely INSEE rebases.
- **Lazy User creation at first sign-in.** v1 has no Pléiades data; the OIDC callback creates the Person + User pair the first time it sees a new `oid`. When Pléiades sync ships (ADR-0027), the same provisioner extends with a `Person.externalId` lookup before falling back to `email`. The schema does not change between the two regimes — only the provisioner's lookup order does.
- **`Person.source` and `Etablissement.kind` as enum-as-string + drift-gate extension.** The catalogue-drift gate ([ADR-0025 §"Confirmation"](docs/decisions/0025-authorization-model-privileges-roles-scopes.md) and `scripts/check-catalogue-drift.mjs`) was built precisely to constrain hand-edited string columns. Extending it to assert every `Person.source` write is in the closed catalogue closes one of the soft spots in the v1 schema without standing up a Postgres ENUM.
- **PII posture.** `Person.firstName` / `lastName` / `email` are flagged in the ADR's Decision Drivers. The Pino redact list ([ADR-0012](docs/decisions/0012-observability-pino-opentelemetry.md)) and the audit-log salt ([ADR-0013](docs/decisions/0013-audit-trail-separated-postgres-append-only.md)) already cover Entra `oid`; the new PII fields ride the same posture (caller-redacted at the audit module, redacted at the Pino logger before the line ships).
- **What "two PRs" means in the More Information section.** PR 1: Prisma schema migration + `PersonAndUserProvisioner` called from `SessionEstablisher` + drift-gate extension. PR 2: `PrismaScopeResolver` replacing `StubScopeResolver` + `/admin/users/:id/scopes` screen + `prisma/seed.ts` populating the 19 test-tenant personas' `user_scopes` rows.
- **Backward compatibility for in-flight Redis sessions.** Sessions minted before the schema migration carry a Principal whose `user.id` is the Entra `oid` placeholder. The legacy-session bridge in [`apps/portal-bff/src/auth/principal-extractor.ts`](apps/portal-bff/src/auth/principal-extractor.ts) (added in #208) already handles this — when the migration deploys, those sessions continue to work for the 12 h absolute-TTL window, after which every session in Redis has the real `personId`.
## Open questions to resolve before acceptance
These were flagged in the ADR text and are worth surfacing here for the review pass:
- **`Person.email` as the v1 dedup key.** The ADR's "Bad, because" item — two Pléiades records sharing an email would crash the unique constraint. ADR-0027 will add the `externalId` precedence, but in the interim the v1 lazy-creator either trusts emails-are-unique (acceptable for the test tenant where every persona has a distinct one) or skips the dedup attempt and creates a fresh Person per `oid`. The ADR currently picks the dedup-by-email path; flag if the safer choice is "always fresh, reconcile later".
- **Should `Region` / `Delegation` / `Etablissement` migrations be seeded as part of the schema PR, or kept as a separate dataset import?** The ADR is silent on this; my default is "seed the geographic codes APF actually operates in" (a one-off SQL fixture in the migration directory). Worth confirming.
## Test plan
- [x] `pnpm exec prettier --check docs/decisions/0026-person-user-portal-data-model.md docs/decisions/README.md` — clean.
- [ ] **Review focus** — the chosen Option B vs the rejected A/C/D, the deferred facets list, the `Person.source` catalogue, the `Etablissement.kind` catalogue, and the two open questions above.
- [ ] Once accepted, the implementation phasing in the ADR's `§More Information` opens (2 PRs).
## What's next
- **This PR** — ADR-0026 ships as `proposed`.
- **Acceptance PR** — review pass, address open questions, promote `proposed → accepted` (same cadence as [#201 → #205](docs/decisions/0025-authorization-model-privileges-roles-scopes.md) for ADR-0025).
- **Implementation PR 1** — Prisma schema + lazy provisioner.
- **Implementation PR 2** — `PrismaScopeResolver` + admin-UI scope-seeding + test-tenant seed.
- **ADR-0027** — Pléiades + Acteurs+ syncs + facet schemas (the deferred surface from this ADR).
---------
Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr>
Reviewed-on: #213
|
||
|
|
ca3963b2e3 |
docs(claude.md): refresh Repository status with ADR-0022/0023/0025 shipments (#212)
## Summary `CLAUDE.md`'s **Repository status** section drifted from `main` while three ADRs landed back-to-back. The roadmap block still listed ADR-0022 (docs static site) and ADR-0023 (charts lib + audit dashboards) as upcoming chantiers even though both shipped, and the new `libs/shared/auth/` lib introduced by ADR-0025 was not in the lib-roots list. This PR refreshes the section to reflect what `main` actually carries today. No code touched. ## What lands | File | Change | | --- | --- | | `CLAUDE.md` | Repository status section rewritten: lib roots updated, three new "Shipped on `main`" bullets (ADR-0022 / 0023 / 0025), roadmap pruned and rebalanced around what is genuinely outstanding. | ## Notes for the reviewer - **Lib roots.** Was: "four lib roots (`libs/feature/`, `libs/shared/state`, `libs/shared/tokens`, `libs/shared/ui`, `libs/shared/util`)" — five entries called "four". Now: "seven lib roots" listing `libs/feature/auth`, `libs/shared/auth`, `libs/shared/charts`, `libs/shared/state`, `libs/shared/tokens`, `libs/shared/ui`, `libs/shared/util`. The count is correct and the new libs from ADR-0023 and ADR-0025 are explicit. - **CI line** now mentions the catalogue-drift gate alongside the standard `format:check / lint / test / build` quartet — readers grepping for "what CI runs" land on the actual gate set. - **AI relay entry merged with its live consumer.** The previous wording said "Live consumer (chatbot widget on `portal-shell`) and the proto-drift CI gate ship next" — both half-true: chatbot widget shipped (under `apps/portal-shell/src/app/features/chatbot/`), proto-drift gate did not. Entry now says chatbot is live and moves the proto-drift gate to the roadmap. - **Phase-3a admin app phrasing updated.** Previous wording: "business modules (CMS, menu management, user list, audit log viewer) not yet implemented". Two of those four exist on `main` today: `/admin/users` (user-list reader via `admin-users-reader.service.ts`) and `/admin/audit` (audit-log viewer + statistics + integrated charts). Entry now names them and notes that only CMS + menu management remain. - **Roadmap entry split for the guards.** The old line bundled `@RequireMfa()` and `@RequireAdmin()` as both "designed-in, awaiting first consumer route". `@RequireAdmin` is wired into every `/api/admin/*` controller today; only `@RequireMfa` is still consumer-less. Roadmap line narrowed to mention only `@RequireMfa`. - **ADR-0026 referenced as `(proposed)`.** The link target is a placeholder `(#)` because the ADR file does not exist yet — kept lowercase-light so the link does not render as a dead anchor in editors. Will become a real link in the same PR that drafts ADR-0026. ## Test plan - [x] `pnpm exec prettier --check CLAUDE.md` — clean. - [x] Manual cross-check against the workspace state: - `find libs/shared -maxdepth 1 -type d` matches the listed lib roots. - `ls docs/.vitepress/` confirms VitePress is wired. - `ls libs/shared/charts/src/lib/` confirms the three chart components exist. - `grep -l BarChart\\|DonutChart\\|StackedBarChart apps/portal-admin/src/app/pages/audit/` confirms the audit-page integration. - `ls scripts/check-catalogue-drift*` confirms the drift gate exists. - `grep -rn @RequireAdmin apps/portal-bff/src/admin/` confirms the admin gate is wired today. No CI gates affected — this is a doc-only change. --------- Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr> Reviewed-on: #212 |