a8ddb0f63dff00cc2ac8d83073cc63914e7c61fc
190 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
0464ce3ac8 |
feat(portal-bff): close the auth loop — callback persists session, /me, RP-initiated /logout (#112)
## Summary
Closes the OIDC loop end-to-end on the BFF side:
- `/auth/callback` now writes the resolved `AuthenticatedUser` into `req.session.user` and waits for `req.session.save()` before redirecting, so the SPA reaches the landing page with a populated session.
- `GET /auth/me` returns the curated public view of the session user (`oid`, `tid`, `username`, `displayName`) or `401 {"error": "unauthenticated"}`. `amr` and other internal claims stay server-side.
- `GET /auth/logout` destroys the BFF session (Redis `DEL`), clears the session cookie, and 302s to Entra's `/oauth2/v2.0/logout` so the IdP-side session is killed too — RP-initiated logout per ADR-0009.
Scope intentionally stops here: the absolute-timeout interceptor (12 h hard ceiling) and the `user_sessions:{userId}` secondary index land in dedicated follow-ups.
## Notable choices
**`req.session.save()` is awaited before the redirect.** Express-session writes to its store on response end; emitting the 302 closes the response before `connect-redis` finishes the write, so without an explicit await the browser can race the SPA into requesting `/me` against a missing key. Awaiting `save()` is the documented fix.
**Logout via `GET`.** Matches `/login` (also `GET`) and keeps the UX a plain anchor / top-level navigation. The CSRF surface is mitigated by `SameSite=Lax` on the session cookie — cross-site subresource requests (`<img src>`, `fetch`) don't carry it. A dedicated CSRF middleware lands with phase-2 security; if we want POST-only logout earlier, easy follow-up.
**`/me` strips `amr`.** The session payload mirrors `AuthenticatedUser` (used internally by the future `@RequireMfa()` guard, ADR-0011), but the SPA only ever needs the curated subset. Mapping happens in the controller — no leak by default.
**Logout URL skips `id_token_hint`.** ADR-0009 mentions it for single-account logout UX, but v1 doesn't persist the `id_token` in the session yet (the encrypted `tokens` blob lands with downstream API support per ADR-0014). Without `id_token_hint`, Entra shows an account picker — the conservative default until token persistence ships.
**Cookie name in logout.** Uses `sessionCookieName()` from `session/session-cookie.ts` so logout clears the same cookie the middleware sets — `__Host-portal_session` in prod, `portal_session` in dev.
## Out of scope (next PRs)
- Absolute-timeout interceptor (12 h hard ceiling, ADR-0010).
- `user_sessions:{userId}` secondary index for admin "logout everywhere".
- Persisting the `id_token` / `access_token` / `refresh_token` blob in the encrypted session (ADR-0014 dependency).
- CSRF middleware (phase-2 security).
- Renaming `ENTRA_POST_LOGOUT_REDIRECT_URI` if we want a distinct post-login redirect target — for now both flows land on the same SPA URL.
## Test plan
- [x] `pnpm nx test portal-bff` → **110/110 pass** (was 99 before this PR; +11 specs across `auth.controller.spec.ts` and `auth.service.spec.ts`).
- [x] `pnpm nx lint portal-bff` → clean.
- [x] `pnpm nx build portal-bff` → webpack compiled successfully.
- [x] Prettier-clean on all touched files.
- [ ] Manual end-to-end smoke test:
- [ ] `/api/auth/login` → Entra → back at `/api/auth/callback` → session cookie set, redirect to SPA.
- [ ] `/api/auth/me` → 200 JSON when authenticated, 401 when anonymous.
- [ ] `/api/auth/logout` → Redis key gone, cookie cleared, lands at SPA via Entra logout.
---------
Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr>
Reviewed-on: #112
|
||
|
|
2e9605a078 |
chore(deps): pin protobufjs to >=8.0.2 to clear 7 transitive advisories (#111)
## Summary `protobufjs 8.0.0` / `8.0.1` ship 7 fresh advisories (4 high, 3 moderate) pulled in transitively as `@opentelemetry/exporter-trace-otlp-http → @opentelemetry/otlp-transformer → protobufjs`. `pnpm audit --audit-level=moderate` flags them, which blocks the `ci:audit` gate on every push. Pins the package directly via `pnpm.overrides` — same shape we already use for `axios`, `brace-expansion`, `follow-redirects`, `ip-address`, `tmp`, `yaml`. Resolution lands on `protobufjs@8.2.0` (latest stable). The override is `protobufjs@<8.0.2: ">=8.0.2"` so it auto-yields the moment a non-vulnerable transitive lands and the override becomes a no-op — no need to remember to remove it. ## Advisories cleared | ID | Severity | Issue | | ------------------- | -------- | ----------------------------------------------------- | | GHSA-66ff-xgx4-vchm | high | code generation gadget | | GHSA-75px-5xx7-5xc7 | high | code generation gadget after prototype pollution | | GHSA-jvwf-75h9-cwgg | high | process-wide DoS through unsafe option paths | | GHSA-685m-2w69-288q | high | DoS through unbounded protobuf recursion | | GHSA-q6x5-8v7m-xcrf | moderate | overlong UTF-8 decoding | | GHSA-2pr8-phx7-x9h3 | moderate | DoS from crafted field names in generated code | | GHSA-fx83-v9x8-x52w | moderate | prototype injection in generated message constructors | ## Test plan - [x] `pnpm audit --audit-level=moderate` → **No known vulnerabilities found** - [x] `pnpm nx test portal-bff` → 99/99 pass - [x] `pnpm nx build portal-bff` → webpack compiled successfully - [x] `pnpm install` resolves `protobufjs@8.2.0` (verified in lockfile) --------- Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr> Reviewed-on: #111 |
||
|
|
758d723744 |
feat(portal-bff): session middleware with AES-256-GCM at rest per ADR-0010 (#110)
## Summary
Mounts `express-session` + `connect-redis` at bootstrap on top of the shared `ioredis` client, with **AES-256-GCM applied to the full JSON payload before it lands in Redis** (per ADR-0010). The configured middleware is exposed as a NestJS provider (`SESSION_MIDDLEWARE`) and `main.ts` mounts it through `app.get(...)` so it sits on the same Redis connection the rest of the BFF uses — no second client at the bootstrap layer.
Envelope is versioned (`v1.<iv>.<tag>.<ciphertext>`, all base64url) so the algorithm / key derivation can rotate without a flag-day re-encryption. Tamper / wrong-key / unknown-version all raise `SessionDecryptError`; for now the failure is logged via Pino with `event: session.decrypt_failed` — the first-class audit event lands with ADR-0013.
Scope is intentionally **infrastructure only**:
- middleware mounted on every request, `req.session` available downstream
- session id = `crypto.randomBytes(32).toString('base64url')` (256 bits per ADR-0010)
- cookie name: `__Host-portal_session` in production, `portal_session` in dev (the `__Host-` prefix mandates `Secure`, which dev HTTP can't satisfy)
- `httpOnly + sameSite=lax + path=/`; `resave:false`, `saveUninitialized:false`, `rolling:true`
- cookie `maxAge` follows `SESSION_IDLE_TIMEOUT_SECONDS` (default 1800)
- encryption-at-rest active end-to-end
Out of scope, landing in follow-ups: `/auth/callback` populating `req.session.user`, `/me`, `/auth/logout`, the absolute-timeout interceptor, and the `user_sessions:{userId}` secondary index.
## Notable shape choices (ADR-0010 amended in the same commit)
**Full-payload encryption vs. just the `tokens` field.** The first draft of ADR-0010 scoped at-rest encryption to a `tokens` sub-field. The session also carries claims (`oid`, `tid`, `preferred_username`, …) that qualify as PII under GDPR — for an APF-Handicap portal handling health-adjacent data this matters. Encrypting the envelope is strictly stronger and removes the need to classify fields one by one. The ADR text is updated to match.
**`ioredis` + adapter vs. switching the BFF to `node-redis`.** `connect-redis` v9 was rewritten for `node-redis` v4 and no longer accepts `ioredis` directly. Two reasonable paths:
1. **Adapter (chosen)** — keep the shared `ioredis` client; shim the six commands `connect-redis` actually calls (`get`, `set` with `{expiration:{type:'EX',value}}`, `expire`, `del`, `mGet`, `scanIterator`) to the node-redis shape. Smallest blast radius — RedisModule, OBO cache (ADR-0014), future pub/sub all stay on a single Redis library.
2. **Switch RedisModule to `node-redis`** — clean alignment with `connect-redis`'s expectations, but touches every Redis consumer and would itself require an ADR amendment.
The adapter is reversible: if we ever decide to standardise on `node-redis`, deleting one file removes it. Happy to switch if you'd rather take that path.
## Env vars
- `SESSION_ENCRYPTION_KEY` — **mandatory**, AES-256-GCM key (32 bytes after base64url decode). New `assertSessionEncryptionKey()` validator wired in `main.ts` alongside the other pre-flight checks.
- `SESSION_IDLE_TIMEOUT_SECONDS` — optional, default `1800`.
- `SESSION_ABSOLUTE_TIMEOUT_SECONDS` — optional, default `43200` (consumed by the absolute-timeout interceptor in a follow-up).
`.env.example` updated; the three variables are promoted from the "future vars" block to the active section.
## Test plan
- [x] `pnpm nx test portal-bff` — **99/99 pass** (was 62 before this PR; +37 new specs across the 5 new files).
- [x] `pnpm nx build portal-bff` — clean webpack build.
- [x] `pnpm nx lint portal-bff` — clean.
- [x] Prettier-clean for all PR source files.
- [ ] Local smoke test once the next PR wires `/auth/callback` → `req.session.user`; this PR has no user-visible behaviour to exercise on its own.
---------
Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr>
Reviewed-on: #110
|
||
|
|
d4b5ed1c5d |
feat(portal-bff): redis client foundation per ADR-0010 (#109)
## Summary First step toward Redis-backed sessions (ADR-0010). Adds the shared `ioredis` connection that every downstream consumer (session storage, OBO token cache, …) injects via the new `REDIS_CLIENT` DI token. No session logic in this PR — that's the next one. ## What lands - **`ioredis@^5.10.1`** as a direct dependency. Chosen by ADR-0010 for its mature Sentinel support — single-instance URL today, Sentinel-HA configuration lands with the prod infrastructure ADR. - **[`.env.example`](apps/portal-bff/.env.example)** promotes `REDIS_URL` from its future-vars comment to an active variable, defaulting to the local Compose stack's address. The Sentinel-style keys (`REDIS_SENTINEL_HOSTS`, `REDIS_SENTINEL_NAME`, `REDIS_TLS`) stay in the future-vars comment until the prod deploy. - **[`check-redis-config.ts`](apps/portal-bff/src/config/check-redis-config.ts)** — boot-time guard mirroring the existing four: - Refuses to start on missing / non-`redis(s)://` / passwordless / placeholder URLs. - Returns a typed `RedisConfig` with parsed `host` + `port` for downstream observability. - **[`redis.token.ts`](apps/portal-bff/src/redis/redis.token.ts)** — `REDIS_CLIENT` string token + `Redis` type alias. Same shape as the existing `ENTRA_CONFIG` / `MSAL_CLIENT`. - **[`redis.module.ts`](apps/portal-bff/src/redis/redis.module.ts)** — `RedisModule` factory provider: - Caps `maxRetriesPerRequest: 3` so an unreachable Redis surfaces a clear command-time error rather than an infinite reconnect storm. - Wires `connect` / `ready` / `error` / `close` / `reconnecting` events into the Pino stream under the `redis` context — easy log isolation. - Non-global; consumers import the module to state "I depend on Redis". - **`main.ts`** calls `assertRedisConfig()` alongside the other three validators; **`AppModule`** imports `RedisModule`. ## Decisions worth flagging - **`maxRetriesPerRequest: 3`** rather than the ioredis default of 20. With the default, a Redis outage masquerades as request-level timeouts spread over minutes. Capping low surfaces the outage in the first command failure — the BFF can then return 503 and recover quickly when Redis comes back. - **Single shared client.** Pub/sub use-cases (when they appear) duplicate via `redis.duplicate()` per ioredis convention. Connect/disconnect is one socket per BFF instance. - **No explicit shutdown hook yet.** Node's process-exit handlers and ioredis's own cleanup take care of the socket on SIGTERM / Ctrl+C. If we see stuck connections in real load, we wire `OnApplicationShutdown` + `redis.quit()`. - **Sentinel-style config stays in the future-vars comment.** ioredis supports it natively, but plumbing it on top of the URL form complicates the validator and the factory for zero v1 payoff. Lands with the prod infrastructure ADR. ## Verification - `nx run-many -t lint test build --projects=portal-bff` — green. - **62 / 62 specs** (was 52; +10 — `check-redis-config` covers happy path + 6 failure modes; `redis.module` covers DI resolution against an unreachable URL plus the missing-env failure). - Boot smoke against the local Compose stack: Pino's `redis` context shows `redis.connect` → `redis.ready` on startup; killing the Redis container produces `redis.close` / `redis.reconnecting` lines. ## What this PR explicitly does NOT do - Mount `express-session` + `connect-redis` middleware. The next PR wires the session cookie (`__Host-portal_session`), the encrypted payload, and the lookup middleware that attaches `user` to every request. - Plug the callback into session creation. Auth still ends with a Pino log + redirect; the SPA still sees the user anonymous on the next request. - Sentinel / TLS configuration. Future-var keys are documented in `.env.example` for when the prod deploy lands. --------- Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr> Reviewed-on: #109 |
||
|
|
bfa35d3283 |
fix(portal-bff): drop strict amr check — flow blocked when Entra omits the claim (#108)
## Bug
After a real sign-in against the Entra tenant, the callback rejected the flow with:
```
{"context":"AuthCallback","event":"auth.flow_error","failure":{"kind":"amr-missing"}}
```
The user landed on the SPA with `?auth_error=amr-missing` instead of authenticated. Every dev sign-in is blocked.
## Root cause
PR #107's `amr-missing` guard misread ADR-0011's intent. `amr` is an **optional** claim in Entra ID tokens: it's populated for fresh interactive sign-ins where Conditional Access asked for an MFA method, and frequently absent for SSO / refresh flows or in tenants where no CA policy is configured on the app registration. Rejecting tokens on empty `amr` blocks every legitimate sign-in against such a tenant.
ADR-0011 actually specifies:
- **Conditional Access** (org-side) is the enforcement layer for "MFA happened".
- The **`@RequireMfa({ freshness: 600 })`** decorator (designed-in, no v1 consumer) is what guards sensitive routes.
- The BFF surfaces `amr` through the audit log and the future guard, not as a callback precondition.
## Fix
- **[`auth.errors.ts`](apps/portal-bff/src/auth/auth.errors.ts)**: drop the `amr-missing` variant from the `AuthCodeFlowError` discriminator. Three failure modes left: `state-mismatch`, `flow-expired`, `token-exchange-failed`. MSAL's ID-token validation (signature, issuer, audience, exp, nbf) is the real gate at this stage.
- **[`auth.service.ts`](apps/portal-bff/src/auth/auth.service.ts)**: `toAuthenticatedUser` keeps extracting `amr` and passing it through (as a possibly-empty string array) so the structured log line and the future `@RequireMfa` guard still see it. The strict `if (amr.length === 0) throw` is replaced by a comment explaining the new shape.
- **[`auth.service.spec.ts`](apps/portal-bff/src/auth/auth.service.spec.ts)**: the `'throws amr-missing'` test becomes `'returns the user even when the ID token has no amr claim'` — asserts the array passes through empty rather than blocking the flow.
## Verification
- `nx run-many -t lint test build --projects=portal-bff` — green. **52/52 specs**.
- Manual smoke: end-to-end sign-in against the live tenant now lands cleanly on the SPA; Pino's `auth.signed_in` log shows the resolved identity with `amr` (often `[]` until CA is configured on the org side).
---------
Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr>
Reviewed-on: #108
|
||
|
|
c50794eceb |
feat(portal-bff): /auth/callback route — token exchange + amr check (#107)
## Summary
Fourth step of ADR-0009 wiring. Closes the OIDC round-trip on the BFF side (modulo session persistence — that's the next PR per ADR-0010). Entra now redirects the user back to `GET /api/auth/callback`; the BFF verifies the state, exchanges the code for tokens via MSAL's `acquireTokenByCode`, runs the ADR-0011 `amr` sanity-check, logs the resolved identity to Pino, clears the single-use pre-auth cookie, and 302s the user back to the SPA.
## What lands
- **[`auth.errors.ts`](apps/portal-bff/src/auth/auth.errors.ts)** — discriminated-union `AuthCodeFlowError` (`state-mismatch` / `flow-expired` / `amr-missing` / `token-exchange-failed`) + `AuthCodeFlowException` wrapper. The `kind` field doubles as the `?auth_error=<code>` query param on the SPA-bound redirect so the front-end can render an exact message without duplicating the string set.
- **[`AuthService.completeAuthCodeFlow(code, state, preAuth, now?)`](apps/portal-bff/src/auth/auth.service.ts)** — verifies state binding, refuses cookies older than the 5-minute flow TTL, calls MSAL Node's `acquireTokenByCode` with the stored verifier, validates `amr` is non-empty (the BFF sanity-check per ADR-0011 — Entra Conditional Access on the org side does the real enforcement), extracts `oid` / `tid` / `preferred_username` / `name` / `amr` into an `AuthenticatedUser` shape.
- **[`auth.cookie.ts`](apps/portal-bff/src/auth/auth.cookie.ts)** gains `clearPreAuthCookieOptions()` mirroring the set-options minus `maxAge` so the browser actually drops the cookie. (Cookies match by name + path + secure; getting any of those wrong leaves the old cookie in place.)
- **[`AuthController.callback()`](apps/portal-bff/src/auth/auth.controller.ts)** — `@Get('callback')`. Always clears the cookie first (single-use). Bails on Entra-side errors (`?error=`), missing query params, missing or malformed cookie — each branch logs a structured Pino warning and redirects with the right `auth_error` code. On `AuthCodeFlowException`, logs + redirects with the typed `kind`. On success, logs an `auth.signed_in` event with `oid`, `tid`, `username`, `amr` (PII-sensitive bits only; no tokens), then 302s to `entra.postLogoutRedirectUri`.
## Decisions worth flagging
- **`postLogoutRedirectUri` reused as the SPA root URL.** Semantically a tiny stretch (its OIDC role is the post-logout destination) but the value is the same. Avoids one more env var until / unless the two URLs need to diverge.
- **Cookie cleared FIRST**, before any branching. Single-use is a property we want guaranteed regardless of which path exits the handler — overlap with a parallel /login from the same browser session would otherwise leak a usable cookie.
- **`auth.signed_in` logged via Pino, not via the audit module.** ADR-0013 wants this in the audit table; pairing audit with the session that ships in the next PR keeps the audit row carrying a `session_id` (otherwise it'd reference a "phantom" auth event with no follow-up).
- **`amr` non-empty is the BFF's check; the Conditional Access policy is what enforces "MFA happened".** ADR-0011 explicitly factors it this way — empty `amr` would indicate a policy misconfiguration where MFA never fired.
## Verification
- `nx run-many -t lint test build --projects=portal-bff` — green.
- **52 / 52 specs** (was 39; +13 across the new completeFlow branches and callback branches).
- Service spec covers happy path + 6 failure modes (state mismatch, flow expired, amr missing, MSAL throws, MSAL returns null, oid claim missing).
- Controller spec covers happy redirect, Entra error, missing cookie, AuthCodeFlowException branch, missing query, malformed cookie.
## Manual smoke test (end-to-end)
1. `apps/portal-bff/.env` carries real `ENTRA_*` + `SESSION_SECRET`.
2. `nx serve portal-bff` and `nx serve portal-shell`.
3. Open `http://localhost:3000/api/auth/login` → redirects to Entra.
4. Authenticate. Entra redirects to `http://localhost:3000/api/auth/callback?code=…&state=…`.
5. BFF processes; redirects to `http://localhost:4200/`. Pino log shows `auth.signed_in` with the user's `oid`, `tid`, `username`, `amr`.
6. Tamper test: open the link again, hand-edit the `state=` in the callback URL → BFF redirects with `?auth_error=state-mismatch`.
## What this PR explicitly does NOT do
- **Persist a session.** The user is "authenticated" from the BFF's point of view (identity resolved + logged) but the next request lands anonymous. Closes in the Redis sessions PR per ADR-0010.
- **Audit log entry.** Pairs with sessions so the row carries a `session_id`.
- **Logout / `/me`.** Land after sessions.
---------
Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr>
Reviewed-on: #107
|
||
|
|
9443a52bb7 |
chore(brand): swap header logo to a real svg + ship favicons for portal-admin (#106)
## Summary Two related brand-asset cleanups, bundled per request: ### portal-shell — header logo - Replace [`apf-small.png`](apps/portal-shell/public/logos/) (a 7.6 KB raster we'd extracted from the original PNG-in-SVG and re-encoded through sharp) with [`apf-logo.svg`](apps/portal-shell/public/logos/apf-logo.svg) — an actual 1024×1024 vector export (2.1 KB). Sharper at any density, smaller payload, no rasterisation artefacts when zoomed. - [`header.html`](apps/portal-shell/src/app/components/header/header.html) swaps `<img src=…>` accordingly. - The wide-format `apf-portal.svg` stays in place for future surfaces (login splash, etc.). ### portal-admin — favicons + PWA manifest Mirrors what PR #84 did for `portal-shell`: - Copy the six favicon images (`favicon.ico`, `favicon.svg`, `favicon-96x96.png`, `apple-touch-icon.png`, `web-app-manifest-{192,512}.png`) into [`apps/portal-admin/public/favicons/`](apps/portal-admin/public/favicons/). Single visual identity across the two apps. - Customise [`site.webmanifest`](apps/portal-admin/public/favicons/site.webmanifest) for admin: `name: "APF Portal Admin"`, `short_name: "Admin"`. Everything else (icons, theme-color, display) stays identical to portal-shell's manifest. - Wire the `<link>` block + `<meta name="theme-color">` in [`apps/portal-admin/src/index.html`](apps/portal-admin/src/index.html). - Remove the obsolete top-level `apps/portal-admin/public/favicon.ico` — now under `favicons/`, served via `<link rel="shortcut icon">`. ## Decision worth flagging **Assets duplicated rather than shared via a lib.** Both apps ship their own copy of the 7 favicon files (~120 KB binary each). The alternative — a `shared-assets` (or extended `shared-tokens`) lib with the assets glob copied into each app's `dist/` at build time — is the architecturally tidier path, but introduces a build-config change with no real payoff at our scale. Revisit if a third surface (e.g., a future static landing page) ends up needing the same set. ## Verification - `nx run-many -t lint test build --projects=portal-shell,portal-admin` — green. - Both `dist/apps/{portal-shell,portal-admin}/browser/{en,fr}/favicons/` ship the seven expected files. - Admin's emitted `site.webmanifest` carries `"name": "APF Portal Admin"`. - Admin's emitted `index.html` carries the full `<link>` block + `theme-color` meta. - Portal-shell ships `apf-logo.svg` in its `logos/` folder per locale; `apf-small.png` is gone. ## Test plan - [x] Lint + test + build green. - [x] Built outputs spot-checked (assets ship, manifest text correct, index.html wired). - [ ] Manual: `nx serve portal-shell` → header shows the new SVG logo crisp at 1x / 2x / 3x DPR. - [ ] Manual: `nx serve portal-admin` → tab favicon visible, dev tools → Application → Manifest shows "APF Portal Admin" with no errors. - [ ] Manual: install portal-admin as a PWA from a Chromium browser → the install dialog reads "Install APF Portal Admin", home-screen icon uses the same family as portal-shell. --------- Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr> Reviewed-on: #106 |
||
|
|
0eb404d111 |
feat(portal-bff): /auth/login route — pkce flow start + signed cookie (#105)
## Summary
Third step of ADR-0009 wiring. Adds the first OIDC route, `GET /api/auth/login`: it 302s the browser to Entra's authorize endpoint with a freshly-generated state + PKCE challenge, and stashes the matching `{state, codeVerifier}` payload in a short-lived signed cookie so the next-PR callback can verify the round-trip.
## What lands
- **Cookie infra**: `cookie-parser` + `@types/express` deps; `main.ts` mounts the cookie middleware with the `SESSION_SECRET` signing key. Signed cookies are now available via `req.signedCookies` for the upcoming callback.
- **[`.env.example`](apps/portal-bff/.env.example)** promotes `SESSION_SECRET` from a future-vars comment into an active section, with a one-liner showing how to generate 32 random bytes.
- **[`check-session-secret.ts`](apps/portal-bff/src/config/check-session-secret.ts)** — boot-time guard: refuses to start if `SESSION_SECRET` is unset, still the .env.example placeholder, or decodes below 32 bytes of entropy. Same family as `check-database-url` / `check-entra-config`.
- **[`auth.service.ts`](apps/portal-bff/src/auth/auth.service.ts)** — `beginAuthCodeFlow()` uses MSAL's `CryptoProvider` for canonical PKCE verifier / challenge generation and a fresh GUID state per call, calls `msal.getAuthCodeUrl()` with the configured redirect URI + OIDC scopes (`openid profile email` — no `offline_access` in v1), and returns `{ authUrl, preAuthPayload }`.
- **[`auth.cookie.ts`](apps/portal-bff/src/auth/auth.cookie.ts)** — `portal_pre_auth` name, 5-minute TTL, shared `CookieOptions`: `signed`, `httpOnly`, `sameSite: 'lax'` (lets Entra's cross-site top-level redirect back through), `secure` toggled by `NODE_ENV`.
- **[`auth.controller.ts`](apps/portal-bff/src/auth/auth.controller.ts)** — `@Controller('auth') @Get('login')`: writes the cookie then 302s. Thin shell around the service.
- **AuthModule** registers the new controller + service alongside the existing `ENTRA_CONFIG` and `MSAL_CLIENT` providers.
## Decisions worth flagging
- **Scope deliberately stops before the callback.** It's the next PR. Clicking `/auth/login` today round-trips through Entra and lands on a 404 — bounded mid-state, documented in the commit and here.
- **State + verifier in the cookie, not in Redis.** Keeps `/login` stateless (no server-side store), which means the BFF stays horizontally scalable from day one without sticky-session config. The next-PR callback reads `req.signedCookies` to recover the payload.
- **`portal_pre_auth`, not `__Host-portal_pre_auth`.** `__Host-` mandates `Secure`, and local dev is HTTP. The prefix + `Secure: true` lands together with the production TLS hardening ADR.
- **No `offline_access` scope.** Sessions are short-lived (per ADR-0010); the user re-authenticates through Entra rather than the BFF refreshing tokens behind their back. Smaller token footprint, less code to write, easier to reason about.
- **5-minute cookie TTL.** Enough for the Entra round-trip (including a fresh MFA prompt), short enough that a stale cookie can't be replayed long after the user abandoned the flow.
## Verification
- `nx run-many -t lint test build --projects=portal-bff` — green.
- **39 / 39 specs** (was 30; +9 across `check-session-secret`, `auth.service`, `auth.controller`).
- The service spec mocks `getAuthCodeUrl`, asserts the redirect URI / scopes / S256 method, the state-verifier identity between the cookie payload and what's sent to Entra, and fresh-per-call replay protection.
- The controller spec asserts the cookie name + options + serialized payload and the 302 redirect.
## Manual smoke test (next PR completes the loop)
1. `apps/portal-bff/.env` has real `ENTRA_*` + `SESSION_SECRET`.
2. `nx serve portal-bff`.
3. `curl -i http://localhost:3000/api/auth/login` → 302 with `Set-Cookie: portal_pre_auth=…; HttpOnly; SameSite=Lax; Path=/`, `Location: https://login.microsoftonline.com/<tenant>/oauth2/v2.0/authorize?...`.
4. Open the `Location` in a browser, authenticate, Entra redirects to `http://localhost:3000/api/auth/callback?code=…&state=…` → 404 today, will be the next PR.
## Next PR on the auth track
`GET /api/auth/callback` — reads the signed cookie, verifies `state` matches, calls `acquireTokenByCode` with the stored verifier, validates the ID token (issuer, audience, exp, nonce, `amr` per ADR-0011), clears the pre-auth cookie, logs the resolved user identity, redirects to `/` (SPA). Still no session — that's the PR after.
---------
Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr>
Reviewed-on: #105
|
||
|
|
b7093d61de |
feat(portal-bff): msal confidential client provider in AuthModule (#104)
Second step of ADR-0009 wiring. AuthModule now exposes the `@azure/msal-node` confidential client alongside the parsed Entra config — the building block the upcoming OIDC routes inject to issue the auth-code URL, exchange the callback code for tokens, and acquire downstream tokens on behalf of the user. What lands: - `@azure/msal-node` added as a direct dependency (^5.2.1). - `apps/portal-bff/src/auth/msal-client.token.ts` — `MSAL_CLIENT` string token + `ConfidentialClientApplication` type re-export. Mirrors the `ENTRA_CONFIG` token shape from PR #102. - AuthModule grows a factory provider for `MSAL_CLIENT`: - Injects `ENTRA_CONFIG` + nestjs-pino `Logger`. - Builds a `ConfidentialClientApplication` with `clientId`, `authority`, `clientSecret` from the parsed config. - Wires `system.loggerOptions.loggerCallback` to forward MSAL's internal log lines into the Pino stream (per ADR-0012) — Error → logger.error, Warning → logger.warn, Verbose / Trace → logger.debug, Info → logger.log. PII logging is disabled by default so tokens / user identifiers never leak into our structured log records. - Sets MSAL's `logLevel` to Info — Pino's own threshold re-filters from there. - All MSAL log lines carry the `msal` Pino context for easy isolation in log queries. - `AuthModule.exports` extended to include `MSAL_CLIENT`. Verification: - `nx run-many -t lint test build --projects=portal-bff` — green. - 30/30 specs (was 29; +1 covering MSAL client construction). - New spec imports `nestjs-pino`'s `LoggerModule.forRoot({ pinoHttp: { level: 'silent' } })` to provide the same Logger the production app supplies via `ObservabilityModule`, without flooding test stdout. Two tests assert the provider tree resolves correctly (ENTRA_CONFIG + MSAL_CLIENT) and one re-checks the missing-env failure mode still propagates through the new factory. Construction is cheap — MSAL Node defers authority discovery to the first auth call — so the client is built eagerly at module init. The factory is injection-only; no MSAL methods get invoked yet. Routes land in the next PR. <!-- PR title format — becomes the squash-merge subject on main, validated by commitlint. <type>(<scope>): <short description> Examples: feat(portal-shell): add user-preferences panel skeleton fix(portal-bff): correct env var bracket access docs(decisions): add ADR-0018 for security baseline chore(deps): bump @nx/* to 22.7.2 Imperative mood, lowercase, no trailing period, target ≤ 70 chars. See docs/development.md §5 for the full convention (types, scopes). --> ## Summary ## Motivation ## Implementation notes ## Verification - [ ] `pnpm ci:check` green locally - [ ] `pnpm ci:audit` green (or pre-existing drift acknowledged) - [ ] Tested manually: - [ ] Architecture diagram updated (if `docs/architecture.md` was affected) - [ ] ADR amended or added (if a decision changed) ## Related --------- Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr> Reviewed-on: #104 |
||
|
|
abd651b697 |
chore(deps): update dependency @types/node to v24.12.4 (#103)
This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/node) ([source](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node)) | devDependencies | patch | [`24.12.3` -> `24.12.4`](https://renovatebot.com/diffs/npm/@types%2fnode/24.12.3/24.12.4) | --- ### 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: #103 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> |
||
|
|
58e3b65bd9 |
feat(portal-bff): entra config foundation — boot validator + auth module (#102)
## Summary First step of ADR-0009 wiring on the BFF: capture the Entra app-registration env vars in the boot pipeline so subsequent PRs can plug `@azure/msal-node` onto a typed, already-validated config without re-reading `process.env`. **No MSAL client, no OIDC routes, no session integration yet** — those land in follow-up PRs. ## What lands - **[`.env.example`](apps/portal-bff/.env.example)** promotes the Entra block from its previous "future-vars" comment stub to an active section. Six keys: - `ENTRA_INSTANCE_URL` — the Microsoft login endpoint (e.g. `https://login.microsoftonline.com/`). - `ENTRA_TENANT_ID`, `ENTRA_CLIENT_ID`, `ENTRA_CLIENT_SECRET` — the values from the Entra app-registration UI. - `ENTRA_REDIRECT_URI`, `ENTRA_POST_LOGOUT_REDIRECT_URI` — consumed by the OIDC routes in a follow-up PR. Multi-tenant `ENTRA_ACCEPTED_TENANT_IDS` stays in the future-vars comment until External ID activation (ADR-0008 phase 2). - **[`apps/portal-bff/src/config/check-entra-config.ts`](apps/portal-bff/src/config/check-entra-config.ts)** — boot-time validator mirroring `check-database-url.ts`. Verifies every required key is present, the instance URL is `https://` and ends with `/`, tenant + client IDs are UUIDs, none of them are the literal placeholder values from `.env.example`, and the two redirect URIs parse as URLs. Returns a typed `EntraConfig` object with a pre-computed `authority` field (`${instanceUrl}${tenantId}`) so the future MSAL factory does not re-derive it. - **[`auth.module.ts`](apps/portal-bff/src/auth/auth.module.ts)** — `AuthModule` whose v1 surface is one provider: the parsed `EntraConfig` keyed by the `ENTRA_CONFIG` injection token. Factory delegates to `assertEntraConfig()`. Non-global on purpose — consumers state intent by importing the module. - **Bootstrap wiring** — `main.ts` calls `assertEntraConfig()` alongside `assertDatabaseUrl()` so misconfiguration fails fast at boot rather than mid-request (per ADR-0018 §"BFF env-var loading"). `AppModule` imports `AuthModule`. ## Naming choice Chose `ENTRA_*` rather than `AZURE_AD_*` to align with the ADR text (Microsoft Entra ID, post-2023 rebrand). The values you copy from the Entra app-registration UI go into `apps/portal-bff/.env` (git-ignored). ## Decisions worth flagging - **Validator called twice** — once in `main.ts` (boot-time fail-fast) and once in the `AuthModule` factory (to obtain the value for DI). Both reads are idempotent and trivially cheap. The duplication is intentional: boot-time gives a clear, pre-NestFactory error; the factory call surfaces the typed value to consumers. - **No `@azure/msal-node` dependency added yet** — introducing the dep without a consumer would be a smell. Lands in the next PR alongside the MSAL client factory. - **Pre-computed `authority`** in the parsed config rather than letting each MSAL consumer concatenate `instanceUrl + tenantId`. One place to change if the multi-tenant authority (`/organizations`, `/common`) replaces the tenant-scoped one when External ID activates. ## Verification - `nx run-many -t lint test build --projects=portal-bff` — green. - **29 / 29 specs** (was 20; +9 from the new entra-config spec + auth.module spec). - Boot smoke test (manual): with the placeholder values in `.env.example`, `nx serve portal-bff` aborts immediately with `ENTRA_CLIENT_ID is still the .env.example placeholder (…)`. With real values in a local `.env`, the BFF starts normally. ## Test plan - [x] Lint + test + build green. - [x] Validator unit-test covers happy path + every documented failure mode. - [ ] Manual: drop the real Entra values you obtained into `apps/portal-bff/.env`, `nx serve portal-bff` boots clean. - [ ] Manual: temporarily blank out one of the four `ENTRA_*` keys → BFF aborts at boot with a clear message naming the missing key. ## Next PRs on the auth track 1. Install `@azure/msal-node`, add the `MsalConfidentialClient` factory provider in `AuthModule`, expose it via DI. 2. First OIDC routes: `/api/auth/login` (PKCE-initiated redirect to Entra) + `/api/auth/callback` (token exchange + ID-token validation, audit-logged, no session persistence yet). 3. Session persistence per ADR-0010 (Redis + AES-GCM, `__Host-portal_session` cookie). Closes the auth loop. 4. RP-initiated logout, CSRF protection, route guards. --------- Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr> Reviewed-on: #102 |
||
|
|
cb69a692e7 |
chore(ts): drop deprecated baseUrl from tsconfig.base.json (#101)
## Summary Remove the deprecated `baseUrl: "."` from [`tsconfig.base.json`](tsconfig.base.json). TypeScript 5.x flags it as deprecated; it stops functioning in TS 7.0. Since TS 5.0, `paths` keys are resolved relative to the tsconfig file where they are declared when `baseUrl` is absent — and the repo root is exactly what `baseUrl: "."` was pointing at. ## Why this is a no-op Our `paths` entries already start with `./libs/shared/.../src/index.ts` (relative to the repo root, which is `tsconfig.base.json`'s directory). With or without `baseUrl: "."`, the resolver lands on the same files. No other tsconfig in the workspace declares `baseUrl` (only `tsconfig.base.json` did), and the only path imports across the codebase are the workspace aliases (`shared-ui`, `shared-state`, `shared-tokens`, `shared-util`, `feature-auth`). `rootDir: "."` stays — it is independent of the deprecation and removing it would change `dist/` layout semantics across the workspace. Revisit only if a future TS version flags it. ## Verification `pnpm exec nx run-many -t lint test build --projects=portal-shell,portal-admin,shared-ui,shared-state,shared-tokens,shared-util` — green across 6 projects. The IDE deprecation warning is gone. --------- Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr> Reviewed-on: #101 |
||
|
|
d962be838a |
feat(portal-admin): skeleton app per ADR-0020 (#100)
## Summary
First step of the admin track per ADR-0020. Scaffold the second Angular SPA in the workspace alongside `portal-shell`, with the architectural decisions from the v1 ADRs already wired so subsequent feature PRs can drop straight into modules.
## What lands
- **App generated** via `nx g @nx/angular:application` (with the matching e2e project skeleton). Standalone, zoneless-ready, prefix `app`, scss component styles, css root stylesheet, no SSR.
- **Tags** `scope:portal-admin, type:app` — module-boundary lint now treats `portal-admin` distinctly from `portal-shell` and lets it depend only on `scope:portal-admin` + `scope:shared` libs.
- **Tailwind v4 + brand tokens**: `.postcssrc.json`, `@import 'tailwindcss'`, class-based dark variant, and `@import '../../../libs/shared/tokens/src/brand-tokens.css'` — identical visual baseline to portal-shell.
- **i18n config mirrors portal-shell** (per ADR-0019): sourceLocale `en`, target `fr`, baseHref `/{locale}/` per locale, `@angular/localize/init` polyfill, `localize: ["en", "fr"]` on production, `i18nMissingTranslation: "error"` so CI blocks any PR that adds a marker without translating it. Seed [`messages.fr.xlf`](apps/portal-admin/src/locale/messages.fr.xlf) carries the four marked strings used today.
- **Budgets** relaxed to **500 KB gzip initial** per ADR-0020 §"Performance budgets" (vs 300 KB for portal-shell).
- **Skeleton home page** at `/` — `<h1>` + intro paragraph + "Skeleton — coming soon" chip in the brand-accent palette. All marked for i18n.
- **Skip-link landmark** (WCAG 2.4.1) with the same component-scoped scss as portal-shell.
- **Wildcard catch-all route** → bounces unknown paths to home (same defensive pattern as PR #96 on portal-shell).
- **Dev server on port 4300** (vs 4200 for portal-shell) — both can run in parallel.
- **`serve-static` target without `spa: true`** — locale-prefixed routing in production is handled by the reverse proxy (per ADR-0019), same as portal-shell since PR #92.
## CLAUDE.md
Picked up the second app in the **Naming** entry and the **Commands** snippet (`<app>` is now one of `portal-shell`, `portal-admin`, `portal-bff`).
## Verification
- `pnpm exec nx run-many -t lint test build --projects=portal-shell,portal-admin,shared-ui,shared-state,shared-tokens` — green.
- `portal-admin` test: 1 spec (skip-link + main landmark present).
- Production build of `portal-admin`: **62 KB gzip initial per locale** (vs 500 KB budget — plenty of headroom for the upcoming modules).
- Both `dist/apps/portal-admin/browser/{en,fr}/` emit with the right `<html lang>` and `<base href>`. FR bundle contains `"Administration"` / `"Squelette"` / `"bientôt disponible"` — translations applied.
- portal-shell production build unchanged.
## Out of scope (each its own follow-up PR)
- Admin app shell (header / sidebar / footer with "Admin" badge — likely sharing graduated primitives plus admin-specific bits).
- OpenTelemetry tracing setup (`service.name=portal-admin` per ADR-0012).
- `environment.ts` wiring (per ADR-0018) for admin-specific endpoints.
- BFF `AdminModule` + `AdminRoleGuard` + smoke `/api/admin/me` (per ADR-0020 §"Auth").
- First functional module — CMS / menu / users / audit viewer.
## Test plan
- [x] Lint + test + build green across the workspace.
- [x] Per-locale production build emits both folders with correct metadata.
- [ ] Manual: `pnpm exec nx serve portal-admin` boots on `:4300`, smoke page renders.
- [ ] Manual: prod build + `pnpm exec nx run portal-admin:serve-static` → `http://localhost:4300/en/` and `/fr/` show the placeholder with the matching language.
---------
Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr>
Reviewed-on: #100
|
||
|
|
8329fa133d |
refactor(libs): graduate Icon / LayoutStateService / brand tokens to libs/shared/* (#99)
## Summary
Per ADR-0020 §"Shared-libs graduation": the three primitives that `portal-shell` (today) and `portal-admin` (next) will both share move out of `apps/portal-shell/src/` and into the workspace libs. Mechanical move — no behaviour change, prepares the ground for the admin-app PR.
## Graduated
| Primitive | Old location | New location |
|---|---|---|
| `Icon` component (+ `IconName` type) | `apps/portal-shell/src/app/components/icon/` | [`libs/shared/ui/src/lib/icon/`](libs/shared/ui/src/lib/icon/) |
| `LayoutStateService` (+ `ThemeMode` type) | `apps/portal-shell/src/app/state/` | [`libs/shared/state/src/lib/`](libs/shared/state/src/lib/) |
| Brand-palette `@theme` block | `apps/portal-shell/src/styles.css` | [`libs/shared/tokens/src/brand-tokens.css`](libs/shared/tokens/src/brand-tokens.css) |
## Notable changes
- **Icon selector renamed `app-icon` → `lib-icon`.** Required by the lib's ESLint `@angular-eslint/component-selector` rule (prefix `lib` for shared libs). All ~20 usages in `portal-shell` templates updated in one sweep.
- **New `shared-state` library** generated via `nx g @nx/angular:library libs/shared/state`. Mirrors the existing `feature-auth` / `shared-ui` shape (vite + Angular + spec setup). `tsconfig.base.json` path alias `shared-state` added by the generator.
- **Lib tags rebalanced** to satisfy the module-boundary lint rule:
- `shared-state`: new lib → `scope:shared, type:shared`.
- `shared-ui`: was `scope:portal-shell` (scaffold default) → `scope:shared, type:shared`.
- **`shared-tokens` is now CSS-only.** The placeholder TS function is gone; `index.ts` is an empty `export {}` with a TODO note. Vitest config flips `passWithNoTests: true` so the test target stops failing on the empty lib.
- **`portal-shell`'s `styles.css`** drops the inline `@theme {}` block and instead `@import`s `../../../libs/shared/tokens/src/brand-tokens.css`. Both apps will read the same source.
- Placeholder scaffolded files in `shared-ui/src/lib/shared-ui/` removed.
## Verification
- `nx run-many -t lint test build` across `portal-shell, shared-ui, shared-state, shared-tokens` — green.
- Tests redistributed: **portal-shell 28**, **shared-state 9**, **shared-ui 3**, **shared-tokens 0** = 40 total (same as before the move).
- Production build: **128.7 KB gzip** initial per locale (was 128.5 KB on `main` — noise-level delta).
- Brand-color CSS variables (`--color-brand-primary-500: #12546c`, …) still inlined in the produced `styles-*.css`.
- `dist/.../browser/{en,fr}/` emitted as expected.
## What this PR explicitly does NOT do
- Move other components (Header, Sidebar, Footer, ThemeSwitcher, LocaleSwitcher). Those are app-specific shell concerns; if `portal-admin` ends up needing them, they graduate in their own PR.
- Set up `portal-admin`. That's the next PR on the admin track — and now imports from `shared-ui` / `shared-state` / `shared-tokens` will Just Work.
- Refactor `feature-auth`'s tagging. It still says `scope:portal-shell`; revisit when the auth implementation actually lands and the dual-audience design (per ADR-0008) makes the right scope obvious.
## Test plan
- [x] Per-project run-many — green.
- [x] Production build emits both locales with brand tokens applied.
- [ ] Manual: `pnpm exec nx serve portal-shell` — the dev experience is unchanged.
- [ ] Manual: serve-static + locale switcher / theme switcher / sidebar collapse / navigation — all the behaviours the moved primitives drive still work in production.
---------
Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr>
Reviewed-on: #99
|
||
|
|
99522540a5 |
feat(portal-shell): ci gate — fail prod build on missing translations (#98)
## Summary
Set `"i18nMissingTranslation": "error"` on the production build configuration in [`apps/portal-shell/project.json`](apps/portal-shell/project.json). The Angular CLI checks every `<source>` extracted from `i18n` markers and `$localize` calls against the loaded translation file (`messages.fr.xlf`); with the flag on `error`, the build now **fails** when a target is missing instead of silently falling back to the source text in the FR bundle.
Closes the loop on ADR-0019 §"Confirmation" — the final piece of the i18n track that does not depend on the BFF route + cookie work (deferred to the auth-flow chantier).
## Gate mechanics
- `pnpm ci:check` runs `nx affected -t ... build`, which uses the production configuration by default. A PR that adds an `i18n` marker without updating `messages.fr.xlf` fails its build job → merge blocked.
- The default build configuration is unchanged (`production`), so no other CI plumbing is needed.
## Verification
Locally, temporarily inserting an unmarked-in-fr string:
```html
<span i18n="@@sanity.test.missingTranslation">A string with no FR translation</span>
```
then running `pnpm exec nx build portal-shell --configuration=production` produces:
```
ERROR: No translation found for "sanity.test.missingTranslation"
("A string with no FR translation").
Application bundle generation failed.
NX Running target build for project portal-shell failed
```
…with a non-zero exit code. The patch was reverted before commit.
## Test plan
- [x] `pnpm exec nx run-many -t lint test build --projects=portal-shell` — green (40 / 40 specs, build still passes on the current state).
- [x] Sanity-check that the gate actually fires when a translation is missing (above).
- [ ] CI: the next PR that adds an `i18n` marker without updating `messages.fr.xlf` should fail at the `build` step with the explicit error.
## What this PR explicitly does NOT do
- Catch **unmarked** source strings (e.g. a developer who forgets to add `i18n="@@..."` to a new template line). That would need a custom ESLint rule against the Angular template AST — a separate, smaller scope mentioned in ADR-0019 as a future refinement.
- Localise editorial / CMS content. Editorial copy comes from the BFF already localised; this gate covers only the developer-owned UI strings baked at build time.
- Add a similar gate to the i18n extraction (we could fail the build if `nx extract-i18n` produces diffs against the committed `messages.xlf`, but we don't commit `messages.xlf` today).
---------
Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr>
Reviewed-on: #98
|
||
|
|
fe180fd125 |
feat(infra): local serve-static profile — Caddy reverse proxy for the prod build (#97)
## Summary Add a Caddy reverse proxy behind a new `--profile serve-static` so a contributor can exercise the production build locally with the per-locale routing the on-prem reverse proxy will use (per ADR-0019). Closes the gap surfaced by PR #96: the locale switcher / accessibility route fusion / cookie plumbing all need a prod-faithful local setup, and `nx serve-static` falls short (no SPA fallback per locale, no smart `/` redirect, exposes the `http-server` directory-listing footgun we hit during the perf-gate fix in PR #92). ## What lands - **[`infra/local/Caddyfile`](infra/local/Caddyfile)** — explicit `route` block: - `GET /` → 302 to `/{locale}/` based on `Accept-Language`, falling back to `/fr/` (APF audience). - `/fr/*` → `dist/.../browser/fr/` with SPA fallback to `fr/index.html`. - `/en/*` → mirror. - Catch-all → 302 to `/fr/`. - **[`infra/local/dev.compose.yml`](infra/local/dev.compose.yml)** — new `serve-static` service on the `serve-static` profile. Bind-mounts the Caddyfile and `dist/apps/portal-shell/browser/` read-only. Port 4200, overridable via `SERVE_STATIC_PORT`. - **[`infra/local/.env.example`](infra/local/.env.example)** — adds `SERVE_STATIC_PORT=4200`. - **[`infra/local/dev.sh`](infra/local/dev.sh)** — registers `serve-static` in `ALL_PROFILES` so `dev.sh down|status|logs` catches the new container, and `dev.sh up serve-static` works. - **[`infra/README.md`](infra/README.md)** — file row, workflow snippet, cheat-sheet row, and a service-endpoint row with the `nx build … -c=production` prerequisite called out. ## Workflow ``` pnpm exec nx build portal-shell --configuration=production ./infra/local/dev.sh up serve-static open http://localhost:4200/ # → /fr/ or /en/ per Accept-Language ``` ## Decision worth flagging Used **Caddy** rather than nginx or Traefik. Reason: minimal Caddyfile, single binary, no daemon config drift, sensible defaults (TLS off explicitly for local-only). Same family of choice as the rest of `infra/local/` — small, single-purpose images. `redir` in a Caddyfile is **ambiguous** when the first arg starts with `/`: Caddy reads it as a path matcher rather than a redirect target. Using `redir * /fr/ 302` (explicit `*` matcher) avoids the gotcha. Documented inline in the Caddyfile via a comment block. ## What this PR explicitly does NOT do - Wire TLS. Local convenience only, binds to `localhost`. - Replace `nx run portal-shell:serve-static` (still used by Lighthouse CI in `ci:perf`). - Set the `__Host-portal_locale` cookie or honour it for the smart redirect. Cookie handling needs the BFF route (ADR-0019 future PR). - Land an on-prem reverse-proxy ADR. The on-prem infra ADR is phase 3b. ## Verified locally | Probe | Expected | Actual | |---|---|---| | `GET / -H 'Accept-Language: fr'` | 302 `/fr/` | ✓ | | `GET / -H 'Accept-Language: en'` | 302 `/en/` | ✓ | | `GET /unknown` | 302 `/fr/` | ✓ | | `GET /fr/deep/route` | 200 (SPA fallback to `fr/index.html`) | ✓ | | `GET /fr/favicons/favicon.svg` | 200 (asset under locale folder) | ✓ | | `/fr/index.html` markup | `lang="fr"`, `<base href="/fr/">` | ✓ | | `/en/index.html` markup | `lang="en"`, `<base href="/en/">` | ✓ | ## Test plan - [x] `docker compose -f infra/local/dev.compose.yml --profile serve-static config` validates clean. - [x] `dev.sh up serve-static` brings the container up; `dev.sh status` lists it; `dev.sh stop serve-static` brings it down. - [x] Routing probes above all pass. - [ ] Manual: build + serve-static + click the locale switcher → URL becomes `/{other-locale}/`, the matching bundle boots, no console errors. (Verifies PR #95 + #96 end-to-end against a prod-faithful proxy.) - [ ] Manual: `/fr/accessibilite` → router-level redirect to `/fr/accessibility` (verifies PR #94 under SPA fallback). - [ ] Manual: `Accept-Language: en` in browser settings → root URL lands on `/en/`. --------- Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr> Reviewed-on: #97 |
||
|
|
d118d09aba |
fix(portal-shell): catch-all route prevents NG04002 on /fr in dev mode (#96)
## Reproducer 1. `pnpm exec nx serve portal-shell` (source-locale dev server, no `--localize`). 2. Open `http://localhost:4200/`, the EN home page renders. 3. Click the locale switcher's **Français** entry in the footer. Browser navigates to `/fr/`. The Angular CLI dev server applies its SPA fallback and serves the same source `index.html`. The source bundle boots with `<base href="/">`, the router tries to match the URL `/fr/`, finds no route, and rejects the bootstrap promise: ``` ERROR RuntimeError: NG04002: Cannot match any routes. URL Segment: 'fr' ``` ## Fix Add a `{ path: '**', redirectTo: '' }` catch-all to [`app.routes.ts`](apps/portal-shell/src/app/app.routes.ts). Unknown paths now bounce to home gracefully. ## Why this doesn't break production Production builds with `--localize` emit one bundle per locale, each with its own `<base href="/{locale}/">`. The router never sees the locale segment — `/fr/foo` is normalised to `foo` before route matching, hitting the bundle's `foo` route. The wildcard only fires when **no** declared route matches, which is exactly what we want for genuine 404 paths in either mode. ## What this does NOT fix The locale switcher's underlying dev-mode limitation stands: under `nx serve` there is no per-locale bundle, so switching to FR from dev mode can only land back on the source-locale bundle. The fix turns the ugly bootstrap error into a silent bounce to home, so the dev iteration is no longer interrupted — but full locale switching still requires the production build (`nx run portal-shell:serve-static` or a real deploy). This matches the limitation already documented in PR #95. ## Test plan - [x] `pnpm exec nx run-many -t lint test build --projects=portal-shell` — green (40 / 40 specs unchanged). - [ ] Manual `nx serve` + click Français → URL becomes `/fr/`, page bounces back to `/` with no console error. - [ ] Manual prod build + serve-static, `/fr/` → loads FR bundle as before, no regression. - [ ] Manual prod build + serve-static, `/fr/does-not-exist` → router-level 404 catch redirects to `/fr/` (home), no NG04002. --------- Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr> Reviewed-on: #96 |
||
|
|
192cc483b6 |
feat(portal-shell): locale switcher in the footer (#95)
## Summary
Add a locale switcher (FR / EN) to the footer's right cluster, next to the accessibility link. Closes the user-facing i18n loop — the FR bundle has existed since the sweep PR but had no in-app entry point until now.
The switcher reads the active locale from `<html lang>` (set per locale by the build), shows the native name + a globe + chevron-down chip, and on selection rewrites the URL prefix (`/en/...` ↔ `/fr/...`) and hard-refreshes so the right bundle boots — per ADR-0019.
## Architecture
Same pattern as the theme switcher:
- **`@angular/cdk/menu`** for the trigger + roving-focus menu + escape / click-outside dismissal.
- **`ViewEncapsulation.None`** because the menu opens in an overlay portal outside the component host — BEM-style class names (`.locale-switcher__*`) keep the global emissions contained.
- Each menu item carries `[attr.lang]="locale.code"` so screen readers pronounce the native names correctly.
## Decisions worth flagging
- **Locale display names ("Français", "English") are NOT i18n-marked.** Universal switcher convention: each language is always shown in its own language. Translating them would defeat the purpose for someone trying to switch *away* from the active locale they can't read.
- **No backend, no cookie, no smart `/` redirect — yet.** The URL prefix is the source of truth in v1: the next visit lands on the same locale because the URL says so. The `__Host-portal_locale` cookie + the BFF route at `/api/preferences/locale` + the smart `/` redirect described in ADR-0019 wait for the auth flow to bring the BFF online.
- **Dev-mode limitation, accepted.** Under `nx serve`, the dev server has no locale prefix in the URL — clicking the trigger lands on a non-existent path. The switcher works against the production build (`nx run portal-shell:serve-static` or any real deploy). This matches ADR-0019: dev = source locale, locale switching is a built-bundle concern.
- **Touch target.** Visible height stays at ~28 px to fit the 40 px footer; vertical padding extends the tap area to **44 px**, meeting the ADR-0016 AAA minimum without inflating the footer.
## Translation choices
Two new i18n keys:
| Key | Source (EN) | Target (FR) |
|---|---|---|
| `@@locale.trigger.aria` | `Language: <name> (change language)` | `Langue : <name> (changer de langue)` |
| `@@locale.menu.aria` | `Language` | `Langue` |
## Test plan
- [x] `pnpm exec nx run-many -t lint test build --projects=portal-shell` — green. **40 / 40 specs** (+5: four new for `LocaleSwitcher`, one for the footer embedding).
- [x] Production build emits both locales; spot-checked the FR bundle for "Langue", "changer de langue", and the absence of "Language:" leakage.
- [ ] Manual: build prod + serve-static → on `/en/`, click switcher → lands on `/fr/`; widget shows "Français"; reload stays in FR.
- [ ] Manual: keyboard the trigger → ENTER opens, arrows navigate, ENTER selects, ESC closes; focus returns to trigger on close.
- [ ] Manual: screen reader announces both languages with the right pronunciation (`<button lang="fr">Français</button>` is announced with the FR voice).
- [ ] Manual: query/hash preserved across switch (`/en/accessibility?foo=bar` → `/fr/accessibility?foo=bar`).
## What this PR explicitly does NOT do
- BFF route `/api/preferences/locale` + `__Host-portal_locale` cookie.
- Smart `/` redirect (cookie → Accept-Language → fr) — that's reverse-proxy / BFF work.
- CI gate on missing translations.
---------
Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr>
Reviewed-on: #95
|
||
|
|
8f84cc6389 |
feat(portal-shell): collapse accessibility routes into one i18n-marked route (#94)
## Summary
Continue ADR-0019: replace the `/accessibility` + `/accessibilite` twin routes with a single canonical route whose content is i18n-marked in the template. The per-locale build (en/fr) already inlines the right copy — the route-data + `copy()` service indirection is no longer carrying its weight.
## What changes
- **`AccessibilityStatement`** loses its `ActivatedRoute` injection, the `Lang` discriminator, and the `COPY` lookup table. The component is now a plain shell over the template.
- **`accessibility.html`** carries the title + intro + status panel as `i18n="@@page.accessibility.*"` markers. Six new trans-units in [`messages.fr.xlf`](apps/portal-shell/src/locale/messages.fr.xlf) provide the French copy — verbatim from the old `COPY.fr` block, so the page reads the same in FR as before.
- **`app.routes.ts`** declares the single canonical route at `path: 'accessibility'` and keeps `/accessibilite` alive as a `redirectTo: 'accessibility'`. Drops `data: { lang: ... }` — no longer consumed.
- **`footer.html`** collapses the dual link into one i18n-marked link (`@@footer.accessibilityLink`). EN bundle reads "Accessibility statement"; FR bundle reads "Déclaration d'accessibilité".
## Decision worth flagging
The path stays in English across both locales for now: `/en/accessibility` and `/fr/accessibility`. Translating route *segments* (`/fr/declaration-d-accessibilite`) needs either a custom URL serializer or per-locale route trees — not worth the complexity at this scale. The page title and the link label already differ per locale via i18n, which is what's actually visible to users.
The historical `/accessibilite` path keeps working via the route-level redirect. Drops out of the codebase once analytics confirm no traffic reaches it.
## Test plan
- [x] `pnpm exec nx run-many -t lint test build --projects=portal-shell` — green. **35 / 35 specs** (was 36; the obsolete `lang`-fallback test is removed).
- [x] Production build emits both locales. FR bundle contains `Statut`, `Déclaration`, the FR intro / panel bodies. No leftover English on swept strings.
- [x] `extract-i18n` clean (49 unique units now: +5 for `page.accessibility.*` + `footer.accessibilityLink`, −0; the old route-data `lang` markers were not i18n).
- [ ] Manual: serve-static then `/en/accessibility` and `/fr/accessibility` render their respective content; `/fr/accessibilite` 301-redirects to `/fr/accessibility`.
- [ ] Manual: footer shows one link, locale-aware ("Accessibility statement" / "Déclaration d'accessibilité").
- [ ] Manual: browser tab title flips between bundles ("Accessibility statement · APF Portal" / "Déclaration d'accessibilité · Portail APF").
## What this PR explicitly does NOT do
- Translate the URL path segment (next ADR-only refinement if needed).
- Add the locale switcher in the footer — that's the next PR on the i18n track.
- Wire a CI gate on missing translations.
---------
Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr>
Reviewed-on: #94
|
||
|
|
65fae7f963 |
feat(portal-shell): i18n string sweep — mark UI strings + FR translations (#93)
## Summary
Continue ADR-0019 implementation. Mark every UI string surfaced by the shell with `i18n="@@id"` (templates) or `$localize` (TypeScript), and populate [`messages.fr.xlf`](apps/portal-shell/src/locale/messages.fr.xlf) with French translations. The production build now ships **two genuinely different bundles** under `dist/.../{en,fr}/`.
**43 trans-units** marked, grouped by feature:
| Surface | Strings |
|---|---|
| `app.html` | skip-link |
| `header.html` | wordmark, search label + placeholder, action button aria-labels, user-menu placeholder |
| `sidebar.ts` + `.html` | menu groups + items, aside / nav aria-labels, role badge, toggle button (Expand / Collapse aria + Collapse text) |
| `theme-switcher.ts` + `.html` | mode labels, menu aria, trigger aria (`Theme: <mode> (open menu)` with named placeholder) |
| `footer.html` | aria-labels, copyright (interpolation preserved) |
| `home.html` | welcome heading + intro + status widget labels |
| `app.routes.ts` | browser tab titles |
## Tooling
- Add `"@angular/localize"` to the `types` array in both [`tsconfig.app.json`](apps/portal-shell/tsconfig.app.json) and [`tsconfig.spec.json`](apps/portal-shell/tsconfig.spec.json) so TypeScript resolves the `$localize` global at compile time. Specs need it too — they evaluate the same component code paths.
- Extraction target (`nx run portal-shell:extract-i18n`) reports **44 messages** (43 unique IDs).
## Translation choices worth flagging
- **Wordmark**: "APF Portal" → "Portail APF". Same key used for the `/` browser tab title. The PWA manifest (`site.webmanifest`) stays "APF Portal" — manifest values are not bundled, they sit in the static assets and are language-neutral in v1.
- **System theme mode** → "Système".
- **"Anonymous"** role → "Anonyme"; **"Role:"** → **"Rôle :"** (French uses a non-breaking space before the colon — typographic convention, preserved in the XLIFF target).
- **Accessibility links in the footer stay bilingual.** Each carries its own `lang` attribute (`lang="en"` and `lang="fr"`). The dual-link pattern goes away in the upcoming route-fusion PR; until then it's the most honest stopgap.
- **Both accessibility routes share one title key** (`@@route.accessibility.title`). In the EN bundle, both display "Accessibility statement · APF Portal"; in the FR bundle, both display "Déclaration d'accessibilité · Portail APF". After route fusion only one route remains.
## Verification
- Production build: **129 kB gzip initial per locale** (vs 122 kB before). +7 kB absorbs the i18n marker metadata and the embedded translation data in the FR bundle. Well under the 300 KB budget.
- Spot-checked the FR bundle: no leftover English source text on any swept string, route title, or home page intro. The `Welcome to APF Portal` in the lazy `home` chunk shows "Bienvenue sur Portail APF" in FR.
- **36 / 36 specs unchanged.** They run in the source locale (`en`), so the English assertions still match. No spec edits needed.
- Lint clean.
## Out of scope (each its own follow-up PR)
- **Locale switcher in the footer** + `__Host-portal_locale` cookie + smart `/` redirect.
- **Collapse `/accessibility` + `/accessibilite`** into one localised route with locale-translated path segments.
- **CI gate** that fails the build on a missing translation. Once the sweep is reviewed, we add `nx build --localize` to `ci:check` and verify it rejects unsealed strings.
- **Accessibility page content localisation.** Its content is driven by a `copy()` service rather than i18n-marked templates — restructured during the route fusion.
## Test plan
- [x] `pnpm exec nx run-many -t lint test build --projects=portal-shell` — green (36 / 36 specs).
- [x] `pnpm exec nx run portal-shell:extract-i18n` — clean, 43 unique unit IDs.
- [x] Production build emits both locales; FR bundle contains "Tableau de bord", "Aller au contenu principal", etc.
- [ ] Manual: `pnpm exec nx serve portal-shell` shows the EN source. `pnpm exec nx build portal-shell --configuration=production && pnpm exec nx run portal-shell:serve-static` → open `http://localhost:4200/fr/` for FR, `/en/` for EN.
- [ ] Manual: every aria-label announced by a screen reader in FR build matches the French translation.
- [ ] Manual: browser tab title flips between bundles ("APF Portal" vs "Portail APF").
---------
Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr>
Reviewed-on: #93
|
||
|
|
04675b1b59 |
fix(ci): perf job — point Lighthouse at /fr/ and /en/, drop spa fallback (#92)
## Summary The previous PR (#91) enabled `--localize` on the production build, so the output layout became `dist/apps/portal-shell/browser/{en,fr}/` with **no top-level `index.html`**. The `perf` CI job broke in two places downstream: 1. **`nx run portal-shell:serve-static`** had `spa: true`. The `@nx/web:file-server` executor reads that as "copy `<staticFilePath>/index.html` to `404.html` for SPA fallback". The source file no longer exists, so the executor crashed with `ENOENT … copyfile … index.html` before opening the port. lhci then failed its healthcheck and exited 1. 2. **`lighthouserc.js`** was hitting `http://localhost:4200/`, which now lands on `http-server`'s directory listing (no index.html at that path). Even if the server had started, the audit would have measured the wrong page. ## What changes - **Drop `spa: true`** from the `serve-static` target in [`apps/portal-shell/project.json`](apps/portal-shell/project.json). Deep-link fallback in production is the reverse proxy's job (it routes `/{en,fr}/anything` to the matching `index.html`); `nx serve-static` is only used here for the perf gate and for local prod-build inspection of entry points. For deep-link testing in dev, `nx serve` is the right tool. - **Update [`lighthouserc.js`](lighthouserc.js)** `url` list to `['http://localhost:4200/fr/', 'http://localhost:4200/en/']`, matching the directive in ADR-0019 that both locales clear the same performance bar. ## Verification Local repro (against the merged plumbing PR's build): ``` $ pnpm exec nx run portal-shell:serve-static $ curl -s -o /dev/null -w '%{http_code}\n' http://localhost:4200/en/ # 200 $ curl -s -o /dev/null -w '%{http_code}\n' http://localhost:4200/fr/ # 200 ``` Served files have the right metadata per locale: ``` /tmp/probe-en.html: lang="en" <base href="/en/"> /tmp/probe-fr.html: lang="fr" <base href="/fr/"> ``` ## Side-effect to call out - `/en/deep/route` and `/fr/deep/route` now return 404 from `nx serve-static`. That's by design — Lighthouse only audits the root locale URLs, and the reverse proxy owns deep-link routing in production. - `http://localhost:4200/` returns http-server's directory listing under the new layout. Lighthouse doesn't hit it, so the perf gate is unaffected. We could disable the listing if it becomes a footgun. ## Test plan - [x] `pnpm exec nx run-many -t lint test build --projects=portal-shell` — green. - [x] Local `nx serve-static` + curl against `/en/` and `/fr/` returns the expected per-locale `index.html`. - [ ] CI: `pnpm ci:perf` runs through `serve-static` start → Lighthouse autorun (×3 per locale, ×2 locales = 6 audits) → assertions hold ≥ 90 on Performance for both. --------- Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr> Reviewed-on: #92 |
||
|
|
29d16c7527 |
feat(portal-shell): wire @angular/localize plumbing per ADR-0019 (#91)
## Summary
First implementation step of ADR-0019. Wire the `@angular/localize` plumbing into `portal-shell` so the next sweep PR can start marking UI strings without any infrastructure work.
## What changes
- **Promote `@angular/localize` to a direct dependency** (it was already a transitive via the Angular metapackage; promoting it makes the `init` polyfill explicitly resolvable from the project).
- **Configure the `i18n` block** in [`apps/portal-shell/project.json`](apps/portal-shell/project.json):
- `sourceLocale: { code: "en", baseHref: "/en/" }` — matches the project English-only rule.
- `locales.fr: { translation: "...messages.fr.xlf", baseHref: "/fr/" }` — single target locale for now.
- **Add the `init` polyfill** to the build target (`"polyfills": ["@angular/localize/init"]`).
- **Add an `extract-i18n` Nx target** that wraps Angular's `@angular/build:extract-i18n` executor and drops the source XLF next to the translation files.
- **Enable `--localize` on the production build** — `nx build portal-shell --configuration=production` now emits two folders side by side: `dist/apps/portal-shell/browser/en/` and `.../fr/`. Each carries its own `<html lang>` and `<base href>` per the ADR.
- **Seed an empty `messages.fr.xlf`** (XLIFF 1.2 skeleton with sourceLanguage="en" / targetLanguage="fr" and an inline editor convention note). The sweep PR drops `<trans-unit>` entries directly into the body block.
## Verification
```
dist/apps/portal-shell/browser/
├── en/
│ └── index.html ← <html lang="en">, <base href="/en/">
└── fr/
└── index.html ← <html lang="fr">, <base href="/fr/">
```
Until the sweep PR marks strings, both bundles ship the same English source text — that's expected and matches what the ADR calls out ("the FR bundle falls back to source text for every untranslated key").
## What this PR explicitly does NOT do
- **Mark UI strings.** Every `i18n` attribute / `$localize` call lands in the next PR. Pure infra commit here.
- **Locale switcher in the footer** + `__Host-portal_locale` cookie + smart `/` redirect. Lands once switching shows a meaningful difference.
- **Collapse `/accessibility` + `/accessibilite` into a single localised route.** Depends on marked text + localized route paths — sweep PR territory.
- **CI gate** that fails the build on missing translations. Lands when there are translations to be missing.
## Test plan
- [x] `pnpm exec nx run-many -t lint test build --projects=portal-shell` — green (36 / 36 specs unchanged).
- [x] `pnpm exec nx build portal-shell --configuration=production` — produces both locale folders with correct `<html lang>` and `<base href>`.
- [x] `pnpm exec nx run portal-shell:extract-i18n` — runs cleanly, reports `(Messages: 0)` as expected.
- [x] Production initial bundle: **123 kB gzip per locale** (vs 121 kB on `main`; +1.5 kB for the `@angular/localize` runtime polyfill). Both stay well under the 300 KB budget.
- [ ] Manual: `pnpm exec nx serve portal-shell` runs unchanged (source locale `en`, no `--localize` in dev for now).
---------
Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr>
Reviewed-on: #91
|
||
|
|
8f125d2a90 |
feat(portal-shell): wire environment.ts per ADR-0018 (#90)
## Summary First implementation step of ADR-0018. Create [`src/environments/environment.ts`](apps/portal-shell/src/environments/environment.ts) holding the two SPA per-environment values the ADR calls out — `bffApiBaseUrl` and `otlpEndpoint` — and replace the hard-coded URLs at the two SPA call sites that needed them. ## What changes - **New `environment.ts`** with dev defaults (`http://localhost:3000/api` and `http://localhost:4318/v1/traces`). Header comment links to ADR-0018, documents the constraint that per-environment siblings must share the same shape, and notes that nothing here is a secret (the SPA bundle is public). - **`observability/tracing.ts`** reads `environment.otlpEndpoint` for the exporter, and **derives** the `propagateTraceHeaderCorsUrls` regex from `environment.bffApiBaseUrl` — a future change to the BFF origin propagates `traceparent` to the right host automatically, no second edit needed. - **`home-status.service.ts`** builds `/health` as `${environment.bffApiBaseUrl}/health`. ## What this PR explicitly does NOT do - **No `environment.staging.ts` / `environment.prod.ts`** yet. The ADR says "ship later" for those, and the real prod / staging URLs are unknown until the infrastructure ADR lands. Dropping plausible-but-wrong URLs into the repo would be worse than waiting. - **No `fileReplacements` configuration in `project.json`** — it depends on the per-environment files existing. Wired in the same PR that introduces them. - **No BFF-side audit pool split** (`AUDIT_DATABASE_URL` validator, second Prisma client, boot-time UPDATE-rejection self-test). Also in the ADR's Confirmation list, but it touches `AuditModule` and deserves its own review. Separate PR. - **No `SERVICE_VERSION` wiring** in `tracing.ts`. Still hard-coded to `'dev'`; the build-time version source (same one that will feed the footer's dev-only version badge) is its own small chantier. ## Test plan - [x] `pnpm exec nx run-many -t lint test build --projects=portal-shell` — green (36 / 36 specs unchanged, no new tests needed). - [x] Production build size unchanged (121 kB gzip initial — `environment.ts` is one literal object inlined by the bundler). - [ ] Manual: `pnpm exec nx serve portal-shell` → home page loads, the health widget hits the BFF, Jaeger shows the SPA `document_load` + `fetch` + BFF child span trace. - [ ] Manual: temporarily change `bffApiBaseUrl` to `http://localhost:9999/api` → the fetch fails (expected), and the `traceparent` propagation regex no longer matches `:3000` (verifiable in Network panel — header is absent on cross-origin requests). --------- Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr> Reviewed-on: #90 |
||
|
|
c5e91f240b |
docs(decisions): add ADR-0019 i18n + ADR-0020 portal-admin (#89)
## Summary
Pure documentation PR. Two ADRs that answer the two strategic questions raised after the footer chantier:
- **[ADR-0019](docs/decisions/0019-internationalisation-angular-localize.md)** — how the portal handles multiple languages.
- **[ADR-0020](docs/decisions/0020-portal-admin-app.md)** — where portal administration lives.
Implementation will land across follow-up feature PRs, each consumable on its own.
## ADR-0019 — Internationalisation
**Decision:** `@angular/localize` in **build-time** mode, two locales (`fr` default served at `/`, `en` source). Path-based URLs always prefixed (`/fr/...`, `/en/...`); `/` smart-redirects via cookie → `Accept-Language` → `fr`. The locale switcher in the footer writes a `__Host-portal_locale` cookie and hard-refreshes to the matching bundle.
**Considered and rejected:**
- `@angular/localize` runtime mode (single bundle, higher LCP / payload cost).
- `@ngx-translate` / `transloco` (community libraries; tech-bar prefers Angular first-party for foundational primitives).
- Query-param URL strategy (fragile, weaker SEO, `<html lang>` becomes harder).
- Subdomain URL strategy (breaks `__Host-` cookie scoping from ADR-0010).
**Scope boundary:** UI strings owned by developers (templates + `$localize` in code). Editorial content (CMS-managed pages, news, etc.) is BFF-served already localised — that's the admin-app pipeline (ADR-0020), not `@angular/localize`.
**First sweep consequence:** the duplicate `/accessibility` + `/accessibilite` routes collapse to one Angular route with locale-translated paths.
## ADR-0020 — `portal-admin`
**Decision:** new Angular SPA `portal-admin` alongside `portal-shell`, sharing the existing `portal-bff` via `/api/admin/*` routes guarded by an Entra `admin` role plus `@RequireMfa({ freshness: 600 })` at the entry route. Distinct origin + cookie + session (`__Host-portal_admin_session`).
**v1 modules** (all four selected):
1. Editorial pages CMS (multilingual content, fed back to `portal-shell` via the BFF).
2. Sidebar menu management (activates the `requiredPermissions` field already on `MenuItem`).
3. User list (read-only).
4. Audit log viewer (consumes the `audit.events` table per ADR-0013, via the `audit_reader` role).
**Out of v1:** B2B invitations (stay in Entra Admin Center), feature flags (no substrate yet), CMS workflow / approval flows, theme customisation, live preview.
**Considered and rejected:**
- `/admin/*` lazy-loaded inside `portal-shell` (admin code in the same origin → weaker defense in depth, admin URL not IP-restrictable independently).
- Two SPAs **and** two BFFs (doubles infra at our scale — bricolage).
- Off-the-shelf admin tooling (Retool, etc. — escapes our security baseline).
**Performance budget for admin:** ≤ 500 KB gzip initial (vs 300 KB for `portal-shell`, per ADR-0017). Lighthouse Performance ≥ 85 on critical admin routes (vs ≥ 90 on `portal-shell`). Same a11y baseline (ADR-0016), same dark-mode support.
**Shared-libs graduation:** `Icon`, `LayoutStateService`, brand tokens, dark-mode SCSS helpers move from `portal-shell` to `libs/shared/{ui,state}` when both apps need them. Mechanical refactor; tracked as the first implementation PR.
## Implementation roadmap (out of scope of this PR)
ADR-0019:
1. Install `@angular/localize`, wire build target.
2. Mark every existing UI string in `portal-shell` with `i18n` + `@@id`; produce `messages.fr.xlf`.
3. Locale switcher in footer + `/api/preferences/locale` BFF route + smart redirect at `/`.
4. Collapse the duplicate accessibility routes into a localised single route, with 301s.
5. CI gate: `nx build portal-shell --localize` is added to `ci:check` and fails on missing translation.
ADR-0020:
1. `nx g @nx/angular:app portal-admin` skeleton.
2. Shared-libs extraction (`libs/shared/ui`, `libs/shared/state`).
3. BFF `AdminModule` + `AdminRoleGuard` + smoke `GET /api/admin/me`.
4. Admin shell (header / sidebar / footer with an "Admin" badge).
5. One PR per v1 module — suggested order: CMS pages → menu management → audit viewer → user list.
## Test plan
- [x] Both ADRs follow MADR 4.0.0 (frontmatter, sections, tags from the canonical vocabulary).
- [x] `docs/decisions/README.md` index updated in the same commit.
- [x] `CLAUDE.md` architecture summary picks up entries for both decisions and bumps the ADR coverage line to 0020.
- [ ] Read-through review — invite the project lead to push back on any decision before locking implementation.
---------
Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr>
Reviewed-on: #89
|
||
|
|
8b986f3dc3 |
feat(portal-shell): thin full-width footer with copyright + a11y links (#88)
## Summary
- Re-add a **40 px** (h-10) footer pinned to the bottom of the shell, spanning the full viewport width below header + sidebar + main.
- Hosts the **`© <year> APF France handicap`** copyright on the left and the FR + EN accessibility statement links on the right, separated by a thin divider.
- **Move the accessibility links out of the sidebar bottom.** Putting legal / compliance links in the footer matches universal web convention and keeps the sidebar focused on product navigation.
## Why footer rather than the help menu
The header's `circle-help` icon will eventually open a help menu (FAQ, keyboard shortcuts, contact support) — that's **product help**, not **legal / compliance**. Auditors (RGAA 4.1, EN 301 549) look for the accessibility statement in the footer; hiding it inside a help dropdown would hurt discoverability. The footer is the canonical home.
## Why both FR + EN stay visible
There is no language toggle yet — `@angular/localize` and its ADR are a separate chantier. Until then, exposing both languages prevents a francophone user from landing on the EN page (or vice versa) via a stale favourite. Once the locale switcher lands, the footer drops the link that doesn't match the active locale.
## Layout
```
:host flex column, h-100vh
├── app-header shrink-0, h-16
├── div.shell-body flex-1, flex row
│ ├── app-sidebar w-{64|16}, h-full (== shell-body)
│ └── main.shell-main flex-1, overflow-y-auto
└── app-footer shrink-0, h-10
```
The sidebar now sits *between* header and footer, so the collapse toggle stays flush above the footer at every viewport size. shell-main still owns its own vertical scroll so long content does not push the footer off-screen.
## What this PR reserves for later (placeholder)
- **Language toggle (FR / EN)** — lands once `@angular/localize` is in.
- **Dev-only version badge** — lands once `environment.ts` per ADR-0018 is wired up.
Both belong in the footer; the layout already has slots for them (center / right groupings).
## Accessibility
- `<footer aria-label="Page footer">` landmark.
- Inline text links inside `<nav aria-label="Legal">` with hover underline + visible focus ring (brand primary, 4 px offset). Inline-link exception applies to the 44×44 touch target (ADR-0016).
- Dark mode: white → `dark:bg-gray-900`, gray-500 text → `dark:text-gray-400`, brand-primary-500 hover → `dark:hover:text-brand-primary-300`.
## Test plan
- [x] `pnpm exec nx run-many -t lint test build --projects=portal-shell` — green (**36 / 36 specs**, +3 for the footer).
- [x] Production build: **121 kB gzip initial** (unchanged from main).
- [ ] Manual: footer pinned at the bottom in every viewport size; sidebar height tracks shell-body so the collapse button sits just above it.
- [ ] Manual: both `/accessibility` and `/accessibilite` links navigate correctly and get `aria-current="page"` when active.
- [ ] Manual: dark mode → footer surface flips with the rest of the shell.
- [ ] Manual: keyboard — Tab into the footer reaches each link, focus ring is visible, Shift+Tab walks back out.
---------
Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr>
Reviewed-on: #88
|
||
|
|
3a0a9c700d |
fix(portal-shell): dark mode actually applies to component-scoped surfaces (#87)
## Bug
Three component stylesheets (`app.scss`, `sidebar.scss`, `theme-switcher.scss`) carried `:where(.dark) &` rules to react to the `<html>.dark` class — the same pattern Tailwind uses internally. **Angular's emulated CSS encapsulation descends into `:where()` and rewrites its contents with the `_ngcontent-XXX` scoping attribute**, so the produced selector looked like:
```css
:where(.dark[_ngcontent-c0]) .shell-main[_ngcontent-c0] { ... }
```
`<html>` carries `.dark` but no `_ngcontent` attribute — the rule never matched.
Visible symptom: the main page background stayed light even when `.dark` was on `<html>` and every Tailwind `dark:` utility in the templates was working correctly. Sidebar hover/focus/active states and the theme-switcher menu surface had the same bug.
## Fix
- **`app.scss` and `sidebar.scss`** switch to `:host-context(.dark)`. The Angular compiler recognises this directive and expands it without forcing the ancestor (`<html>`) to carry the scoping attribute. Compiled output:
```css
.dark[_nghost-c0] .shell-main[_ngcontent-c0],
.dark [_nghost-c0] .shell-main[_ngcontent-c0] { ... }
```
The second selector matches `<html>.dark` as an ancestor of `<app-root>` (the host) — exactly what we want.
- **`theme-switcher` switches to `ViewEncapsulation.None`**. Its CDK menu opens in an overlay portal appended to `<body>` — outside the component's host subtree — so even `:host-context()` would miss it. With encapsulation disabled, the styles emit globally; the BEM-style class names (`.theme-switcher__menu`, `.theme-switcher__item`, ...) keep the rules contained without leaking.
## Drive-by
- Remove the right border on the `.header__logo-zone`. The visible hairline above the sidebar was extra noise — the alignment of widths already carries the visual relationship to the rail below.
## Why not also rip out the `dark:` Tailwind utilities used in the templates?
They work. They're emitted into the global Tailwind sheet, sit outside view encapsulation, and already handle the `.dark` ancestor lookup via their own selector (`:where(.dark, .dark *)`). The bug was specifically about component-authored CSS *inside* an SCSS file under emulated encapsulation.
## Test plan
- [x] `pnpm exec nx run-many -t lint test build --projects=portal-shell` — green (33 / 33 specs).
- [x] Inspect the produced CSS: `:host-context(.dark)` expands to both `.dark[_nghost-XXX]` and `.dark [_nghost-XXX]` selectors, no `_ngcontent` attribute leaked into the `.dark` portion.
- [ ] Manual: pick dark mode → `/` shows the dark page background; sidebar hover / active / focus visibly switches; theme-switcher menu opens with the dark surface.
- [ ] Manual: header no longer shows a vertical hairline between the logo zone and the search/actions cluster.
---------
Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr>
Reviewed-on: #87
|
||
|
|
0ae7e0e23d |
feat(portal-shell): light / dark / auto theme switcher (#86)
## Summary
- Add a header dropdown letting users pick **light**, **dark**, or **auto** (follow the OS) color schemes. Choice persists in `localStorage`, applies on next reload, and reacts live to OS theme changes when in `auto`.
- Built on `@angular/cdk/menu` for accessible roving focus, escape-to-close, and proper `menuitemradio` semantics on the three options.
- Apply `dark:` variants across the shell (header, sidebar, main bg) and the existing two pages. No semantic-token refactor yet — that belongs in a future ADR (`--color-surface-1`, `--color-text-1`, …).
## Architecture
- [`LayoutStateService`](apps/portal-shell/src/app/state/layout-state.service.ts) grows a `themeMode: signal<'light'|'dark'|'auto'>` alongside the existing `sidebarCollapsed`, plus an `effectiveTheme` computed that resolves `auto` against the system preference. A side-effect toggles the `.dark` class on `<html>`, so every `dark:` Tailwind utility flips at once.
- Tailwind v4 dark mode is rewired to **class-based** via `@custom-variant dark (&:where(.dark, .dark *));` in `styles.css`. This overrides the v4 default (`prefers-color-scheme` only) so the user's explicit override wins over the OS preference.
- The switcher trigger reflects the **selected** mode (sun / moon / monitor), not the effective theme — so users can tell which mode they're in even when `auto` happens to resolve to the same scheme as a manual pick.
## Side-edits in the same PR (already validated in the chat)
- **Logo asset.** Replace `apf-small.svg` (94 kB — a base64-PNG wrapped in SVG markup, not actually vector) with `apf-small.png` (144×144, 7.6 kB after `sharp --kernel lanczos3 --compressionLevel 9`). Header swaps to the PNG. The wide vector `apf-portal.svg` stays around for future surfaces that want the horizontal lockup.
- **Revert FR strings** that crept into the header template — project rule (CLAUDE.md) is English-only for source artefacts; FR localisation will happen properly via `@angular/localize` (separate ADR).
## Decisions worth flagging
- **Dropdown over segmented control.** The CDK Menu pays its weight: accessible by default (proper `aria-haspopup`, focus management, ESC handling, click-outside dismissal), reusable for future header menus (user, language, notifications), and one tidy primitive rather than three competing buttons.
- **`auto` is the default,** not `light`. Most users have an OS-level preference already; respecting it is the least surprising baseline.
- **`<html>.dark` class lives at the root,** not on `<app-root>`. That's the Tailwind convention and it means CDK overlay popups (the menu itself, future dialogs) inherit the right theme without extra wiring.
- **Bundle delta +21 kB gzip** (100 → 121 kB initial). All of it is `@angular/cdk/{menu,overlay,a11y}` and the dark CSS rules. We stay well under the 300 kB budget. The CDK is already on the architecture menu (ADR-0016 — *UI stack: Angular CDK + TailwindCSS*) so this is on-strategy spend.
- **No semantic tokens yet.** The dark variants use raw Tailwind gray ramps (`dark:bg-gray-900`, etc.) instead of a `--color-surface-1` / `--color-text-1` token layer. That keeps the change tractable for now; promotion to semantic tokens deserves its own ADR with the design team in the loop.
## Accessibility (ADR-0016)
- Menu trigger has `aria-haspopup="menu"`, `aria-label` announcing the current mode + "(open menu)".
- Menu uses `role="menu"`, items use `role="menuitemradio"` with `aria-checked` — assistive tech announces the selection state correctly.
- All interactive controls keep the 44×44 px touch target.
- `prefers-reduced-motion: reduce` already covered by the sidebar transitions; theme switcher has no animations of its own.
- Contrast: dark surfaces are gray-900 + gray-800 border / gray-100 text — passes WCAG AA. Brand primary shifted to the `300` step in dark mode so the active states keep contrast against gray-900.
## What this PR explicitly does NOT do
- Tokenise the palette into semantic surface / text / border roles (next iteration, ADR-led).
- Localise UI strings (separate `@angular/localize` ADR + PR).
- Animate the theme transition (FOIT-style flicker on toggle is acceptable in v1; we can soften later with a `color-scheme` CSS transition if it bothers users).
## Test plan
- [x] `pnpm exec nx run-many -t lint test build --projects=portal-shell` — green (**33 / 33 specs**, +8 for the theme work).
- [x] Production build: **121 kB gzip initial** (was 100 kB). Under the 300 kB budget.
- [ ] Manual: toggle each of the three modes → header / sidebar / main / cards switch surface colors instantly; trigger glyph updates.
- [ ] Manual: pick `auto`, change OS theme → UI follows live (Chrome DevTools → Rendering → "Emulate CSS media feature prefers-color-scheme").
- [ ] Manual: reload after each pick → the chosen mode is restored.
- [ ] Manual: keyboard the trigger → ENTER opens menu, arrow keys navigate, ENTER selects, ESC closes; focus returns to the trigger on close.
- [ ] Manual: Lighthouse accessibility on `/` in dark mode — score unchanged from light mode.
---------
Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr>
Reviewed-on: #86
|
||
|
|
b6aa17f6a0 |
feat(portal-shell): align header logo zone with the sidebar rail (#85)
## Summary - Split the header into two zones: a **logo zone** whose width tracks the sidebar (16 rem expanded / 4 rem collapsed), and the existing search + actions cluster on the right. - The logo glyph stays at both widths; the "APF Portal" wordmark hides when the rail collapses, mirroring the sidebar's icon-only mode. - Promote the sidebar's `collapsed` state to a new [`LayoutStateService`](apps/portal-shell/src/app/state/layout-state.service.ts) (Signals + `localStorage`) so header and sidebar share a single source of truth. ## Why a service rather than prop-drilling Two reasons: 1. The state is **shell-level**, not sidebar-internal — the header now reads it, and future surfaces (right rail, breadcrumbs, command palette) will too. Passing it down via inputs would force `<app-root>` to act as the owner and turn every intermediate component into a relay. 2. As we add other cross-cutting shell state (density, theme, panel pinning, RTL), they all belong in the same place. `LayoutStateService` is the natural collector and stays trivial as long as we don't over-broaden it. v1 ships with one signal — keeping it narrow. The service is `providedIn: 'root'` (singleton), Signals-only, and owns localStorage persistence — same UX as before, just relocated. ## Decisions worth flagging - **Widths stay duplicated between `header.scss` and `sidebar.scss`** (16 rem / 4 rem). Extracting a shared SCSS variable would be premature — only two callers, and the coupling is loud (cross-file comment in `header.scss`). If a third surface needs the same widths, we promote to a shared token. - **Border continuity.** The logo zone's right border and the sidebar's right border share the same x coordinate, so they read as one continuous vertical separator running the full shell height. Same `#e5e7eb` so the seam is invisible. - **Width transition** matches the sidebar's (`0.18s ease-out`) and is skipped under `prefers-reduced-motion: reduce`. - **Sidebar component lost its private state.** Its own signal + effect + storage glue is gone; it delegates reads to the service and the toggle click to `layout.toggleSidebar()`. Net `sidebar.ts` shrunk by ~15 lines. ## Test plan - [x] `pnpm exec nx run-many -t lint test build --projects=portal-shell` — green (25 / 25 specs, +6 for the new service spec + header zone tests). - [x] Production build under bundle budgets (100 kB gzip initial — +0.2 kB vs main). - [ ] Manual: load `/`, confirm the logo zone's right edge aligns exactly with the sidebar's right edge at both widths. - [ ] Manual: toggle the sidebar → both columns animate in sync; wordmark hides; logo glyph stays centered in the 4 rem zone. - [ ] Manual: reload after toggling → both header and sidebar restore to the persisted state. - [ ] Manual: `prefers-reduced-motion: reduce` → no width animation in either zone. --------- Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr> Reviewed-on: #85 |
||
|
|
29f79d677e |
feat(portal-shell): full favicon set + PWA manifest (#84)
## Summary - Replace the single root `favicon.ico` with a complete asset bundle under [`apps/portal-shell/public/favicons/`](apps/portal-shell/public/favicons/): SVG primary favicon, PNG fallback (96×96), legacy ICO, Apple touch icon (180×180), and a Web App Manifest with 192 / 512 maskable PNGs. - Wire the assets in [`index.html`](apps/portal-shell/src/index.html) via the standard `<link rel="icon|apple-touch-icon|manifest">` block and add a matching `<meta name="theme-color" content="#ffffff">`. - Set the manifest name to `"APF Portal"` (short_name `"Portal"`) so the PWA install banner and home-screen icon read consistently with the in-app header. ## Why it matters - **Modern favicons.** SVG is now the primary icon — vector-clean at every density, automatic dark-mode adaptation when the SVG carries `prefers-color-scheme` rules. PNG + ICO entries cover legacy and pinned-tab contexts. - **Installability.** With a valid manifest + 192/512 icons + `display: standalone`, the portal is installable on Android home-screens and as a desktop PWA in Chromium-based browsers, without shipping a service worker. - **Mobile chrome.** `<meta name="theme-color">` aligned with the manifest's `theme_color` tints the browser address bar and the PWA chrome to white — matching the app header. ## Decisions worth flagging - **Icons declared `purpose: "maskable"` only.** The PNGs were generated with the Android safe-zone padding. On Android they render correctly (cropped to the device's adaptive shape); on contexts that ask for `"any"` the browser falls back to the SVG / PNG / ICO entries from the `<link rel="icon">` block, so nothing breaks. If we later want a no-padding edge-to-edge variant for desktop, we can add a second icon entry with `purpose: "any"` and a different source. - **`theme_color: #ffffff` rather than brand teal.** The app header is white in the v1 design; tinting the mobile chrome to teal would create a visible seam at the top of the viewport. We can revisit if a darker header lands. - **Cache-buster query strings kept (`?v=20260511`).** Static `public/` assets are not hashed by Angular's build (only bundled JS/CSS are), so the explicit version stamp guards against stale caches on icon updates. The date matches the generation day. ## What this PR explicitly does NOT do - Ship a service worker / offline support (PWA installability does not require it; offline strategy is a separate decision and likely a future ADR). - Replace the SVG icon contents with a brand-tuned design — uses the existing generator output. - Wire a localized manifest (single `name` / `short_name`, no `lang` variant per locale). ## Test plan - [x] `pnpm exec nx build portal-shell --configuration=production` — green, 100 kB gzip initial (unchanged). - [x] All 7 assets ship to `dist/apps/portal-shell/browser/favicons/`. - [x] `index.html` in the production build contains every `<link>` + the `theme-color` meta. - [ ] Manual: tab favicon visible in Chrome / Firefox / Safari. - [ ] Manual: Chrome DevTools → Application → Manifest reports no errors and shows both 192/512 icons. - [ ] Manual: Chrome desktop install prompt offers "Install APF Portal". - [ ] Manual: Add-to-Home-Screen on Android shows the maskable icon clipped to the device's adaptive shape. --------- Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr> Reviewed-on: #84 |
||
|
|
3371fbd613 |
feat(portal-shell): app-shell layout with collapsable sidebar (#83)
## Summary - Replace the flat header+main+footer layout with a real app-shell: fixed header on top, collapsable sidebar + scrollable main below. Sidebar state (collapsed / expanded) persists across reloads via `localStorage`. - Introduce the APF brand palette as Tailwind v4 `@theme` tokens (primary teal `#12546c`, accent orange `#f7a919`) so every utility (`bg-brand-primary-500`, `text-brand-accent-400`, `ring-brand-primary-200`, …) is available from now on. - Add an `<app-icon>` façade backed by `lucide-angular` for v1. Logical kebab-case names already match the icomoon-sprite convention, so the future migration is a single-file change in `icon.ts` and consumers stay untouched. - Migrate the accessibility-statement links from the (now-deleted) footer to the bottom of the sidebar. ## Decisions worth flagging - **Static menu, permission-shaped data.** Items point to `#` placeholders in v1, except *Dashboard* which is `routerLink="/"` so the active-state styling is visible on the home page. The `MenuItem` shape already carries an optional `requiredPermissions: string[]` so the permission-aware filter (PR 2, alongside ADR-0009 auth) plugs in without restructuring. - **`<app-icon>` over direct lucide imports.** Consumers write `<app-icon name="bell">` rather than importing the lucide pascal-case symbol. When the icomoon sprite lands, only the registry in `icon.ts` changes — templates do not. - **Sidebar persistence via `localStorage`, not backend.** Zero round-trip, survives reloads, falls back gracefully when storage is blocked (private mode). Eventually mirrored server-side if the user-preferences feature lands. - **Footer removed entirely.** With the sidebar carrying the FR + EN accessibility-statement links and the role badge, the bottom rail no longer earned its vertical real estate. The version badge moved out for now; it will return as part of a debug/help menu when there's a real release to surface. ## Accessibility (ADR-0016) - Skip-link preserved (WCAG 2.4.1 *Bypass Blocks*) and restyled in the brand palette. - Sidebar exposes named landmarks (`<nav aria-label="Sections">`, `<nav aria-label="Accessibility">`) and the collapse button uses `aria-expanded` + a descriptive `aria-label`. - Active links carry `ariaCurrentWhenActive="page"`. - All interactive controls (header action buttons, sidebar links/toggle) meet the 44×44 px minimum hit-target. - Sidebar width transition is skipped under `prefers-reduced-motion: reduce`. - Lucide SVGs are marked `aria-hidden` (decorative); accessible names live on the parent control. ## Perf (ADR-0017) - Production build: **100 kB gzip** initial transfer (budget: 300 kB). Lucide imports are tree-shaken — only the ~18 icons actually used ship. ## What this PR explicitly does NOT do - Wire icon-set migration to icomoon (kept as a deferred swap behind `<app-icon>`). - Filter the menu by permission (deferred to PR 2 once the auth flow lands). - Replace the avatar placeholder with a real user menu (waits on ADR-0009). - Implement the search input behavior (placeholder only; needs a search backend). ## Test plan - [x] `pnpm exec nx run-many -t lint test build --projects=portal-shell` — green (19 / 19 specs). - [x] Production build under bundle budgets (100 kB gzip initial). - [ ] Manual: load `/`, confirm Dashboard appears active in the sidebar, collapse → reload → still collapsed, focus the address bar then Tab → skip-link visible, keyboard-traverse the sidebar. - [ ] Manual: `prefers-reduced-motion: reduce` → no width animation when toggling. - [ ] Manual: zoom to 200 % → no horizontal scroll, header search hides at narrow widths (`md:` breakpoint). --------- Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr> Reviewed-on: #83 |
||
|
|
a463199728 |
feat(infra): reactivate act_runner cache by sharing the runners network (#82)
## Summary Closes the deferred-since-day-one cache-server gap (documented as "Cache server (deferred)" in `infra/README.md` and mentioned every time we hit a slow CI install). **Root cause.** `act_runner`'s built-in cache server binds inside the runner container and advertises an IP on the compose-defined `apf-portal-act-runners` bridge — but jobs are spawned via the mounted `/var/run/docker.sock`, which puts them on Docker's anonymous default `bridge`. The advertised URL is unreachable from the job, every cache request burns a ~2 min `ETIMEDOUT` (restore + save), the hit rate is zero. **Fix.** Tell `act_runner` to attach jobs to the same compose-defined bridge as the runners, via `container.network` in the shared `runner-config.yaml`. The advertised cache URL becomes a normal internal-network DNS hop, jobs reach the cache server, `cache: 'pnpm'` works end-to-end. **Blast-radius trade-off** (bounded). Every container on `apf-portal-act-runners` is one of our runner containers, plus the jobs they spawn — all of which already have full docker-socket access. Sharing a network doesn't widen what a malicious workflow can already do; it just lets jobs reach the cache server. ## What lands - `infra/runner-config.yaml` — add `container.network: apf-portal-act-runners`. Surface the `cache.enabled: true` default explicitly so the toggle is discoverable. - `.gitea/workflows/ci.yml` — re-enable `cache: 'pnpm'` on every `actions/setup-node` step (5 jobs). Drop the now-stale block comment that explained the disablement. - `.gitea/workflows/security-scheduled.yml` — same on the two setup-node steps. - `infra/README.md` "Cache server" section rewritten — was `"(deferred)"`, now describes the working setup, rationale, and the disable toggle. - `ci.yml`'s Trivy comment trimmed to drop the cross-reference to the deferred-cache-server section that no longer exists. ## Roll-out (manual, post-merge, on the runner host) ```bash cd <repo>/infra git pull ./ci-runners.sh rotate ``` `rotate` recreates the containers with the new `runner-config.yaml` mount intact (rolling restart, ~15 s pause between each runner so the CI pipeline stays online). ## Test plan - [ ] CI green on this PR (the gates run on the runners as configured **before** rollout, so this PR's run is one last "uncached" cycle). - [ ] After rollout, the next CI run's `Set up Node.js` step shows the cache restore attempt **succeed quickly** (no ETIMEDOUT). The `Run pnpm install --frozen-lockfile` step on the first post-rollout run still reports `Progress: resolved N, reused 0, downloaded N` (cold seed). - [ ] The **second** post-rollout run reports `reused N, downloaded 0` (or a small downloaded delta if Renovate moved a dep meanwhile) — the cache hit is real. - [ ] `Complete job` step at the end no longer shows `reserveCache failed: connect ETIMEDOUT` warnings. - [ ] Wall-clock for a typical PR's CI drops by ~5-10 min (5 jobs × ~30-90 s saved on `pnpm install` + the 2× ~2 min ETIMEDOUTs we used to eat). --------- Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr> Reviewed-on: #82 |
||
|
|
2d676cc279 |
feat(infra): add ci-runners.sh wrapper for the runner stack (#81)
## Summary Mirrors the convenience-script pattern we adopted for `infra/local/dev.sh`: typing `docker compose -f infra/ci-runners.compose.yml ...` for routine ops gets old fast, the pre-pull of the catthehacker job images is documented but easy to forget, and the "rotation of one runner at a time" tip in `infra/README.md` is a sequence the contributor was supposed to hand-roll every time. `infra/ci-runners.sh` exposes the everyday verbs and automates the rolling-restart pattern. ## What lands | Command | Effect | | --------------------------------------------- | --------------------------------------------------------------------------------- | | `./ci-runners.sh up` | Bring the three runner containers up | | `./ci-runners.sh up --prepull` | Pre-pull the job images (`act-22.04` + `:full-22.04`) on the host first | | `./ci-runners.sh down` | Stop and remove the containers (**preserves** `data/runner-N/.runner` credentials) | | `./ci-runners.sh restart <runner>` | Restart one runner | | `./ci-runners.sh rotate` | Rolling restart of every runner with a 15 s pause between each — keeps at least N-1 runners online through a config refresh | | `./ci-runners.sh status` | `docker compose ps` for the runner services | | `./ci-runners.sh logs [runner]` | Follow logs (one runner or all of them) | | `./ci-runners.sh pull-images` | Pre-pull / refresh the job images (idempotent) | | `./ci-runners.sh <other>` | Pass-through to `docker compose -f ci-runners.compose.yml ...` | The destructive `down -v` (wipes `data/`, forces re-registration with a fresh Gitea token) is intentionally **not** exposed as a verb — invoke `docker compose -f ci-runners.compose.yml down -v` directly so the path is explicit at the typing level. ## Doc updates (`infra/README.md`) - Inventory table at the top picks up the script. - "First-time registration" walkthrough swaps the explicit `docker pull` / `docker compose up` steps for `./ci-runners.sh up --prepull`. - New "Convenience script — `ci-runners.sh`" subsection with the cheat-sheet table. - "Operational tips" rephrased to point at the script's `rotate` / `restart` / `logs` verbs as the canonical commands; the raw-docker-compose form is kept in parentheses as the underlying mechanism. - "Adding a fourth runner" tip now reminds to update the `RUNNERS=()` array at the top of the script. ## Trade-off The 15 s pause in `rotate` is a conservative approximation — `act_runner` doesn't expose a Compose healthcheck, so we can't poll for ready. Adjust the constant at the top of the script if reality argues for a different value. ## Test plan - [ ] CI green on this PR. - [ ] On the runner host: `./infra/ci-runners.sh status` shows the three runners running. - [ ] `./infra/ci-runners.sh logs runner-1` tails runner-1's stdout. - [ ] `./infra/ci-runners.sh rotate` cycles through runner-1 → runner-2 → runner-3 with the 15 s pauses; `status` between rotations shows N-1 runners online at any moment (with a brief gap for the one currently restarting). - [ ] `./infra/ci-runners.sh restart runner-99` errors out with the "unknown runner" message. --------- Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr> Reviewed-on: #81 |
||
|
|
674c8b2f88 |
chore(deps): update dependency gitleaks/gitleaks to v8.30.1 (#79)
This PR contains the following updates: | Package | Update | Change | |---|---|---| | [gitleaks/gitleaks](https://github.com/gitleaks/gitleaks) | minor | `8.21.0` -> `8.30.1` | --- ### Release Notes <details> <summary>gitleaks/gitleaks (gitleaks/gitleaks)</summary> ### [`v8.30.1`](https://github.com/gitleaks/gitleaks/releases/tag/v8.30.1) [Compare Source](https://github.com/gitleaks/gitleaks/compare/v8.30.0...v8.30.1) ##### Changelog - [`83d9cd6`](https://github.com/gitleaks/gitleaks/commit/83d9cd684c87d95d656c1458ef04895a7f1cbd8e) update goreleaser - [`8d1f98c`](https://github.com/gitleaks/gitleaks/commit/8d1f98c7967eb1e79cb44ac6241a124e145d2165) Removed unnecessary functions from report template ([#​2040](https://github.com/gitleaks/gitleaks/issues/2040)) - [`ca20267`](https://github.com/gitleaks/gitleaks/commit/ca20267a84aa1fa2c2a9c1a13cdb50cafb48eeb0) its the simple things ([#​2020](https://github.com/gitleaks/gitleaks/issues/2020)) - [`b66ac75`](https://github.com/gitleaks/gitleaks/commit/b66ac75e4fa93d86d78fccd6e2f36d2c0698b2a2) build: switch to Go 1.24 ([#​2002](https://github.com/gitleaks/gitleaks/issues/2002)) ### [`v8.30.0`](https://github.com/gitleaks/gitleaks/releases/tag/v8.30.0) [Compare Source](https://github.com/gitleaks/gitleaks/compare/v8.29.1...v8.30.0) ##### Changelog - [`6eaad03`](https://github.com/gitleaks/gitleaks/commit/6eaad03) 0 to 5 - notes on recursive decoding ([#​1994](https://github.com/gitleaks/gitleaks/issues/1994)) - [`09242ce`](https://github.com/gitleaks/gitleaks/commit/09242ce) Add new Looker client ID and client secret rules ([#​1947](https://github.com/gitleaks/gitleaks/issues/1947)) - [`c98e5e0`](https://github.com/gitleaks/gitleaks/commit/c98e5e0) feat: add Airtable Personnal Access Token detection ([#​1952](https://github.com/gitleaks/gitleaks/issues/1952)) - [`4ed0ca4`](https://github.com/gitleaks/gitleaks/commit/4ed0ca4) build: upgrade Go & alpine version ([#​1989](https://github.com/gitleaks/gitleaks/issues/1989)) ### [`v8.29.1`](https://github.com/gitleaks/gitleaks/releases/tag/v8.29.1) [Compare Source](https://github.com/gitleaks/gitleaks/compare/v8.29.0...v8.29.1) ##### Changelog - [`fb5d707`](https://github.com/gitleaks/gitleaks/commit/fb5d707) thats a paddlin - [`50493db`](https://github.com/gitleaks/gitleaks/commit/50493db) feat: document stdout report path ([#​1990](https://github.com/gitleaks/gitleaks/issues/1990)) ### [`v8.29.0`](https://github.com/gitleaks/gitleaks/releases/tag/v8.29.0) [Compare Source](https://github.com/gitleaks/gitleaks/compare/v8.28.0...v8.29.0) ##### Changelog - [`ed65b65`](https://github.com/gitleaks/gitleaks/commit/ed65b65) Add trace log for skipped archive file when not enabled ([#​1961](https://github.com/gitleaks/gitleaks/issues/1961)) - [`c5ccbb9`](https://github.com/gitleaks/gitleaks/commit/c5ccbb9) Respect contexts with timeouts ([#​1948](https://github.com/gitleaks/gitleaks/issues/1948)) - [`3821f30`](https://github.com/gitleaks/gitleaks/commit/3821f30) Config min version ([#​1955](https://github.com/gitleaks/gitleaks/issues/1955)) - [`d223718`](https://github.com/gitleaks/gitleaks/commit/d223718) fix(config): validate rules when \[extend] is used ([#​1592](https://github.com/gitleaks/gitleaks/issues/1592)) - [`87d9629`](https://github.com/gitleaks/gitleaks/commit/87d9629) feat: add Amazon Bedrock API key detection ([#​1935](https://github.com/gitleaks/gitleaks/issues/1935)) - [`228396b`](https://github.com/gitleaks/gitleaks/commit/228396b) Add GitHub Sponsors section and Discord link - [`a82bc53`](https://github.com/gitleaks/gitleaks/commit/a82bc53) feat: improve regex to detect Sonar tokens with prefixes ([#​1931](https://github.com/gitleaks/gitleaks/issues/1931)) ### [`v8.28.0`](https://github.com/gitleaks/gitleaks/releases/tag/v8.28.0) [Compare Source](https://github.com/gitleaks/gitleaks/compare/v8.27.2...v8.28.0) ##### Changelog - [`4fb4382`](https://github.com/gitleaks/gitleaks/commit/4fb4382) cant count - [`b1c9c7e`](https://github.com/gitleaks/gitleaks/commit/b1c9c7e) Composite rules ([#​1905](https://github.com/gitleaks/gitleaks/issues/1905)) - [`72977e4`](https://github.com/gitleaks/gitleaks/commit/72977e4) feat: add Anthropic API key detection ([#​1910](https://github.com/gitleaks/gitleaks/issues/1910)) - [`7b02c98`](https://github.com/gitleaks/gitleaks/commit/7b02c98) fix(git): handle port ([#​1912](https://github.com/gitleaks/gitleaks/issues/1912)) - [`2a7bcff`](https://github.com/gitleaks/gitleaks/commit/2a7bcff) dont prematurely calculate fragment newlines ([#​1909](https://github.com/gitleaks/gitleaks/issues/1909)) - [`bd79c3e`](https://github.com/gitleaks/gitleaks/commit/bd79c3e) feat(allowlist): promote optimizations ([#​1908](https://github.com/gitleaks/gitleaks/issues/1908)) - [`7fb4eda`](https://github.com/gitleaks/gitleaks/commit/7fb4eda) Fix: CVEs on go and go crypto ([#​1868](https://github.com/gitleaks/gitleaks/issues/1868)) - [`a044b81`](https://github.com/gitleaks/gitleaks/commit/a044b81) feat: add artifactory reference token and api key detection ([#​1906](https://github.com/gitleaks/gitleaks/issues/1906)) - [`bf380d4`](https://github.com/gitleaks/gitleaks/commit/bf380d4) silly - [`f487f85`](https://github.com/gitleaks/gitleaks/commit/f487f85) Update gitleaks.yml - [`958f55a`](https://github.com/gitleaks/gitleaks/commit/958f55a) add just like that, no leaks ##### Optimizations [#​1909](https://github.com/gitleaks/gitleaks/issues/1909) waits to find newlines until a match. This ends up saving a boat load of time since before we were finding newlines for every fragment regardless if a rule matched or not. [#​1908](https://github.com/gitleaks/gitleaks/issues/1908) promoted [@​rgmz](https://github.com/rgmz) excellent stopword optimization ##### Composite Rules (Multi-part or `required` Rules) [#​1905](https://github.com/gitleaks/gitleaks/issues/1905) In v8.28.0 Gitleaks introduced composite rules, which are made up of a single "primary" rule and one or more auxiliary or `required` rules. To create a composite rule, add a `[[rules.required]]` table to the primary rule specifying an `id` and optionally `withinLines` and/or `withinColumns` proximity constraints. A fragment is a chunk of content that Gitleaks processes at once (typically a file, part of a file, or git diff), and proximity matching instructs the primary rule to only report a finding if the auxiliary `required` rules also find matches within the specified area of the fragment. **Proximity matching:** Using the `withinLines` and `withinColumns` fields instructs the primary rule to only report a finding if the auxiliary `required` rules also find matches within the specified proximity. You can set: - **`withinLines: N`** - required findings must be within N lines (vertically) - **`withinColumns: N`** - required findings must be within N characters (horizontally) - **Both** - creates a rectangular search area (both constraints must be satisfied) - **Neither** - fragment-level matching (required findings can be anywhere in the same fragment) Here are diagrams illustrating each proximity behavior: ``` p = primary captured secret a = auxiliary (required) captured secret fragment = section of data gitleaks is looking at *Fragment-level proximity* Any required finding in the fragment ┌────────┐ ┌──────┤fragment├─────┐ │ └──────┬─┤ │ ┌───────┐ │ │a│◀────┼─│✓ MATCH│ │ ┌─┐└─┘ │ └───────┘ │┌─┐ │p│ │ ││a│ ┌─┐└─┘ │ ┌───────┐ │└─┘ │a│◀──────────┼─│✓ MATCH│ └─▲─────┴─┴───────────┘ └───────┘ │ ┌───────┐ └────│✓ MATCH│ └───────┘ *Column bounded proximity* `withinColumns = 3` ┌────────┐ ┌────┬─┤fragment├─┬───┐ │ └──────┬─┤ │ ┌───────────┐ │ │ │a│◀┼───┼─│+1C ✓ MATCH│ │ ┌─┐└─┘ │ └───────────┘ │┌─┐ │ │p│ │ │ ┌──▶│a│ ┌─┐ └─┘ │ ┌───────────┐ │ │└─┘ ││a│◀────────┼───┼─│-2C ✓ MATCH│ │ │ ┘ │ └───────────┘ │ └── -3C ───0C─── +3C ─┘ │ ┌─────────┐ │ │ -4C ✗ NO│ └──│ MATCH │ └─────────┘ *Line bounded proximity* `withinLines = 4` ┌────────┐ ┌─────┤fragment├─────┐ +4L─ ─ ┴────────┘─ ─ ─│ │ │ │ ┌─┐ │ ┌────────────┐ │ ┌─┐ │a│◀──┼─│+1L ✓ MATCH │ 0L ┌─┐ │p│ └─┘ │ ├────────────┤ │ │a│◀──┴─┴────────┼─│-1L ✓ MATCH │ │ └─┘ │ └────────────┘ │ │ ┌─────────┐ -4L─ ─ ─ ─ ─ ─ ─ ─┌─┐─│ │-5L ✗ NO │ │ │a│◀┼─│ MATCH │ └────────────────┴─┴─┘ └─────────┘ *Line and column bounded proximity* `withinLines = 4` `withinColumns = 3` ┌────────┐ ┌─────┤fragment├─────┐ +4L ┌└────────┴ ┐ │ │ ┌─┐ │ ┌───────────────┐ │ │ │a│◀┼───┼─│+2L/+1C ✓ MATCH│ │ ┌─┐└─┘ │ └───────────────┘ 0L │ │p│ │ │ │ └─┘ │ │ │ │ │ ┌────────────┐ -4L ─ ─ ─ ─ ─ ─┌─┐ │ │-5L/+3C ✗ NO│ │ │a│◀┼─│ MATCH │ └───-3C────0L───+3C┴─┘ └────────────┘ ``` ### [`v8.27.2`](https://github.com/gitleaks/gitleaks/releases/tag/v8.27.2) [Compare Source](https://github.com/gitleaks/gitleaks/compare/v8.27.1...v8.27.2) ##### Changelog - [`c7acf33`](https://github.com/gitleaks/gitleaks/commit/c7acf33) Merge branch 'master' of github.com:gitleaks/gitleaks - [`9faaa4a`](https://github.com/gitleaks/gitleaks/commit/9faaa4a) Add experimental allowlist optimizations ([#​1731](https://github.com/gitleaks/gitleaks/issues/1731)) - [`79068b3`](https://github.com/gitleaks/gitleaks/commit/79068b3) Detect Notion Public API Keys [#​1889](https://github.com/gitleaks/gitleaks/issues/1889) ([#​1890](https://github.com/gitleaks/gitleaks/issues/1890)) ### [`v8.27.1`](https://github.com/gitleaks/gitleaks/releases/tag/v8.27.1) [Compare Source](https://github.com/gitleaks/gitleaks/compare/v8.27.0...v8.27.1) ##### Changelog - [`80468ef`](https://github.com/gitleaks/gitleaks/commit/80468ef) Merge branch 'master' of github.com:gitleaks/gitleaks - [`ef82237`](https://github.com/gitleaks/gitleaks/commit/ef82237) fix(atlassian): reduce false-positives for v1 pattern ([#​1892](https://github.com/gitleaks/gitleaks/issues/1892)) - [`2463f11`](https://github.com/gitleaks/gitleaks/commit/2463f11) Fix log suppresion issue ([#​1887](https://github.com/gitleaks/gitleaks/issues/1887)) - [`6f251ee`](https://github.com/gitleaks/gitleaks/commit/6f251ee) Added Heroku API Key New Version ([#​1883](https://github.com/gitleaks/gitleaks/issues/1883)) - [`20f9a1d`](https://github.com/gitleaks/gitleaks/commit/20f9a1d) Add Platform Bitbucket ([#​1886](https://github.com/gitleaks/gitleaks/issues/1886)) - [`722ce82`](https://github.com/gitleaks/gitleaks/commit/722ce82) Add Platform Gitea ([#​1884](https://github.com/gitleaks/gitleaks/issues/1884)) - [`79780b8`](https://github.com/gitleaks/gitleaks/commit/79780b8) Merge branch 'master' of github.com:gitleaks/gitleaks - [`c5683ca`](https://github.com/gitleaks/gitleaks/commit/c5683ca) prevent default warn message when max-archive-depth not set ([#​1881](https://github.com/gitleaks/gitleaks/issues/1881)) - [`0357c3c`](https://github.com/gitleaks/gitleaks/commit/0357c3c) prevent default warn message when max-archive-depth not set ### [`v8.27.0`](https://github.com/gitleaks/gitleaks/releases/tag/v8.27.0) [Compare Source](https://github.com/gitleaks/gitleaks/compare/v8.26.0...v8.27.0) ##### Changelog - [`782f310`](https://github.com/gitleaks/gitleaks/commit/782f310) Archive support ([#​1872](https://github.com/gitleaks/gitleaks/issues/1872)) - [`489d13c`](https://github.com/gitleaks/gitleaks/commit/489d13c) Update README.md - [`d29ee55`](https://github.com/gitleaks/gitleaks/commit/d29ee55) Reduce aws-access-token false positives ([#​1876](https://github.com/gitleaks/gitleaks/issues/1876)) - [`611db65`](https://github.com/gitleaks/gitleaks/commit/611db65) Set `pass_filenames` to `false` for Docker hook ([#​1850](https://github.com/gitleaks/gitleaks/issues/1850)) - [`0589ae0`](https://github.com/gitleaks/gitleaks/commit/0589ae0) unicode decoding ([#​1854](https://github.com/gitleaks/gitleaks/issues/1854)) - [`82f7e32`](https://github.com/gitleaks/gitleaks/commit/82f7e32) Diagnostics ([#​1856](https://github.com/gitleaks/gitleaks/issues/1856)) - [`f97a9ee`](https://github.com/gitleaks/gitleaks/commit/f97a9ee) chore: include decoder in debug log ([#​1853](https://github.com/gitleaks/gitleaks/issues/1853)) Got another [@​bplaxco](https://github.com/bplaxco) release. Cheers! ##### Archive Scanning Sometimes secrets are packaged within archive files like zip files or tarballs, making them difficult to discover. Now you can tell gitleaks to automatically extract and scan the contents of archives. The flag `--max-archive-depth` enables this feature for both `dir` and `git` scan types. The default value of "0" means this feature is disabled by default. Recursive scanning is supported since archives can also contain other archives. The `--max-archive-depth` flag sets the recursion limit. Recursion stops when there are no new archives to extract, so setting a very high max depth just sets the potential to go that deep. It will only go as deep as it needs to. The findings for secrets located within an archive will include the path to the file inside the archive. Inner paths are separated with `!`. Example finding (shortened for brevity): ``` Finding: DB_PASSWORD=8ae31cacf141669ddfb5da ... File: testdata/archives/nested.tar.gz!archives/files.tar!files/.env.prod Line: 4 Commit: 6e6ee6596d337bb656496425fb98644eb62b4a82 ... Fingerprint: 6e6ee6596d337bb656496425fb98644eb62b4a82:testdata/archives/nested.tar.gz!archives/files.tar!files/.env.prod:generic-api-key:4 Link: https://github.com/leaktk/gitleaks/blob/6e6ee6596d337bb656496425fb98644eb62b4a82/testdata/archives/nested.tar.gz ``` This means a secret was detected on line 4 of `files/.env.prod.` which is in `archives/files.tar` which is in `testdata/archives/nested.tar.gz`. Currently supported formats: The [compression](https://github.com/mholt/archives?tab=readme-ov-file#supported-compression-formats) and [archive](https://github.com/mholt/archives?tab=readme-ov-file#supported-archive-formats) formats supported by mholt's [archives package](https://github.com/mholt/archives) are supported. ### [`v8.26.0`](https://github.com/gitleaks/gitleaks/releases/tag/v8.26.0) [Compare Source](https://github.com/gitleaks/gitleaks/compare/v8.25.1...v8.26.0) ##### Changelog - [`78eebac`](https://github.com/gitleaks/gitleaks/commit/78eebac) Percent/URL Decoding Support ([#​1831](https://github.com/gitleaks/gitleaks/issues/1831)) - [`6f967ca`](https://github.com/gitleaks/gitleaks/commit/6f967ca) fix(kubernetes): remove slow element from pat ([#​1848](https://github.com/gitleaks/gitleaks/issues/1848)) - [`88f56d3`](https://github.com/gitleaks/gitleaks/commit/88f56d3) feat: identify slow file ([#​1479](https://github.com/gitleaks/gitleaks/issues/1479)) - [`9609928`](https://github.com/gitleaks/gitleaks/commit/9609928) rm 1password detect test since we test it in cfg gen - [`23cb69f`](https://github.com/gitleaks/gitleaks/commit/23cb69f) feat(rules): Add 1Password secret key detection ([#​1834](https://github.com/gitleaks/gitleaks/issues/1834)) Calling this one [@​bplaxco](https://github.com/bplaxco)'s release as he introduced a really clever method for mixed decoding without sacrificing too much performance. As I stated in his PR, I think he's either a wizard or some time traveling AI. Dude [is wicked smaht](https://www.youtube.com/watch?v=hIdsjNGCGz4) Anyways, Gitleaks now supports the following decoders: `hex`, `percent(url enconding)`, and `b64`. It's relatively straight forward to add a new decoder so if you're motivated, community contributions are welcomed! Here's an example: ``` ~/code/gitleaks-org/gitleaks (master) cat decode.txt text below aGVsbG8sIHdvcmxkIQ%3D%3D%0A text above ~/code/gitleaks-org/gitleaks (master) ./gitleaks dir decode.txt --max-decode-depth=2 --log-level=debug ○ │╲ │ ○ ○ ░ ░ gitleaks 4:08PM DBG using stdlib regex engine 4:08PM DBG unable to load gitleaks config from decode.txt/.gitleaks.toml since --source=decode.txt is a file, using default config 4:08PM DBG found .gitleaksignore file: .gitleaksignore 4:08PM DBG segment found: original=[29,38] pos=[29,38]: "%3D%3D%0A" -> "==\n" 4:08PM DBG segment found: original=[11,38] pos=[11,31]: "aGVsbG8sIHdvcmxkIQ==" -> "hello, world!" 4:08PM INF scanned ~50 bytes (50 bytes) in 1.5ms 4:08PM INF no leaks found ``` ### [`v8.25.1`](https://github.com/gitleaks/gitleaks/releases/tag/v8.25.1) [Compare Source](https://github.com/gitleaks/gitleaks/compare/v8.25.0...v8.25.1) ##### Changelog - [`d1c7759`](https://github.com/gitleaks/gitleaks/commit/d1c7759) fix(detect): test all allowlists ([#​1845](https://github.com/gitleaks/gitleaks/issues/1845)) Big thanks [@​rgmz](https://github.com/rgmz) ### [`v8.25.0`](https://github.com/gitleaks/gitleaks/releases/tag/v8.25.0) [Compare Source](https://github.com/gitleaks/gitleaks/compare/v8.24.3...v8.25.0) ##### Changelog - [`4451b45`](https://github.com/gitleaks/gitleaks/commit/4451b45) feat(config): define multiple global allowlists ([#​1777](https://github.com/gitleaks/gitleaks/issues/1777)) (cause for the minor bump change) - [`7fb21a4`](https://github.com/gitleaks/gitleaks/commit/7fb21a4) feat(rules): Add Perplexity AI API key detection ([#​1825](https://github.com/gitleaks/gitleaks/issues/1825)) - [`f6193bc`](https://github.com/gitleaks/gitleaks/commit/f6193bc) feat(gcp): increase rule entropy ([#​1840](https://github.com/gitleaks/gitleaks/issues/1840)) - [`9bc7257`](https://github.com/gitleaks/gitleaks/commit/9bc7257) Adding clickhouse scanner ([#​1826](https://github.com/gitleaks/gitleaks/issues/1826)) - [`b6cc71a`](https://github.com/gitleaks/gitleaks/commit/b6cc71a) fix(baseline): work with --redact ([#​1741](https://github.com/gitleaks/gitleaks/issues/1741)) - [`cfdeb0d`](https://github.com/gitleaks/gitleaks/commit/cfdeb0d) feat(rule): validate & sort rule when generating ([#​1817](https://github.com/gitleaks/gitleaks/issues/1817)) ### [`v8.24.3`](https://github.com/gitleaks/gitleaks/releases/tag/v8.24.3) [Compare Source](https://github.com/gitleaks/gitleaks/compare/v8.24.2...v8.24.3) ##### Changelog - [`107a418`](https://github.com/gitleaks/gitleaks/commit/107a418) Add support for GitLab Runner Tokens (Routable) ([#​1820](https://github.com/gitleaks/gitleaks/issues/1820)) - [`7fac002`](https://github.com/gitleaks/gitleaks/commit/7fac002) bump repo version in pre-commit example ([#​1815](https://github.com/gitleaks/gitleaks/issues/1815)) - [`4b54104`](https://github.com/gitleaks/gitleaks/commit/4b54104) Fix currentLine out of bounds error ([#​1810](https://github.com/gitleaks/gitleaks/issues/1810)) - [`af7d5bc`](https://github.com/gitleaks/gitleaks/commit/af7d5bc) add support for Azure DevOps platform in SCM detection and link ([#​1807](https://github.com/gitleaks/gitleaks/issues/1807)) - [`3e8cd2d`](https://github.com/gitleaks/gitleaks/commit/3e8cd2d) Add MaxMind license key rule ([#​1771](https://github.com/gitleaks/gitleaks/issues/1771)) - [`ddcc753`](https://github.com/gitleaks/gitleaks/commit/ddcc753) implement new openai regex pattern ([#​1780](https://github.com/gitleaks/gitleaks/issues/1780)) - [`9708e65`](https://github.com/gitleaks/gitleaks/commit/9708e65) A first attempt adding hooks.slack.com/triggers/ ([#​1792](https://github.com/gitleaks/gitleaks/issues/1792)) - [`198e410`](https://github.com/gitleaks/gitleaks/commit/198e410) feat(generic): tweak false-positives ([#​1803](https://github.com/gitleaks/gitleaks/issues/1803)) - [`e273a97`](https://github.com/gitleaks/gitleaks/commit/e273a97) chore: tweak logging and readme for GITLEAKS\_CONFIG\_TOML feature ([#​1802](https://github.com/gitleaks/gitleaks/issues/1802)) - [`a503b58`](https://github.com/gitleaks/gitleaks/commit/a503b58) feat: add option to set config from env var with toml content ([#​1662](https://github.com/gitleaks/gitleaks/issues/1662)) ### [`v8.24.2`](https://github.com/gitleaks/gitleaks/releases/tag/v8.24.2) [Compare Source](https://github.com/gitleaks/gitleaks/compare/v8.24.0...v8.24.2) ##### What's Changed - Fix `platform` flag being ignored with `gitleaks detect` by [@​rgmz](https://github.com/rgmz) in https://github.com/gitleaks/gitleaks/pull/1765 - Make AddFinding public by [@​bplaxco](https://github.com/bplaxco) in https://github.com/gitleaks/gitleaks/pull/1767 - FIX upgrade x/crypto to 0.31.0 to get rid of CVE-2024-45337 by [@​cgoessen](https://github.com/cgoessen) in https://github.com/gitleaks/gitleaks/pull/1768 - Upgrade rs/zerolog, spf13/cobra, and spf13/viper by [@​rgmz](https://github.com/rgmz) in https://github.com/gitleaks/gitleaks/pull/1769 - Infer `report-format` from `report-path` extension if no value is provided by [@​rgmz](https://github.com/rgmz) in https://github.com/gitleaks/gitleaks/pull/1776 - `generic-api-key`: ignore csrf-tokens by [@​rgmz](https://github.com/rgmz) in https://github.com/gitleaks/gitleaks/pull/1779 - Prevent Yocto/BitBake false positives with generic-api-key rule by [@​Okeanos](https://github.com/Okeanos) in https://github.com/gitleaks/gitleaks/pull/1783 - Fix decoded line allowlist by [@​zricethezav](https://github.com/zricethezav) in https://github.com/gitleaks/gitleaks/pull/1788 - Readme badge revisions by [@​jessp01](https://github.com/jessp01) in https://github.com/gitleaks/gitleaks/pull/1744 - feat(regexp): use standard regexp by default, make go-re2 opt-in by [@​twpayne](https://github.com/twpayne) in https://github.com/gitleaks/gitleaks/pull/1798 - gore2 release tags by [@​zricethezav](https://github.com/zricethezav) in https://github.com/gitleaks/gitleaks/pull/1801 ##### New Contributors - [@​cgoessen](https://github.com/cgoessen) made their first contribution in https://github.com/gitleaks/gitleaks/pull/1768 - [@​Okeanos](https://github.com/Okeanos) made their first contribution in https://github.com/gitleaks/gitleaks/pull/1783 - [@​jessp01](https://github.com/jessp01) made their first contribution in https://github.com/gitleaks/gitleaks/pull/1744 - [@​twpayne](https://github.com/twpayne) made their first contribution in https://github.com/gitleaks/gitleaks/pull/1798 **Full Changelog**: https://github.com/gitleaks/gitleaks/compare/v8.24.0...v8.24.2 ### [`v8.24.0`](https://github.com/gitleaks/gitleaks/releases/tag/v8.24.0) [Compare Source](https://github.com/gitleaks/gitleaks/compare/v8.23.3...v8.24.0) ##### Changelog - [`c2afd56`](https://github.com/gitleaks/gitleaks/commit/c2afd56) Make paths and fingerprints platform-agnostic ([#​1622](https://github.com/gitleaks/gitleaks/issues/1622)) - [`818e32f`](https://github.com/gitleaks/gitleaks/commit/818e32f) Add Sonar rule ([#​1756](https://github.com/gitleaks/gitleaks/issues/1756)) - [`3fa5a3a`](https://github.com/gitleaks/gitleaks/commit/3fa5a3a) Minor false positive improvements ([#​1758](https://github.com/gitleaks/gitleaks/issues/1758)) - [`2020e6a`](https://github.com/gitleaks/gitleaks/commit/2020e6a) Add support for streaming DetectReader ([#​1760](https://github.com/gitleaks/gitleaks/issues/1760)) - [`9122a2d`](https://github.com/gitleaks/gitleaks/commit/9122a2d) chore: Update github.com/wasilibs/go-re2 to v1.9.0 ([#​1763](https://github.com/gitleaks/gitleaks/issues/1763)) - [`398d0c4`](https://github.com/gitleaks/gitleaks/commit/398d0c4) docs: describe extended rules take precedence over base rules ([#​1563](https://github.com/gitleaks/gitleaks/issues/1563)) - [`ae26eff`](https://github.com/gitleaks/gitleaks/commit/ae26eff) feat(git): disable link generation ([#​1748](https://github.com/gitleaks/gitleaks/issues/1748)) - [`c6424a6`](https://github.com/gitleaks/gitleaks/commit/c6424a6) added sourcegraph token rule ([#​1736](https://github.com/gitleaks/gitleaks/issues/1736)) - [`6411402`](https://github.com/gitleaks/gitleaks/commit/6411402) feat(config): add rule for .p12 files ([#​1738](https://github.com/gitleaks/gitleaks/issues/1738)) - [`d71d95d`](https://github.com/gitleaks/gitleaks/commit/d71d95d) add deno.lock to default exclusions ([#​1740](https://github.com/gitleaks/gitleaks/issues/1740)) ### [`v8.23.3`](https://github.com/gitleaks/gitleaks/releases/tag/v8.23.3) [Compare Source](https://github.com/gitleaks/gitleaks/compare/v8.23.2...v8.23.3) ##### Changelog - [`3188ad6`](https://github.com/gitleaks/gitleaks/commit/3188ad6) Don't exit with error if git repacking is required ([#​1711](https://github.com/gitleaks/gitleaks/issues/1711)) - [`7fc11bb`](https://github.com/gitleaks/gitleaks/commit/7fc11bb) refactor(config): use non-capture groups for allowlists ([#​1735](https://github.com/gitleaks/gitleaks/issues/1735)) - [`36c52c6`](https://github.com/gitleaks/gitleaks/commit/36c52c6) chore: Enhance `curl-auth-user` to detect empty usernames or passwords ([#​1726](https://github.com/gitleaks/gitleaks/issues/1726)) - [`1f323d8`](https://github.com/gitleaks/gitleaks/commit/1f323d8) fix(cmd): read log-opts before GitLogCmd ([#​1730](https://github.com/gitleaks/gitleaks/issues/1730)) ### [`v8.23.2`](https://github.com/gitleaks/gitleaks/releases/tag/v8.23.2) [Compare Source](https://github.com/gitleaks/gitleaks/compare/v8.23.1...v8.23.2) ##### Changelog - [`d88bc09`](https://github.com/gitleaks/gitleaks/commit/d88bc09) facebook keyword - [`3fdaefd`](https://github.com/gitleaks/gitleaks/commit/3fdaefd) fix(meraki): restrict keyword case ([#​1722](https://github.com/gitleaks/gitleaks/issues/1722)) - [`f3ae52e`](https://github.com/gitleaks/gitleaks/commit/f3ae52e) feat(generic-api-key): detect base64 ([#​1598](https://github.com/gitleaks/gitleaks/issues/1598)) - [`d6a828a`](https://github.com/gitleaks/gitleaks/commit/d6a828a) great branch name ([#​1721](https://github.com/gitleaks/gitleaks/issues/1721)) - [`d2ffffe`](https://github.com/gitleaks/gitleaks/commit/d2ffffe) fix(git): remove .git suffix for links ([#​1716](https://github.com/gitleaks/gitleaks/issues/1716)) - [`a43dc0d`](https://github.com/gitleaks/gitleaks/commit/a43dc0d) chore: refine generic-api-key fps + trace logging ([#​1720](https://github.com/gitleaks/gitleaks/issues/1720)) - [`69ed20e`](https://github.com/gitleaks/gitleaks/commit/69ed20e) fix(generate): move newline out of char range ([#​1719](https://github.com/gitleaks/gitleaks/issues/1719)) - [`52b895a`](https://github.com/gitleaks/gitleaks/commit/52b895a) newline literal ([#​1718](https://github.com/gitleaks/gitleaks/issues/1718)) - [`3f4d91f`](https://github.com/gitleaks/gitleaks/commit/3f4d91f) build: support either stdlib or 3rd-party regexp ([#​1706](https://github.com/gitleaks/gitleaks/issues/1706)) - [`049f5b2`](https://github.com/gitleaks/gitleaks/commit/049f5b2) chore(detect): update trace logging ([#​1713](https://github.com/gitleaks/gitleaks/issues/1713)) - [`7a6183d`](https://github.com/gitleaks/gitleaks/commit/7a6183d) feat(git): redact passwords from remote URL ([#​1709](https://github.com/gitleaks/gitleaks/issues/1709)) - [`3c7f3f0`](https://github.com/gitleaks/gitleaks/commit/3c7f3f0) feat(git): include link in report ([#​1698](https://github.com/gitleaks/gitleaks/issues/1698)) - [`0e3f4f7`](https://github.com/gitleaks/gitleaks/commit/0e3f4f7) chore: reduce generic-api-key fps ([#​1707](https://github.com/gitleaks/gitleaks/issues/1707)) - [`3ed8567`](https://github.com/gitleaks/gitleaks/commit/3ed8567) blorp - [`e977850`](https://github.com/gitleaks/gitleaks/commit/e977850) added new rule for cisco meraki api key ([#​1700](https://github.com/gitleaks/gitleaks/issues/1700)) - [`ad7a4fb`](https://github.com/gitleaks/gitleaks/commit/ad7a4fb) feat: general fp tweaks ([#​1703](https://github.com/gitleaks/gitleaks/issues/1703)) - [`b2cf03c`](https://github.com/gitleaks/gitleaks/commit/b2cf03c) chore(generate): use \x60 instead of literal ([#​1702](https://github.com/gitleaks/gitleaks/issues/1702)) - [`a3f623c`](https://github.com/gitleaks/gitleaks/commit/a3f623c) chore(regex): simplify secretPrefix, suffix ([#​1620](https://github.com/gitleaks/gitleaks/issues/1620)) - [`cc71bb1`](https://github.com/gitleaks/gitleaks/commit/cc71bb1) update version for pre-commit in README.md ([#​1699](https://github.com/gitleaks/gitleaks/issues/1699)) ### [`v8.23.1`](https://github.com/gitleaks/gitleaks/releases/tag/v8.23.1) [Compare Source](https://github.com/gitleaks/gitleaks/compare/v8.23.0...v8.23.1) ##### Changelog - [`7bad9f7`](https://github.com/gitleaks/gitleaks/commit/7bad9f7) chore(gcp): add firebase example keys to the gcp-api-key allowlists ([#​1635](https://github.com/gitleaks/gitleaks/issues/1635)) - [`977236c`](https://github.com/gitleaks/gitleaks/commit/977236c) fix: unaligned 64-bit atomic operation panic ([#​1696](https://github.com/gitleaks/gitleaks/issues/1696)) - [`a211b16`](https://github.com/gitleaks/gitleaks/commit/a211b16) force push to master everyday - [`0e5f644`](https://github.com/gitleaks/gitleaks/commit/0e5f644) feat(config): disable extended rule ([#​1535](https://github.com/gitleaks/gitleaks/issues/1535)) - [`f320a60`](https://github.com/gitleaks/gitleaks/commit/f320a60) style: prevent globbing and word splitting ([#​1543](https://github.com/gitleaks/gitleaks/issues/1543)) - [`c4526b2`](https://github.com/gitleaks/gitleaks/commit/c4526b2) refactor(generic-api-key): remove hard-coded 'magic' ([#​1600](https://github.com/gitleaks/gitleaks/issues/1600)) - [`748076d`](https://github.com/gitleaks/gitleaks/commit/748076d) chore(generate): add failing test case ([#​1690](https://github.com/gitleaks/gitleaks/issues/1690)) ### [`v8.23.0`](https://github.com/gitleaks/gitleaks/releases/tag/v8.23.0) [Compare Source](https://github.com/gitleaks/gitleaks/compare/v8.22.1...v8.23.0) ##### Changelog - [`db8e5e6`](https://github.com/gitleaks/gitleaks/commit/db8e5e6) feat(generate): use multiple allowlists ([#​1691](https://github.com/gitleaks/gitleaks/issues/1691)) - [`973c794`](https://github.com/gitleaks/gitleaks/commit/973c794) chore(rules): include fps in reference ([#​1471](https://github.com/gitleaks/gitleaks/issues/1471)) - [`f0d4499`](https://github.com/gitleaks/gitleaks/commit/f0d4499) Add comma as operator for GenerateSemiGenericRegex ([#​1679](https://github.com/gitleaks/gitleaks/issues/1679)) - [`ab38a46`](https://github.com/gitleaks/gitleaks/commit/ab38a46) refactor: central logger ([#​1692](https://github.com/gitleaks/gitleaks/issues/1692)) - [`b022d1c`](https://github.com/gitleaks/gitleaks/commit/b022d1c) friendship ended with tines READ THIS!!! The default gitleaks config now uses `[[rules.allowlists]]` ```toml ##### ⚠️ In v8.21.0 `[rules.allowlist]` was replaced with `[[rules.allowlists]]`. ##### This change was backwards-compatible: instances of `[rules.allowlist]` still work. # ##### You can define multiple allowlists for a rule to reduce false positives. ##### A finding will be ignored if _ANY_ `[[rules.allowlists]]` matches. [[rules.allowlists]] description = "ignore commit A" ##### When multiple criteria are defined the default condition is "OR". ##### e.g., this can match on |commits| OR |paths| OR |stopwords|. condition = "OR" commits = [ "commit-A", "commit-B"] paths = [ '''go\.mod''', '''go\.sum''' ] ##### note: stopwords targets the extracted secret, not the entire regex match ##### like 'regexes' does. (stopwords introduced in 8.8.0) stopwords = [ '''client''', '''endpoint''', ] [[rules.allowlists]] ##### The "AND" condition can be used to make sure all criteria match. ##### e.g., this matches if |regexes| AND |paths| are satisfied. condition = "AND" ##### note: |regexes| defaults to check the _Secret_ in the finding. ##### Acceptable values for |regexTarget| are "secret" (default), "match", and "line". regexTarget = "match" regexes = [ '''(?i)parseur[il]''' ] paths = [ '''package-lock\.json''' ] ``` ### [`v8.22.1`](https://github.com/gitleaks/gitleaks/releases/tag/v8.22.1) [Compare Source](https://github.com/gitleaks/gitleaks/compare/v8.22.0...v8.22.1) ##### Changelog - [`b69b515`](https://github.com/gitleaks/gitleaks/commit/b69b515) Entropy trace ([#​1659](https://github.com/gitleaks/gitleaks/issues/1659)) - [`7357adc`](https://github.com/gitleaks/gitleaks/commit/7357adc) build: add 'toolchain' to go.mod ([#​1682](https://github.com/gitleaks/gitleaks/issues/1682)) - [`4c3da6e`](https://github.com/gitleaks/gitleaks/commit/4c3da6e) refactor(detect): create readUntilSafeBoundary + add tests ([#​1676](https://github.com/gitleaks/gitleaks/issues/1676)) - [`dbe3746`](https://github.com/gitleaks/gitleaks/commit/dbe3746) twitter really does suck ass now - [`7edfc6b`](https://github.com/gitleaks/gitleaks/commit/7edfc6b) chore(tests): test cases for generate.go ([#​1623](https://github.com/gitleaks/gitleaks/issues/1623)) - [`efe40ca`](https://github.com/gitleaks/gitleaks/commit/efe40ca) fix: only use non-empty secret groups ([#​1632](https://github.com/gitleaks/gitleaks/issues/1632)) - [`7cb5f6f`](https://github.com/gitleaks/gitleaks/commit/7cb5f6f) build: upgrade sprig v2->v3 ([#​1674](https://github.com/gitleaks/gitleaks/issues/1674)) - [`2930537`](https://github.com/gitleaks/gitleaks/commit/2930537) fix: generate report file even if no findings ([#​1673](https://github.com/gitleaks/gitleaks/issues/1673)) ### [`v8.22.0`](https://github.com/gitleaks/gitleaks/releases/tag/v8.22.0) [Compare Source](https://github.com/gitleaks/gitleaks/compare/v8.21.4...v8.22.0) ##### Changelog - [`a91c671`](https://github.com/gitleaks/gitleaks/commit/a91c671) replace std library regex engine with go-re2 ([#​1669](https://github.com/gitleaks/gitleaks/issues/1669)) *** This bumps the gitleaks binary size from around 8.5MB to 15MB but yields 2-4x speedup. Worth it imo. If you feel strongly against this change feel free to open an issue where we can discuss the tradeoffs in more depth. Credit to [@​ahrav](https://github.com/ahrav) ### [`v8.21.4`](https://github.com/gitleaks/gitleaks/releases/tag/v8.21.4) [Compare Source](https://github.com/gitleaks/gitleaks/compare/v8.21.3...v8.21.4) ##### Changelog - [`906085f`](https://github.com/gitleaks/gitleaks/commit/906085f) Update golang version to 1.23 ([#​1672](https://github.com/gitleaks/gitleaks/issues/1672)) - [`8a83062`](https://github.com/gitleaks/gitleaks/commit/8a83062) log bytes ([#​1670](https://github.com/gitleaks/gitleaks/issues/1670)) ### [`v8.21.3`](https://github.com/gitleaks/gitleaks/releases/tag/v8.21.3) [Compare Source](https://github.com/gitleaks/gitleaks/compare/v8.21.2...v8.21.3) ##### Changelog - [`a9e6d8c`](https://github.com/gitleaks/gitleaks/commit/a9e6d8c) go mod 1.23 - [`2f73a3e`](https://github.com/gitleaks/gitleaks/commit/2f73a3e) Ensure keywords are downcased ([#​1633](https://github.com/gitleaks/gitleaks/issues/1633)) - [`f696605`](https://github.com/gitleaks/gitleaks/commit/f696605) feat: add settlemint api keys detection ([#​1663](https://github.com/gitleaks/gitleaks/issues/1663)) - [`0bf13fc`](https://github.com/gitleaks/gitleaks/commit/0bf13fc) feat(dir): better chunking ([#​1665](https://github.com/gitleaks/gitleaks/issues/1665)) - [`83e99ba`](https://github.com/gitleaks/gitleaks/commit/83e99ba) feat(report): allow user-defined templates ([#​1650](https://github.com/gitleaks/gitleaks/issues/1650)) - [`e393d29`](https://github.com/gitleaks/gitleaks/commit/e393d29) Add support for GitLab routable tokens ([#​1656](https://github.com/gitleaks/gitleaks/issues/1656)) - [`263ce82`](https://github.com/gitleaks/gitleaks/commit/263ce82) Add freemius secret key detection ([#​1611](https://github.com/gitleaks/gitleaks/issues/1611)) - [`3c0e068`](https://github.com/gitleaks/gitleaks/commit/3c0e068) fix(kubernetes): only match 'kind: secret' ([#​1649](https://github.com/gitleaks/gitleaks/issues/1649)) - [`f3adda0`](https://github.com/gitleaks/gitleaks/commit/f3adda0) feat: use STDOUT when report file not specified ([#​1642](https://github.com/gitleaks/gitleaks/issues/1642)) - [`ed205a5`](https://github.com/gitleaks/gitleaks/commit/ed205a5) fix(dir): skip opening file\&dir if allowlist matches ([#​1653](https://github.com/gitleaks/gitleaks/issues/1653)) - [`6018012`](https://github.com/gitleaks/gitleaks/commit/6018012) fix: increase chunk size 10kb -> 100kb ([#​1652](https://github.com/gitleaks/gitleaks/issues/1652)) - [`7f77987`](https://github.com/gitleaks/gitleaks/commit/7f77987) feat: detect sentry.io tokens in the new format ([#​1640](https://github.com/gitleaks/gitleaks/issues/1640)) - [`48a2e0e`](https://github.com/gitleaks/gitleaks/commit/48a2e0e) refactor: pre-commit hooks ([#​1627](https://github.com/gitleaks/gitleaks/issues/1627)) - [`4e303d0`](https://github.com/gitleaks/gitleaks/commit/4e303d0) fix(easypost): only detect tokens of correct length ([#​1628](https://github.com/gitleaks/gitleaks/issues/1628)) - [`c1add1d`](https://github.com/gitleaks/gitleaks/commit/c1add1d) feat(dir): continue on permission error ([#​1621](https://github.com/gitleaks/gitleaks/issues/1621)) - [`202106a`](https://github.com/gitleaks/gitleaks/commit/202106a) Add human readable description for curl rules ([#​1625](https://github.com/gitleaks/gitleaks/issues/1625)) - [`8e94f98`](https://github.com/gitleaks/gitleaks/commit/8e94f98) Add option to include `Line` field in report ([#​1616](https://github.com/gitleaks/gitleaks/issues/1616)) - [`dbb42a7`](https://github.com/gitleaks/gitleaks/commit/dbb42a7) hm (great comment) - [`2599460`](https://github.com/gitleaks/gitleaks/commit/2599460) Update README.md - [`8ffb980`](https://github.com/gitleaks/gitleaks/commit/8ffb980) nop for stupid build - [`4181ad6`](https://github.com/gitleaks/gitleaks/commit/4181ad6) Add new jira api token pattern ([#​1601](https://github.com/gitleaks/gitleaks/issues/1601)) - [`48ea14b`](https://github.com/gitleaks/gitleaks/commit/48ea14b) feat: update global & generic allowlist ([#​1618](https://github.com/gitleaks/gitleaks/issues/1618)) - [`81f0002`](https://github.com/gitleaks/gitleaks/commit/81f0002) fix(vault-service-token): ensure that TPS contains digits ([#​1614](https://github.com/gitleaks/gitleaks/issues/1614)) - [`c11adc9`](https://github.com/gitleaks/gitleaks/commit/c11adc9) Generate comprehensive secret samples ([#​1484](https://github.com/gitleaks/gitleaks/issues/1484)) - [`d1d9054`](https://github.com/gitleaks/gitleaks/commit/d1d9054) fix(aws): detect token in url ([#​1615](https://github.com/gitleaks/gitleaks/issues/1615)) - [`5fe58bf`](https://github.com/gitleaks/gitleaks/commit/5fe58bf) fix(rules): entropy, uppercase in samples ([#​1593](https://github.com/gitleaks/gitleaks/issues/1593)) - [`5c2e813`](https://github.com/gitleaks/gitleaks/commit/5c2e813) feat: tweak rules ([#​1608](https://github.com/gitleaks/gitleaks/issues/1608)) ### [`v8.21.2`](https://github.com/gitleaks/gitleaks/releases/tag/v8.21.2) [Compare Source](https://github.com/gitleaks/gitleaks/compare/v8.21.1...v8.21.2) ##### Changelog - [`43fae35`](https://github.com/gitleaks/gitleaks/commit/43fae35) feat(rules): create Octopus Deploy api key ([#​1602](https://github.com/gitleaks/gitleaks/issues/1602)) - [`a158e4f`](https://github.com/gitleaks/gitleaks/commit/a158e4f) fix(aws-access-token): only match if correct length ([#​1584](https://github.com/gitleaks/gitleaks/issues/1584)) - [`b6e0eee`](https://github.com/gitleaks/gitleaks/commit/b6e0eee) fix(config): ignore jquery/swagger w/o version ([#​1607](https://github.com/gitleaks/gitleaks/issues/1607)) - [`722e7d8`](https://github.com/gitleaks/gitleaks/commit/722e7d8) feat: add new GitLab tokens ([#​1560](https://github.com/gitleaks/gitleaks/issues/1560)) - [`961f2e6`](https://github.com/gitleaks/gitleaks/commit/961f2e6) feat(generic-api-key): tune false positives ([#​1606](https://github.com/gitleaks/gitleaks/issues/1606)) - [`e734fcf`](https://github.com/gitleaks/gitleaks/commit/e734fcf) Create .gitleaks.toml ([#​1605](https://github.com/gitleaks/gitleaks/issues/1605)) - [`7206d6b`](https://github.com/gitleaks/gitleaks/commit/7206d6b) feat(curl): tweak tps and fps ([#​1603](https://github.com/gitleaks/gitleaks/issues/1603)) - [`2db25f1`](https://github.com/gitleaks/gitleaks/commit/2db25f1) feat(config): ignore swagger-ui assets ([#​1604](https://github.com/gitleaks/gitleaks/issues/1604)) - [`e97695b`](https://github.com/gitleaks/gitleaks/commit/e97695b) feat(generic-api-key): exclude keywords ([#​1587](https://github.com/gitleaks/gitleaks/issues/1587)) - [`0afb525`](https://github.com/gitleaks/gitleaks/commit/0afb525) feat(okta): bump entropy to 4 ([#​1599](https://github.com/gitleaks/gitleaks/issues/1599)) - [`2068870`](https://github.com/gitleaks/gitleaks/commit/2068870) feat: update global allowlist ([#​1597](https://github.com/gitleaks/gitleaks/issues/1597)) - [`8cf93b9`](https://github.com/gitleaks/gitleaks/commit/8cf93b9) refactor(allowlist): deduplicate commits & keywords ([#​1596](https://github.com/gitleaks/gitleaks/issues/1596)) - [`50c2818`](https://github.com/gitleaks/gitleaks/commit/50c2818) feat(config): ignore jquery static assets ([#​1595](https://github.com/gitleaks/gitleaks/issues/1595)) - [`455ae0a`](https://github.com/gitleaks/gitleaks/commit/455ae0a) More rule fixes ([#​1586](https://github.com/gitleaks/gitleaks/issues/1586)) - [`5407c44`](https://github.com/gitleaks/gitleaks/commit/5407c44) chore: log skipped symlinks ([#​1591](https://github.com/gitleaks/gitleaks/issues/1591)) - [`d03d6c4`](https://github.com/gitleaks/gitleaks/commit/d03d6c4) feat: match left side of identifier ([#​1585](https://github.com/gitleaks/gitleaks/issues/1585)) - [`851c11a`](https://github.com/gitleaks/gitleaks/commit/851c11a) what secrets? - [`8cfa6b2`](https://github.com/gitleaks/gitleaks/commit/8cfa6b2) fix(rules): add entropy ([#​1580](https://github.com/gitleaks/gitleaks/issues/1580)) - [`9152eaa`](https://github.com/gitleaks/gitleaks/commit/9152eaa) feat(aws): add entropy & allowlist ([#​1582](https://github.com/gitleaks/gitleaks/issues/1582)) - [`93acc6e`](https://github.com/gitleaks/gitleaks/commit/93acc6e) feat(rules): add 1password token ([#​1583](https://github.com/gitleaks/gitleaks/issues/1583)) - [`83a5724`](https://github.com/gitleaks/gitleaks/commit/83a5724) feat(config): add curl header rule ([#​1576](https://github.com/gitleaks/gitleaks/issues/1576)) ### [`v8.21.1`](https://github.com/gitleaks/gitleaks/releases/tag/v8.21.1) [Compare Source](https://github.com/gitleaks/gitleaks/compare/v8.21.0...v8.21.1) ##### Changelog - [`cf5334f`](https://github.com/gitleaks/gitleaks/commit/cf5334f) feat: add curl basic auth rule ([#​1575](https://github.com/gitleaks/gitleaks/issues/1575)) - [`d07b394`](https://github.com/gitleaks/gitleaks/commit/d07b394) Update spelling in README.md ([#​1574](https://github.com/gitleaks/gitleaks/issues/1574)) - [`5c03fa4`](https://github.com/gitleaks/gitleaks/commit/5c03fa4) refactor(allowlist): use iota for condition ([#​1569](https://github.com/gitleaks/gitleaks/issues/1569)) - [`12034a7`](https://github.com/gitleaks/gitleaks/commit/12034a7) refactor(config): temporarily switch to \[rules.allowlist] ([#​1573](https://github.com/gitleaks/gitleaks/issues/1573)) </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/79 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> |
||
|
|
4476cbb518 |
docs(decisions): add ADR-0018 — environment configuration strategy (#80)
## Summary
Records the per-environment config strategy for both apps. ADR-only — no code changes; the implementation is anchored in §Confirmation against the next feature work that touches per-environment values.
## What lands
[**ADR-0018 — Environment configuration**](docs/decisions/0018-environment-configuration-strategy.md):
- **SPA**: Angular `environment.ts` + project.json `fileReplacements`. Build-time substitution; no runtime config fetch (rejected for the LCP/TTFB cost it would add and the deploy-time HTML rewrite that the alternatives need). Concretely cleans up the hard-coded URLs we left in `observability/tracing.ts` and `home-status.service.ts` ("hard-coded for v1 — env-config when it lands" comments).
- **BFF**: keep `process.env` + small per-key boot-time validators (the shape `apps/portal-bff/src/config/check-database-url.ts` already follows). `@nestjs/config` rejected as too heavy for the current key count; `zod` not justified yet.
- **Audit log connection**: formalises the `AUDIT_DATABASE_URL` split that ADR-0013 §"Wired as features land" already pointed at. When set, the `AuditModule` instantiates a second Prisma client with `audit_writer`-only credentials (defense in depth); when unset, the dev fallback (shared pool + `SET LOCAL ROLE audit_writer` per transaction) keeps working. A boot-time `UPDATE`-rejection self-test runs against the dedicated pool when configured — refuses to start if the pool can mutate the audit table.
The §Confirmation block cross-references the env-var-as-boot-gate items already pre-figured in ADR-0009 / ADR-0010 / ADR-0012 / ADR-0013 / ADR-0014, so the validator pattern is the single landing place when those features ship.
## What does NOT change in this PR
- No `apps/portal-shell/src/environments/*.ts` files yet — landed alongside the next feature that actually needs per-environment values.
- No `AUDIT_DATABASE_URL` validator in BFF — same; lands when the second pool is wired.
- No CLAUDE.md restructuring; just one extra bullet under the Architecture summary referencing ADR-0018.
## Doc updates
- `docs/decisions/README.md` index — new row for ADR-0018.
- `CLAUDE.md` Architecture summary — one-line reference to ADR-0018 between the perf-budget and local-quality-gates entries.
## Test plan
- [ ] CI green on this PR (`format:check`).
- [ ] ADR-0018 renders in the doc index with the right tags (`frontend`, `backend`, `infrastructure`, `process`) and 2026-05-10 date.
---------
Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr>
Reviewed-on: #80
|
||
|
|
4473b1d4a4 |
chore(ci): track trivy and gitleaks binary versions via renovate custom manager (#78)
## Summary Until now the Trivy and gitleaks pins in `.gitea/workflows/*.yml` were manual — Renovate's built-in managers only see package-manager-tracked deps (npm, docker-compose images, GitHub Actions, etc.) and ignore plain env vars. The comment in each workflow telling the next contributor to "bump manually from the releases page" is the kind of friction that gets forgotten between two security advisories. Add a **custom regex manager** that picks up `# renovate: datasource=… depName=…` annotations immediately followed by an env-var assignment of the form `<NAME>_VERSION: '<version>'`. The 4 pins (`TRIVY_VERSION` + `GITLEAKS_VERSION` in both `ci.yml` and `security-scheduled.yml`) get annotated with the `github-releases` datasource and the upstream `owner/repo` depName. `extractVersionTemplate: ^v?(?<version>.+)$` strips the `v` prefix used by both projects' release tags, so the version substituted into the env var (which our shell script consumes without `v`) stays correct. ## What lands - **`renovate.json`** — new `customManagers` block. The dashboard triage header is updated to reflect the new tracking (was "not Renovate-tracked yet"). - **`.gitea/workflows/ci.yml`** — annotate the Trivy and gitleaks env vars; remove the dead "manual bump" comments. - **`.gitea/workflows/security-scheduled.yml`** — same. ## Verified Node-side dry-run of the regex against both workflow files: ``` ci.yml → trivy 0.70.0, gitleaks 8.21.0 security-scheduled.yml → trivy 0.70.0, gitleaks 8.21.0 ``` All 4 expected matches with the right `datasource` / `depName` / `currentValue` captures. ## Test plan - [ ] CI green on this PR. - [ ] After merge, the next Renovate run picks up Trivy and gitleaks as detected dependencies in the dashboard. New patch / minor releases should now produce normal Renovate PRs (auto-merging on patches per #74). - [ ] No manual "bump from the releases page" reminders left in the workflow YAML. --------- Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr> Reviewed-on: #78 |
||
|
|
922a44bb50 |
chore(ci): auto-merge low-risk renovate updates (#77)
## Summary After ~10 days of clean Renovate track record (~30 PRs merged without regression beyond the TS/ESLint/webpack-cli majors that the dashboard-approval rule now catches), enable auto-merge on the lowest-risk update types. CI is the gate — `check` / `scan` / `a11y` going red leaves the PR open for manual triage. | Update type | Pre-PR | Post-PR | | - | - | - | | **patch** | manual review | **auto-merge if CI green** | | **pin** (rolling-tag → fixed-version pin) | manual | **auto-merge if CI green** | | **digest** (image digest pin refresh) | manual | **auto-merge if CI green** | | **lockFileMaintenance** (weekly transitive refresh) | manual | **auto-merge if CI green** | | **minor** | manual review (unchanged) | manual review | | **major** | dashboard approval (unchanged) | dashboard approval | `automergeStrategy: "squash"` matches our trunk-based squash-merge convention. `automergeType: "pr"` keeps the PR + CI run as the audit trail (vs branch-direct push), and Gitea auto-merges the PR once green via the bot's existing `repo:write` permission. ## Doc updates - `renovate.json` `dependencyDashboardHeader` — the "Open" row now reflects the new reality: mostly minors, with red patches surfacing briefly when CI fails. - `docs/development.md` §"Reviewing Renovate PRs" gains a bullet describing the auto-merge for contributors landing on the project later. ## Test plan - [ ] CI green on this PR. - [ ] After merge, the next patch-level Renovate run produces a PR that auto-squashes into `main` once CI clears (visible in the merged log; no human action required). - [ ] A patch with red CI stays open in "Open" with the `dependencies` label. - [ ] Minor / major Renovate PRs continue to require human merge. --------- Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr> Reviewed-on: #77 |
||
|
|
02ac44e498 |
feat(portal-bff): audit log foundation per ADR-0013 (#76)
## Summary Lays down the append-only audit log per ADR-0013: schema declaration, first migration with role grants, NestJS `AuditWriter` service. Typed event-family methods, the separate `AUDIT_DATABASE_URL` pool, the retention job, and the live-DB integration tests are explicitly listed as "wired as features land" in the ADR's confirmation block — they ship when the matching feature ADRs do. ## What lands **Prisma schema** ([`apps/portal-bff/prisma/schema.prisma`](apps/portal-bff/prisma/schema.prisma)): - `multiSchema` preview enabled; datasource declares `public` + `audit` schemas. - `AuditEvent` model: `id` (uuid), `createdAt`, `eventType` (free-form in v1), `audience` enum (`workforce | customer`), `actorIdHash`, `traceId`, `subject`, `outcome` enum (`success | failure | denied`), `payload` (jsonb). - Indexes on `createdAt`, `eventType`, `traceId` — covering the three obvious query shapes. **Migration** ([`prisma/migrations/*_init_audit_schema/migration.sql`](apps/portal-bff/prisma/migrations/20260510011453_init_audit_schema/migration.sql)): - Standard Prisma `CREATE TABLE` / enums output, then the **append-only contract** re-applied explicitly: - `ALTER TABLE/TYPE OWNER TO audit_owner`. - `GRANT INSERT` to `audit_writer`, `SELECT` to `audit_reader`, **`SELECT, DELETE`** to `audit_archiver` (SELECT is needed to evaluate the `created_at` predicate of "delete older than retention" — Postgres requires SELECT on every column referenced in DELETE's WHERE). - `GRANT USAGE` on the enum types to all three roles (without it `audit_writer.INSERT` fails with "permission denied for type"). - **No** GRANT for `UPDATE` / `TRUNCATE` to anyone — including `audit_owner` at runtime; only fresh schema migrations amend the table. **Service** ([`apps/portal-bff/src/audit/`](apps/portal-bff/src/audit/)): - `AuditWriter.recordEvent(input)` — single entry point. Wraps every INSERT in a transaction whose first statement is `SET LOCAL ROLE audit_writer`, so the role contract holds at runtime even from the otherwise-privileged BFF connection. - `traceId` auto-resolved from the active OTel span (so audit row joins with traces and Pino logs on the same `trace_id`). - `actorIdHash` auto-resolved from CLS (key `actorIdHash`) with explicit input-side override; `null` when neither is set (placeholder until ADR-0009 / ADR-0010 guards populate CLS). - Errors propagate (no catch-and-swallow), per ADR-0013's "blocking writes: no audit ⇒ no action". **Tests** — 8 unit tests on `AuditWriter` (mocked Prisma + CLS): role-locking ordering, input pass-through, `Prisma.JsonNull` for missing payload, CLS-vs-input precedence on `actorIdHash`, OTel trace capture, error propagation. ## End-to-end verification (manual, against local-dev Postgres) ``` INSERT under audit_writer: ok UPDATE under audit_writer: permission denied for table events DELETE under audit_writer: permission denied for table events DELETE under audit_archiver: ok, row removed (after the SELECT-grant fix) ``` ## ADR-0013 §Confirmation rewritten Two-block split: "wired in foundation PR" lists what landed here; "wired as features land" lists the typed event-family methods, AUDIT_DATABASE_URL connection split, startup self-test probe, retention purge job, salt-shared cross-correlation test, and live-DB role-contract integration tests — each anchored to the feature ADR that triggers it. ## Recovery for anyone with a pre-existing local-dev DB If your local-dev Postgres already had the audit migration applied **before** the SELECT-grant fix, the archiver's DELETE will fail. Two options: 1. Apply the missing grant directly: ```bash psql "$DATABASE_URL" -c "GRANT SELECT ON audit.events TO audit_archiver;" ``` 2. Or wipe the volume and re-migrate cleanly: ```bash ./infra/local/dev.sh down -v ./infra/local/dev.sh up pnpm --filter @apf-portal/source exec prisma migrate deploy # or `cd apps/portal-bff && pnpm exec prisma migrate deploy` ``` Fresh DBs land with the corrected migration directly. ## Out of scope (separate PRs) - Typed event-family methods (`signIn`, `signInFailed`, …) — added per matching feature ADR. - `AUDIT_DATABASE_URL` separate connection pool — defense-in-depth, when production needs it. - Startup self-test probe (deliberate failing UPDATE asserting rejection) — lands with the connection split. - Retention purge job (`audit_archiver` daily cron) — phase-3b infra. - Live-DB integration tests asserting the role contract — Testcontainers-style harness, separate PR. ## Test plan - [ ] CI green on this PR. - [ ] `prisma migrate deploy` succeeds on a fresh DB (the recovery instructions cover the SELECT-grant gap for already-migrated dev DBs). - [ ] `psql -c "\dp audit.events"` shows the expected privilege matrix: `audit_owner=arwdDxtm/audit_owner`, `audit_writer=a/audit_owner`, `audit_reader=r/audit_owner`, `audit_archiver=rd/audit_owner`. - [ ] BFF boots; calling `AuditWriter.recordEvent` from a controller (manual smoke once a real flow lands) writes to `audit.events` with the expected `trace_id` matching the request's Jaeger span. --------- Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr> Reviewed-on: #76 |
||
|
|
e2dd2e4dd8 |
fix(portal-shell): use .postcssrc.json so tailwind utilities are emitted (#75)
## Summary The portal-shell scaffold shipped a `postcss.config.js`, but `@angular/build` (Angular 17+ esbuild pipeline) does **not** load that file format — it only reads `.postcssrc.json`. Result: the `@tailwindcss/postcss` plugin was never registered, Tailwind ran with no source files visible, generated only its `@theme` block, and dropped every utility class on the floor. The page therefore rendered unstyled — black text on white — even though `nx build` reported success and shipped a 23 KB `styles*.css`. Rename / re-encode the config to `.postcssrc.json`. Same plugin, same options, just the file format Angular's esbuild adapter actually reads. ## Verification | | Before | After | | - | - | - | | Class selectors in `dist/apps/portal-shell/browser/styles*.css` | **0** | **75** (production) | | `.text-3xl`, `.bg-amber-50`, `.font-semibold` etc. emitted | ❌ | ✓ | | Pages render with intended Tailwind layout | ❌ | ✓ | ```bash pnpm exec nx build portal-shell --configuration=production grep -oE '\.[a-zA-Z][a-zA-Z0-9_-]*' dist/apps/portal-shell/browser/styles*.css | sort -u | wc -l # 75 ``` ## Doc update `docs/development.md` repo-layout walkthrough now reads `.postcssrc.json` and explicitly calls out that `postcss.config.js` is ignored by `@angular/build` — so a future contributor reaching for the legacy filename gets a hint instead of a silent failure. ## Test plan - [ ] CI green on this PR. - [ ] Local: bring up the SPA dev server, `localhost:4200` shows the styled layout — header bar with brand link, system-status widget with rounded card, footer with the two language links. - [ ] Production build: `nx build portal-shell --configuration=production` succeeds; `dist/.../styles*.css` contains tens of utility-class selectors (not just `@theme` tokens). --------- Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr> Reviewed-on: #75 |
||
|
|
0d31937aeb |
feat(portal-shell): replace nx-welcome with real shell and home + budgets per ADR-0017 (#74)
## Summary Cuts the Nx scaffold's `NxWelcome` page (938 LOC of placeholder markup) and lays down the actual portal layout — header, main, footer landmarks plus a skip link — with a real home page and the two RGAA-mandated accessibility statement routes. Bundle budgets in `project.json` are tightened to match ADR-0017. ## What lands **Layout shell** (`apps/portal-shell/src/app/`): - `components/header/` — brand link + primary nav landmark stub - `components/footer/` — accessibility statement links (FR + EN) + version badge - `app.html` / `app.scss` — flex column with sticky footer + skip link to `#main-content` (WCAG 2.4.1) **Pages**: - `pages/home/` — welcome heading + "system status" widget that fetches `/api/health` from the BFF. Doubles as a smoke test of the full SPA → BFF stack: CORS, OTel trace propagation, Pino log correlation. After page load, http://localhost:16686 should show one trace whose root span is the SPA `document_load`, with a child `fetch` span that has its own child `HTTP GET /api/health` BFF span. - `pages/accessibility/` — single component, content selected by route data (`lang: 'fr' | 'en'`). Mounted at `/accessibility` (en) and `/accessibilite` (fr). v1 carries placeholder copy explicitly framed as "awaiting APF user panel review" — RGAA + ADR-0016 require the routes to exist; the substantive statement (audit grid, gaps list, remediation plan) lands separately. **Routing & wiring**: - `app.routes.ts` adds the three routes, all lazy-loaded. - `app.config.ts` adds `provideHttpClient(withFetch())` so HttpClient delegates to the patched browser `fetch` and the W3C `traceparent` header propagates automatically (per ADR-0012 phase 2). - `nx-welcome.ts` removed; `app.ts` no longer imports it. **Bundle budgets** (`apps/portal-shell/project.json`): | Type | Limit (raw) | ADR-0017 (gzip) | | ------------------- | ------------ | --------------- | | `initial` | 1 MB | ≤ 300 KB | | `anyScript` | 300 KB | ≤ 100 KB / chunk | | `anyComponentStyle` | 6 KB | ≤ 6 KB | | `bundle "styles"` | 150 KB | ≤ 150 KB | `maximumWarning == maximumError` so an overshoot fails the build (matching `type: "error"` in spirit). Angular CLI compares **raw** sizes; ADR-0017 §Confirmation is updated to record the gzip→raw translation and the deferred follow-up that will add a CI check on the actual gzipped transfer size (Angular has no native gzip-mode budget). ## Verified locally - `pnpm exec nx run-many -t lint test build` → 8 projects green; **12 tests pass** for `portal-shell` across 5 specs (App, Header, Footer, Home, AccessibilityStatement). - `pnpm nx build portal-shell --configuration=production` → initial bundle **326.99 KB raw / 89.82 KB transfer** (gzip) — well under the ADR-0017 300 KB gzip target. Lazy chunks: home 2.66 KB, accessibility 2.90 KB. - `pnpm audit` clean. After `./infra/local/dev.sh up observability` + `pnpm nx serve portal-bff` + `pnpm nx serve portal-shell`, opening http://localhost:4200 shows the new layout, the system-status widget connects to the BFF, and the corresponding trace appears in Jaeger. ## Out of scope (separate PRs) - **Real RGAA audit content** — APF user-panel review. - **Design tokens system** in `libs/shared/tokens`. - **Reusable UI primitives** in `libs/shared/ui`. - **User-preferences panel** (ADR-0016 §"User-preferences panel"). - **i18n machinery** (`@angular/localize`) — for v1 the FR/EN copy is inlined and route-driven; real i18n is a separate ADR. - **Env-config** (Angular `environment.ts`) — the hard-coded `http://localhost:3000/api/health` will move there alongside the OTLP endpoint when the env-config PR lands. - **CI gzip-transfer-size assertion** to complement the raw-size Angular budgets. ## Test plan - [ ] CI green on this PR. - [ ] Local: home page renders, system-status widget transitions from "Checking the backend…" to the success state when the BFF is up; falls back to "Backend unreachable" with the BFF stopped. - [ ] `/accessibility` and `/accessibilite` render the matching language with `<article lang="en|fr">`. - [ ] Skip link reachable via Tab from address bar; clicking it focuses `#main-content`. - [ ] Production build size summary roughly matches the figures above (initial total ≈ 90 KB transfer). --------- Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr> Reviewed-on: #74 |
||
|
|
288610b9ba |
docs(development): add observability dev-loop section (#73)
## Summary ADR-0012 phase 1 + phase 2 are wired (BFF + SPA), so the "Observability dev-loop" placeholder in the roadmap table now has real content to point at. Promote it from §9 future-work list to a full §5 walkthrough sitting between "Daily commands" and "Dependency updates". ## What §5 covers - **Bringing up the observability stack** — `./infra/local/dev.sh up observability`, with the endpoint table (Jaeger UI, OTLP receiver, BFF stdout, browser DevTools). - **Reading a trace in Jaeger** — service-dropdown filter, span attribute keys to look at (`http.method`, `db.statement`, `service.name`, `trace_id`), the trace-id-as-pivot pattern. - **Correlating a trace with the BFF Pino logs** via `trace_id` and `request_id` (the per-request UUID `nestjs-cls` provides). Concrete `grep` example. - **Reading SPA spans from the browser** — DevTools Network filter on `localhost:4318/v1/traces` + Jaeger UI cross-check. - **Common gotchas** — observability profile not active, CORS pre-flight on `/v1/traces`, `propagateTraceHeaderCorsUrls` mismatches, NODE_ENV mis-set, EADDRINUSE on serve restart. - **What's not in v1** — pointer at the "wired as features land" deltas (CLS keys for session/user/audience, redact list, custom domain spans, prod backend) so a contributor knows what's intentionally not yet there. ## Renumbering The new §5 pushes existing sections down. Final structure: 1. Repo layout / 2. Prerequisites / 3. Initial setup / 4. Daily commands / 5. **Observability dev-loop** / 6. Dependency updates (Renovate) / 7. Conventional commit cycle / 8. Where to look / 9. Roadmap. The "Observability dev-loop" line in §9's roadmap table is removed (now implemented). `docs/README.md` cross-link updated to mention the new section. ## Test plan - [ ] CI green on this PR (`format:check`). - [ ] In a fresh checkout, follow §5's "Bring up the observability stack" verbatim, reach the Jaeger UI, then walk a trace through the `grep` correlation example with a synthetic request — flag any step that's wrong / missing on this real-world rehearsal. --------- Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr> Reviewed-on: #73 |
||
|
|
8f2cd4e068 |
feat(portal-shell): wire spa-side opentelemetry tracing (#72)
## Summary Phase 2 of ADR-0012 — closes the loop SPA → BFF → DB. After this PR, a single user action (initial page load, click, form submit) produces one trace whose root span is owned by the SPA and whose child spans cover the BFF request, Postgres queries through Prisma, and (eventually) Redis / downstream-API hops. ## What lands **Browser-side OTel libs** (production deps): - `@opentelemetry/sdk-trace-web` — browser tracer + provider - `@opentelemetry/exporter-trace-otlp-http` — OTLP/HTTP+JSON exporter - `@opentelemetry/instrumentation` — auto-instrumentation runtime - `@opentelemetry/instrumentation-fetch` — `fetch` + W3C `traceparent` propagation - `@opentelemetry/instrumentation-document-load` — initial-paint timings - `@opentelemetry/instrumentation-user-interaction` — click / keypress / submit No `@opentelemetry/context-zone`: the workspace is zoneless per ADR-0004; the default `StackContextManager` covers the auto-instrumented paths. Custom spans across `await` will need explicit `context.with(...)` plumbing — fine, encountered as code lands. **Code**: - [`apps/portal-shell/src/observability/tracing.ts`](apps/portal-shell/src/observability/tracing.ts) — `WebTracerProvider` bootstrap. Documents the load-order constraint inline (same pattern as the BFF: must be the very first import of `main.ts`, otherwise auto-instrumentations miss everything imported above). - `apps/portal-shell/src/main.ts` now imports the tracing module as line 1. **CORS plumbing** for end-to-end trace propagation: - BFF (`apps/portal-bff/src/main.ts`) calls `enableCors` with a minimal dev allowlist (`http://localhost:4200`) and explicit permission for the W3C `traceparent` / `tracestate` headers. The full security-grade CORS (per-environment allowlists, helmet, cookie-session, CSRF) belongs to the future phase-2 security ADR — this PR adds the strict minimum for the SPA→BFF trace context to survive cross-origin pre-flight. - OTel Collector (`infra/local/otel-collector.yaml`) gains a `cors` block on its OTLP/HTTP receiver so the browser's own OTLP POST clears its pre-flight. **ADR-0012 §Confirmation** rewritten: a new "Wired in the SPA foundation PR (phase 2)" block enumerates what landed here; the carry-over "Wired as features land" list drops the SPA-side SDK item and adds a follow-up note about the security-grade CORS. ## Verification ```bash pnpm exec nx run-many -t lint test build # 8 projects green pnpm audit # 0 vulns ./infra/local/dev.sh up observability # bring up Collector + Jaeger ./infra/local/dev.sh # (separately, BFF stack — your choice) pnpm nx serve portal-bff # localhost:3000 pnpm nx serve portal-shell # localhost:4200 ``` Open http://localhost:4200 → a `document_load` trace appears in http://localhost:16686 with `service.name=portal-shell`. From DevTools, run `fetch('http://localhost:3000/api/health').then(r => r.json())` → a fetch span appears with a child BFF span on the same trace. ## Test plan - [ ] CI green on this PR. - [ ] After local up, `document_load` span visible in Jaeger UI for the SPA. - [ ] Cross-origin fetch from SPA carries `traceparent` (visible in Network tab) and produces a single end-to-end trace SPA → BFF in Jaeger. - [ ] DevTools console shows no CORS warnings about `traceparent`, `tracestate`, or the `localhost:4318/v1/traces` POST. --------- Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr> Reviewed-on: #72 |
||
|
|
c3c15585ff |
style(portal-bff): apply prettier to check-database-url.ts (#71)
## Summary Cosmetic-only follow-up to #70. Collapses one over-cautious multi-line `throw new Error(...)` into a single line that fits within the project's 100-char `printWidth`. The reformat was produced by lint-staged **during** the amend that landed #70, but applied to the working tree only — the modification never made it back into the staged content of the amend. The merged commit therefore carries the un-prettified version. Spotted locally with `pnpm exec prettier --check apps/portal-bff/src/config/check-database-url.ts` failing. Left unchecked, the next PR's `check` job would break at `format:check` for unrelated reasons. ## Test plan - [ ] CI green on this PR (`format:check` clean). - [ ] No behavioural change — only whitespace. --------- Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr> Reviewed-on: #71 |
||
|
|
b74d3f1b9b |
feat(portal-bff): observability foundations (Pino + CLS + OTel) (#70)
## Summary
Implements ADR-0012 phase 1, BFF side. The SPA wiring is a separate phase-2 PR.
The BFF now emits structured JSON logs to stdout, tagged with `trace_id` / `span_id` from the active OTel context, and exports OTLP traces over HTTP/Protobuf to the Collector that already runs in the local-dev compose. Anything Nest, Express, HTTP-out, Prisma (Postgres) or `ioredis` does is auto-spanned. A `GET /api/health` liveness endpoint is added to round things out.
## What lands
**Runtime libs added** (production deps):
- `nestjs-pino`, `pino`, `pino-http` — structured logging
- `nestjs-cls` — request-scoped context
- `@opentelemetry/api` / `sdk-node` / `resources` / `semantic-conventions`
- `@opentelemetry/exporter-trace-otlp-proto` (HTTP/Protobuf, port 4318)
- `@opentelemetry/instrumentation-{http,express,nestjs-core,pg,ioredis,pino}` — curated, **no** `auto-instrumentations-node` mass-import (anti-bricolage)
Dev: `pino-pretty` (gated by `NODE_ENV`).
**Code:**
- `apps/portal-bff/src/observability/tracing.ts` — OTel `NodeSDK` bootstrap. Documents the load-order constraint inline (must be the very first import of `main.ts`). Pure side-effect module.
- `apps/portal-bff/src/observability/observability.module.ts` — composes `ClsModule` (UUID per request stored as `request_id`) and `LoggerModule` (`pino-pretty` in dev, raw JSON in prod, `LOG_LEVEL` env-driven, `/health` excluded from auto-logging, `X-Request-Id` honoured if inbound).
- `apps/portal-bff/src/health/{health.controller,health.module,health.controller.spec}.ts` — `GET /api/health` returning `{status, uptimeSeconds, service, version}`. Cheap liveness only — `/readiness` lands when dependencies have a readiness story.
- `apps/portal-bff/src/config/check-database-url.{ts,spec.ts}` — fail-fast validator called from `main.ts` before NestFactory boots. Catches the same family of bug that bit pgweb in #63: a literal special character in `POSTGRES_PASSWORD` that needs URL-encoding in `DATABASE_URL`. Prisma requires a URL string (no discrete-flag escape hatch), so early validation + a clear error message is the v1 mitigation. Six unit tests cover happy path, missing URL, wrong scheme, encoded special chars, literal `@` in password, malformed URL.
**Wiring:**
- `main.ts` imports `./observability/tracing` as line 1, then uses `app.get(Logger)` from `nestjs-pino` with `bufferLogs: true` so early-bootstrap lines are not lost.
- `app.module.ts` imports `ObservabilityModule` first, then `PrismaModule`, then `HealthModule`.
- `apps/portal-bff/.env.example` promotes `LOG_LEVEL`, `OTEL_SERVICE_NAME`, `OTEL_SERVICE_VERSION`, `OTEL_EXPORTER_OTLP_ENDPOINT`, `OTEL_EXPORTER_OTLP_PROTOCOL`, `OTEL_TRACES_SAMPLER` from the "future" comment to active settings — defaults target the local-dev Collector.
- Both `apps/portal-bff/.env.example` and `infra/local/.env.example` now spell out the URL-encoding constraint on `POSTGRES_PASSWORD` with the char-by-char encoding table (`@` → `%40`, etc.).
**ADR-0012 §Confirmation** rewritten to distinguish what landed in this PR from what is wired as the corresponding feature ADRs ship (CLS keys for `session_id` / `user_id_hash` / `audience`, `LOG_USER_ID_SALT` enforcement, redact list, custom spans, SPA-side SDK, full integration tests, prod Collector config).
## Trace ↔ log correlation
Automatic via `@opentelemetry/instrumentation-pino` — every Pino record gets `trace_id` and `span_id` injected from the active OTel context. No CLS gymnastics needed for that concern.
## Verification
```bash
pnpm exec nx run-many -t lint test build # 8 projects green
pnpm audit --audit-level=moderate # 0 vulnerabilities
./infra/local/dev.sh up observability # start Collector + Jaeger
cp apps/portal-bff/.env.example apps/portal-bff/.env
pnpm nx serve portal-bff
curl http://localhost:3000/api/health
# → {"status":"ok","uptimeSeconds":N,"service":"portal-bff","version":"dev"}
```
Then hit `GET http://localhost:3000/api` once or twice and open http://localhost:16686 — the corresponding spans appear in Jaeger, and Pino logs on stdout carry the matching `trace_id`.
## Test plan
- [ ] `nx run-many -t lint test build` green on this PR's CI run.
- [ ] `pnpm audit` clean.
- [ ] BFF boots, `/api/health` returns the expected JSON.
- [ ] Pino logs in dev are colourised one-liners; in prod they would be raw JSON (toggled by `NODE_ENV=production`).
- [ ] With the local-dev stack's `--profile observability` active, traces are visible in Jaeger UI.
- [ ] Each Pino log line for a request carries the same `trace_id` as the trace span in Jaeger.
---------
Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr>
Reviewed-on: #70
|
||
|
|
80e35c6aab |
chore(deps): update dependency lint-staged to v17.0.4 (#69)
This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [lint-staged](https://github.com/lint-staged/lint-staged) | devDependencies | patch | [`17.0.3` -> `17.0.4`](https://renovatebot.com/diffs/npm/lint-staged/17.0.3/17.0.4) | --- ### Release Notes <details> <summary>lint-staged/lint-staged (lint-staged)</summary> ### [`v17.0.4`](https://github.com/lint-staged/lint-staged/blob/HEAD/CHANGELOG.md#1704) [Compare Source](https://github.com/lint-staged/lint-staged/compare/v17.0.3...v17.0.4) ##### Patch Changes - [#​1788](https://github.com/lint-staged/lint-staged/pull/1788) [`f95c1f8`](https://github.com/lint-staged/lint-staged/commit/f95c1f8df3368758c44c2052e568aac1b3d4c767) - Another fix for making sure *lint-staged* adds task modifications correctly to the commit in the following cases: - after editing `<file>` it is staged with `git add <file>`, and then committed with `git commit` - after editing `<file>` it is committed with `git commit --all` without explicit `git add` - after editing `<file>` it is committed with `git commit <pathspec>` without explicit `git add` There's new test cases which actually setup the Git `pre_commit` hook to run *lint-staged* and verify them. These issues started in **v17.0.0** when trying to improve support for committig without having explicitly staged files. </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: #69 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> |
||
|
|
fc9b63f24a |
feat(infra): add dev.sh wrapper for the local-dev compose stack (#68)
## Summary Two recurring frictions on the local-dev stack: 1. **Compose-profile asymmetry** — `docker compose down` only operates on services whose profile is currently active. Anything brought up with `--profile X` keeps running unless the same flag is passed on `down`. pgweb and Jaeger silently survived several `down -v` invocations before we noticed (#67 documented the gotcha; this PR makes it impossible to hit if you use the wrapper). 2. **Verbose invocations** — typing `docker compose -f infra/local/dev.compose.yml --profile … <verb>` for routine ops gets old fast. Add [`infra/local/dev.sh`](infra/local/dev.sh) as a thin wrapper. Always passes every profile in scope on teardown / status / log commands, exposes ergonomic verbs: | Command | Effect | | --------------------------------------------- | --------------------------------------------------------------- | | `./infra/local/dev.sh up` | Core only (postgres + redis + otel-collector) | | `./infra/local/dev.sh up all` | Core + every profile | | `./infra/local/dev.sh up dbtools` | Core + pgweb | | `./infra/local/dev.sh up observability` | Core + Jaeger | | `./infra/local/dev.sh down [-v]` | Tear down (every profile in scope, no orphaned services) | | `./infra/local/dev.sh stop <service>` | Stop one service (containers stay around) | | `./infra/local/dev.sh restart <service>` | Restart one service | | `./infra/local/dev.sh status` | `ps` with every profile visible | | `./infra/local/dev.sh logs [service]` | Follow logs | | `./infra/local/dev.sh exec <service> <cmd>` | Run a command inside a container | Anything not matching one of the named verbs is passed through to `docker compose -f dev.compose.yml ...` (with every profile flagged in), so the full Compose surface remains available. ## Doc updates - **`infra/README.md`** — new "Convenience script" subsection with the cheat-sheet table; "First-time setup" rewritten to use the script; the standalone "Profile symmetry" tip from #67 is collapsed into a one-liner since the script now handles it (the note remains as a fallback for direct `docker compose` users). - **`docs/development.md` §3** — points at the script for the typical setup flow. The compose file itself is unchanged. ## Test plan - [ ] `./infra/local/dev.sh help` prints the usage block. - [ ] `./infra/local/dev.sh up` brings up the 3 core services (no pgweb / no jaeger). - [ ] `./infra/local/dev.sh up all` adds pgweb and Jaeger. - [ ] `./infra/local/dev.sh down -v` stops and removes all 5 containers (incl. pgweb and Jaeger), wipes the postgres-data and redis-data volumes. - [ ] `./infra/local/dev.sh stop pgweb` stops just pgweb. - [ ] `./infra/local/dev.sh logs otel-collector` follows that service's logs. - [ ] `./infra/local/dev.sh exec postgres psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -c "\du"` lists the audit roles. - [ ] `./infra/local/dev.sh stop` (no arg) errors with a clear message. --------- Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr> Reviewed-on: #68 |
||
|
|
4d2d4d652a |
docs(infra): document Compose profile + down/up symmetry gotcha (#67)
## Summary `docker compose -f infra/local/dev.compose.yml down -v` left pgweb and Jaeger running, with no error and no obvious diagnostic — they kept eating memory until the next reboot. Reason: Compose only operates on services whose profile is currently active. Bringing them up with `--profile dbtools` / `--profile observability` requires the same flag(s) on `down`, otherwise they're invisible to that command. Document the gotcha in `infra/README.md` "Local-dev stack" → "Operational tips", with the two pragmatic resolutions: 1. Pass the same flags on each command (most explicit). 2. Set `COMPOSE_PROFILES=dbtools,observability` once in the shell or `infra/local/.env`, and let it propagate to every `up` / `down` / `ps` invocation. ## Test plan - [ ] Bring up the full stack: `docker compose -f infra/local/dev.compose.yml --profile dbtools --profile observability up -d`. - [ ] `docker compose -f infra/local/dev.compose.yml down -v` (no flags) — confirms pgweb + jaeger keep running. - [ ] Run the documented "complete" command — confirms everything is gone. - [ ] Repeat with `COMPOSE_PROFILES` set; same outcome. --------- Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr> Reviewed-on: #67 |
||
|
|
b2d7fe3fd7 |
chore(deps): update dependency jest to v30.4.2 (#65)
This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [jest](https://jestjs.io/) ([source](https://github.com/jestjs/jest/tree/HEAD/packages/jest)) | devDependencies | patch | [`30.4.1` -> `30.4.2`](https://renovatebot.com/diffs/npm/jest/30.4.1/30.4.2) | --- ### Release Notes <details> <summary>jestjs/jest (jest)</summary> ### [`v30.4.2`](https://github.com/jestjs/jest/blob/HEAD/CHANGELOG.md#3042) [Compare Source](https://github.com/jestjs/jest/compare/v30.4.1...v30.4.2) ##### Fixes - `[jest-runtime]` Fix named imports from CJS modules whose `module.exports` is a function with own-property exports ([#​16150](https://github.com/jestjs/jest/pull/16150)) </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: #65 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> |
||
|
|
3fbb1f92a8 |
chore(deps): update tailwind css to v4.3.0 (#66)
This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [@tailwindcss/postcss](https://tailwindcss.com) ([source](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/@tailwindcss-postcss)) | devDependencies | minor | [`4.2.4` -> `4.3.0`](https://renovatebot.com/diffs/npm/@tailwindcss%2fpostcss/4.2.4/4.3.0) | | [tailwindcss](https://tailwindcss.com) ([source](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/tailwindcss)) | devDependencies | minor | [`4.2.4` -> `4.3.0`](https://renovatebot.com/diffs/npm/tailwindcss/4.2.4/4.3.0) | --- ### Release Notes <details> <summary>tailwindlabs/tailwindcss (@​tailwindcss/postcss)</summary> ### [`v4.3.0`](https://github.com/tailwindlabs/tailwindcss/blob/HEAD/CHANGELOG.md#430---2026-05-08) [Compare Source](https://github.com/tailwindlabs/tailwindcss/compare/v4.2.4...v4.3.0) ##### Added - Add `@container-size` utility ([#​18901](https://github.com/tailwindlabs/tailwindcss/pull/18901)) - Add `scrollbar-{auto,thin,none}` utilities for `scrollbar-width`, and `scrollbar-thumb-*` / `scrollbar-track-*` color utilities for `scrollbar-color` ([#​19981](https://github.com/tailwindlabs/tailwindcss/pull/19981), [#​20019](https://github.com/tailwindlabs/tailwindcss/pull/20019)) - Add `scrollbar-gutter-*` utilities ([#​20018](https://github.com/tailwindlabs/tailwindcss/pull/20018)) - Add `zoom-*` utilities ([#​20020](https://github.com/tailwindlabs/tailwindcss/pull/20020)) - Add `tab-*` utilities ([#​20022](https://github.com/tailwindlabs/tailwindcss/pull/20022)) - Allow using `@variant` with stacked variants (e.g. `@variant hover:focus { … }`) ([#​19996](https://github.com/tailwindlabs/tailwindcss/pull/19996)) - Allow using `@variant` with compound variants (e.g. `@variant hover, focus { … }`) ([#​19996](https://github.com/tailwindlabs/tailwindcss/pull/19996)) - Support `--default(…)` in `--value(…)` and `--modifier(…)` for functional `@utility` definitions ([#​19989](https://github.com/tailwindlabs/tailwindcss/pull/19989)) ##### Fixed - Ensure `@plugin` resolves package JavaScript entries instead of browser CSS entries when using `@tailwindcss/vite` ([#​19949](https://github.com/tailwindlabs/tailwindcss/pull/19949)) - Fix relative `@import` and `@plugin` paths resolving from the wrong directory when using `@tailwindcss/vite` ([#​19965](https://github.com/tailwindlabs/tailwindcss/pull/19965)) - Ensure CSS files containing `@variant` are processed by `@tailwindcss/vite` ([#​19966](https://github.com/tailwindlabs/tailwindcss/pull/19966)) - Resolve imports relative to `base` when `result.opts.from` is not provided when using `@tailwindcss/postcss` ([#​19980](https://github.com/tailwindlabs/tailwindcss/pull/19980)) - Canonicalization: preserve significant `_` whitespace in arbitrary values ([#​19986](https://github.com/tailwindlabs/tailwindcss/pull/19986)) - Canonicalization: add parentheses when removing whitespace from arbitrary values would hurt readability (e.g. `w-[calc(100%---spacing(60))]` → `w-[calc(100%-(--spacing(60)))]`) ([#​19986](https://github.com/tailwindlabs/tailwindcss/pull/19986)) - Canonicalization: preserve the original unit in arbitrary values instead of normalizing to base units (e.g. `-mt-[20in]` → `mt-[-20in]`, not `mt-[-1920px]`) ([#​19988](https://github.com/tailwindlabs/tailwindcss/pull/19988)) - Canonicalization: migrate arbitrary `:has()` variants from `[&:has(…)]` to `has-[…]` ([#​19991](https://github.com/tailwindlabs/tailwindcss/pull/19991)) - Upgrade: don’t migrate inline `style` attributes (e.g. `style="flex-grow: 1"` → `style="flex-grow: 1"`, not `style="grow: 1"`) ([#​19918](https://github.com/tailwindlabs/tailwindcss/pull/19918)) - Allow multiple `@utility` definitions with the same name but different value types ([#​19777](https://github.com/tailwindlabs/tailwindcss/pull/19777)) - Export missing `PluginWithConfig` type from `tailwindcss/plugin` to fix errors when inferring plugin config types ([#​19707](https://github.com/tailwindlabs/tailwindcss/pull/19707)) - Ensure `start` and `end` legacy utilities without values do not generate CSS ([#​20003](https://github.com/tailwindlabs/tailwindcss/pull/20003)) - Ensure `--value(…)` is required in functional `@utility` definitions ([#​20005](https://github.com/tailwindlabs/tailwindcss/pull/20005)) - Canonicalization: preserve required whitespace around operators in negated arbitrary values (e.g. `-left-[(var(--a)+var(--b))]`) ([#​20011](https://github.com/tailwindlabs/tailwindcss/pull/20011)) </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: #66 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> |
||
|
|
a893f8e06b |
chore(infra): migrate Jaeger to v2 for built-in dark theme (#64)
## Summary Jaeger v1's web UI has no theme selector — it ships light-only. v2 (the OTel-Collector-based rewrite, image `jaegertracing/jaeger`, distinct from v1's `jaegertracing/all-in-one`) ships a **Light / Dark / Auto** switcher in the UI nav. v2 is also the actively-developed line; v1 is on the way out within ~6-12 months. Migration is mechanical: - Image: `jaegertracing/all-in-one:1.76.0` → `jaegertracing/jaeger:2.17.0`. - Drop `COLLECTOR_OTLP_ENABLED: 'true'` env — v2 enables OTLP receivers by default. - UI port (`16686`) and OTLP ports (`4317` / `4318`) are unchanged, so the collector → `jaeger:4317` forwarding pipeline keeps working without touching `otel-collector.yaml`. ## Test plan - [ ] After merge: `docker compose -f infra/local/dev.compose.yml --profile observability up -d --force-recreate jaeger` → container stays healthy. - [ ] http://localhost:16686 loads the v2 UI; nav shows the Light/Dark/Auto switcher. - [ ] Send a synthetic OTLP trace to `localhost:4317` (e.g., via grpcurl or once the BFF is wired in B) → it appears in the Jaeger UI. - [ ] OTel Collector logs no longer warn abou --------- Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr> Reviewed-on: #64 |
||
|
|
171f21b99b |
fix(infra): pass pgweb credentials via discrete CLI flags (#63)
## Summary `PGWEB_DATABASE_URL` (post-#62 rename) still fails at boot: ``` Error: Invalid URL. Valid format: postgres://user:password@host:port/db?sslmode=mode ``` Root cause: the userinfo portion of a Postgres URL must be URL-encoded — any `@`, `#`, `:`, `/`, `?`, `%`, `&`, `=`, `+`, `;` etc. in the password breaks the parser. Compose has no built-in URL encoding, so the URL we construct in YAML is fragile by design and depends on the developer happening to pick a URL-safe password. Switch to pgweb's discrete CLI flags (`--host`, `--port`, `--user`, `--pass`, `--db`, `--ssl`). Compose interpolates each value literally — no URL encoding required, any password works. The image's ENTRYPOINT already passes `--bind=0.0.0.0 --listen=8081`; our args are appended to those. ## Side benefit A missing password now yields an explicit Compose error (`POSTGRES_PASSWORD must be set in infra/local/.env`) rather than an opaque pgweb crash with a vague "Invalid URL" message. ## Test plan - [ ] After merge: `docker compose -f infra/local/dev.compose.yml --profile dbtools up -d` → pgweb stays up (no `Restarting (1)` loop). - [ ] http://localhost:8081 loads pgweb's UI; you can navigate to the `audit` schema. - [ ] Confirm with a password that contains a special char (e.g., `dev@pass#2026!`) — should still work. --------- Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr> Reviewed-on: #63 |