main
4 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
db7e479dde |
feat(spa): opt-in 'https' nx serve config for dev-server TLS (#263)
## Summary
Add an opt-in `https` configuration to the SPA dev-servers so the ADR-0030 dockerised dev mode can be reached over a hostname registered in Entra. Entra refuses `http:` redirect URIs for anything other than `localhost`, which made the hostname-based access pattern (`apf-portal.dev-jg.local`, `apf-portal.dev.local`, …) — the only stable way to share a VM-based dev stack with another developer — impossible to wire to OIDC. This PR closes that gap without touching the WSL-native + localhost flow.
## What lands
| File | Change |
| --- | --- |
| `apps/portal-shell/project.json`, `apps/portal-admin/project.json` | New `https` Nx serve configuration: inherits the `development` build, sets `ssl: true` + `sslKey: .secrets/dev-tls.key` + `sslCert: .secrets/dev-tls.pem`. `defaultConfiguration` stays `development`; the `https` config is purely opt-in. |
| `infra/local/dev.compose.yml` | The `portal-shell` and `portal-admin` commands now end with `--configuration=${NX_SERVE_CONFIGURATION:-development}`. Compose interpolates the value at YAML parse time from `infra/local/.env`. Default is `development` (no SSL), so behaviour is unchanged for anyone who doesn't opt in. |
| `infra/local/.env.example` | New commented-out `NX_SERVE_CONFIGURATION=https` block with the rationale + pointer to the mkcert setup. |
| `apps/portal-bff/.env.example` | Comment block above the `ENTRA_*_REDIRECT_URI` defaults shows the HTTPS hostname-based override pattern (the four URIs that go with the `apps` profile when accessing via a hostname) and reminds that each override must be registered Entra-side. |
| `infra/README.md` | New "HTTPS dev-server setup — remote-browser access via a hostname" subsection: mkcert install / `mkcert -install`, cert generation, `.secrets/dev-tls.{key,pem}` convention, Entra registration reminder, `NX_SERVE_CONFIGURATION=https` opt-in. Notes that WSL-native is unaffected and that the cert path stays the same when the corp CA eventually replaces mkcert. |
## Design notes
- **Convention `.secrets/dev-tls.{key,pem}` at repo root.** Matches the existing `apps/portal-bff/.secrets/jwks.pem` pattern (gitignored via `*.pem` + `*.key`). Workspace-relative path means project.json can hardcode it and each dev drops their per-host cert there.
- **Hardcoded path, per-dev cert content.** Each developer generates a cert for **their own** hostname; the cert sits at the same fixed path on every machine. Nothing dev-specific in `project.json`.
- **`https` is opt-in, not default.** Native `nx serve` keeps booting on HTTP (`localhost:4200`) without SSL key files, exactly as before. Compose default is also `development` — only setting `NX_SERVE_CONFIGURATION=https` in `infra/local/.env` switches it on, gated by the dev having actually run the mkcert step.
- **BFF stays plain HTTP.** Only the SPA dev-server terminates TLS — the proxy then hits `http://portal-bff:3000` on the internal Compose network. Entra still gets HTTPS at the browser-facing origin, which is what its policy enforces.
## What this PR deliberately does NOT do
- It does **not** force-enable HTTPS. Devs who don't care about hostname access continue working as before.
- It does **not** touch the BFF code, the Entra config helpers, or the auth flow itself. The whole change is config (project.json + compose + env-examples) + docs.
- It does **not** ship the shared VM cert story. That needs a corp-CA-signed cert (or a shared mkcert CA distributed across workstations); flagged in the README section as a follow-up.
## Test plan
- [x] Both `project.json` files parse as JSON; `nx show project` exposes the new `https` configuration with the expected SSL options.
- [x] `docker compose -f dev.compose.yml --profile apps config` validates with `NX_SERVE_CONFIGURATION=https` (resolves to `--configuration=https`) and without it (resolves to `--configuration=development`).
- [ ] **On vm-jg**: `mkcert -install` on the workstation, `mkcert` against `apf-portal.dev-jg.local`, copy `.secrets/dev-tls.{key,pem}` to the VM, set `NX_SERVE_CONFIGURATION=https` in `infra/local/.env`, register the four `https://apf-portal.dev-jg.local:*` URIs in Entra, restart `dev.sh up apps`, then open `https://apf-portal.dev-jg.local:4200/` from the workstation → SPA loads, no cert warning, sign-in completes and returns to the SPA via the OIDC callback.
- [ ] Native WSL flow unchanged: `nx serve portal-shell` still boots on `http://localhost:4200/`, OIDC against the existing `http://localhost:3000/...` Entra URIs still works.
## Related
- [ADR-0030](docs/decisions/0030-dockerised-dev-mode.md) — the dockerised dev mode this completes for the hostname-access case.
- [ADR-0018](docs/decisions/0018-environment-configuration-strategy.md) — per-environment SPA config strategy; `https` is a new Nx serve _configuration_, not a new `environment.ts` sibling, so 0018's build-time replacement story is unchanged.
- [ADR-0009](docs/decisions/0009-auth-flow-oidc-pkce-msal-node.md) — OIDC flow; no behaviour change, only the redirect-URI strings.
---------
Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr>
Reviewed-on: #263
|
||
|
|
a84ea2d116 |
feat(spa): proxy /api in dev-server, relative bffApiBaseUrl (#259)
## Summary Make the SPAs reach the BFF as a **same-origin** call via an Angular dev-server `/api` proxy. Solves the "Backend unreachable" error surfaced during the ADR-0030 dockerised dev mode VM validation when the SPA is accessed from a remote browser (e.g. `http://<vm-ip>:4200/`), and bypasses CORS in dev altogether. Follow-up to the just-merged ADR-0030 implementation. ## Root cause it fixes Before this PR, both SPAs hardcoded `bffApiBaseUrl: 'http://localhost:3000/api'` (per ADR-0018) and the BFF's `CORS_ALLOWED_ORIGINS` only allowed `http://localhost:4200,http://localhost:4300`. Both assumptions hold for native `nx serve` (developer on the same machine as the BFF) but break the moment the browser sits on a different host than the BFF — exactly the case for the ADR-0030 `apps` Compose profile: open `http://<vm-ip>:4200/` from your workstation and the SPA's `localhost:3000` call hits your **workstation's** loopback (nothing there), not the VM's BFF. Even if the URL were right, the origin `http://<vm-ip>:4200` is not in the CORS allowlist. ## Fix Switch to a same-origin dev pattern: the Angular dev-server proxies `/api/*` to the BFF, and `environment.ts` uses a relative `'/api'` URL. | File | Change | | --- | --- | | `apps/portal-shell/proxy.conf.js` | **New.** Maps `/api → ${BFF_TARGET:-http://localhost:3000}` (JS form so the env var can swap the target at startup). | | `apps/portal-admin/proxy.conf.js` | **New.** Same shape. | | `apps/portal-shell/project.json`, `apps/portal-admin/project.json` | `serve.options.proxyConfig` points at the new file. | | `apps/portal-shell/src/environments/environment.ts`, `apps/portal-admin/src/environments/environment.ts` | `bffApiBaseUrl: 'http://localhost:3000/api'` → `'/api'`. The comment block explains the rationale + how production siblings can still use an absolute origin if SPA + BFF live on different hosts. | | `apps/portal-shell/src/observability/tracing.ts` | `new URL(environment.bffApiBaseUrl)` would throw on a relative URL — resolved against `window.location.origin` so both relative (dev) and absolute (prod cross-origin) bases work. | | `infra/local/dev.compose.yml` | `BFF_TARGET=http://portal-bff:3000` added to `portal-shell` and `portal-admin` so the proxy hits the BFF **container** by name (Compose DNS) when the `apps` profile is up. Native `nx serve` leaves the var unset and falls back to `localhost:3000`. | ## Why a relative URL is safe - `${bffApiBaseUrl}/health` etc. compose to `/api/health` — relative paths work in `fetch` / `HttpClient`. - `tracing.ts` propagates `traceparent` on requests whose origin matches the BFF origin. Resolving the relative base against `window.location.origin` gives the current page's origin, which is exactly the origin the dev-server proxy serves from — so the regex still matches the right requests in dev. In a future cross-origin production deployment, `environment.prod.ts` can set an absolute `bffApiBaseUrl`; the URL constructor's second arg is ignored when the first is absolute, so the same code path keeps working. - Auth flow (`feature-auth` / `auth.config.ts`): consumes `bffApiBaseUrl` via DI as a string prefix — agnostic to absolute vs relative. ## Scope notes - The OTel HTTP exporter (`environment.otlpEndpoint = 'http://localhost:4318/v1/traces'`) and the cross-SPA links (`adminAppUrl`, `shellAppUrl`) **remain absolute**. They hit the same remote-browser problem on `apps`-profile access, but neither is blocking the user-visible "Backend unreachable" path this PR targets. Out of scope here; a follow-up could either proxy them too or surface them via runtime config. - This pattern is dev-server only — production builds do not use the proxy. Per-environment `bffApiBaseUrl` overrides remain the supported lever (ADR-0018), unchanged. ## Test plan - [x] `docker compose -f dev.compose.yml --profile apps config` still validates. - [ ] **On the VM**, `./infra/local/dev.sh up apps`, then in the workstation browser open `http://<vm-ip>:4200/` → SPA loads, the "Backend unreachable" message is gone, network tab shows `/api/...` calls succeeding (same-origin, no CORS preflight). - [ ] Native `nx serve portal-shell` still works (the proxy falls back to `localhost:3000`). - [ ] Trace headers (`traceparent`) appear on `/api/*` fetches in the browser network tab. - [ ] `pnpm exec nx affected -t test build` green on the two SPAs. ## Related - [ADR-0030](docs/decisions/0030-dockerised-dev-mode.md) — the dockerised dev mode this enables to actually work from a remote browser. - [ADR-0018](docs/decisions/0018-environment-configuration-strategy.md) — the `environment.ts` per-env strategy this complements (does not supersede — production behaviour unchanged). --------- Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr> Reviewed-on: #259 |
||
|
|
209f44d667 |
feat(portal-admin): audit dashboard — bar / donut / stacked-bar above the table (#172)
## Summary PR 3 (final) of the charts chantier — closes the loop on [ADR-0023](docs/decisions/0023-charts-d3-observable-plot.md) by wiring the three starter components shipped in #171 onto the `/audit` page. ``` PR 1 ✅ — ADR-0023 (decision + a11y contract + bundle plan). PR 2 ✅ — libs/shared/charts/ foundations + bar / donut / stacked-bar. PR 3 (this one) — /audit page integration: three charts above the existing table. ``` ## What lands ### Three computed aggregations on `AuditPage` ```ts dailyVolume() // (day: 'YYYY-MM-DD', count: number)[] outcomeBreakdown() // (outcome: string, count: number)[] dailyByEventType() // (day, eventType, count)[] — flat, the chart pre-pivots totalOnPage() // number for the donut centre label hasChartData() // gate the section out when the page is empty ``` All four derive from the **current page only** (`page()?.items`). No new BFF endpoint — server-side aggregations across the full filter set would need a `/api/admin/audit/stats` resource that's worth its own ADR + chantier when the use case appears. ### Section markup — [`audit.html`](apps/portal-admin/src/app/pages/audit/audit.html) A new `<section class="charts">` between the status bar and the existing table, gated on `hasChartData()`: ``` [At a glance — current page] [charts-note: "Aggregations are computed from the events currently loaded on this page only…"] ┌─ Events per day (bar) ─┬─ Outcome breakdown (donut) ─┐ ├─ Events per day, by event type (stacked bar) — wide ┤ └──────────────────────────────────────────────────────┘ ``` The stacked-bar tile gets `.chart-tile--wide` (spans both grid columns) since its legend benefits from the horizontal space. The two-column grid collapses to one column under 800 px viewports. ### SCSS — [`audit.scss`](apps/portal-admin/src/app/pages/audit/audit.scss) `.charts` shares the same surface tokens as `.filters` + `.table-wrap` (white / gray-800 background, 1 px border, 0.5 rem radius) so the page reads as one stack of related blocks. `.charts-grid` is `display: grid; grid-template-columns: repeat(2, 1fr)` with a `@media (max-width: 800px)` fallback to single-column. ### Project budget — [`apps/portal-admin/project.json`](apps/portal-admin/project.json) `anyComponentStyle` budget bumped from 5/6 KB warn/error to **6/8 KB**. `audit.scss` lands at ~6.5 KB after the charts grid additions, comfortably under the new ceiling. The old 5/6 KB threshold predated the charts row; this is a one-time accommodation, not a global relaxation. ### Tsconfig wiring — [`apps/portal-admin/tsconfig.app.json`](apps/portal-admin/tsconfig.app.json) `nx sync` added the new project reference to `libs/shared/charts/tsconfig.lib.json` after the `import { BarChart, … } from 'shared-charts'` in `audit.ts`. Standard plumbing. ### Spec — [`audit.spec.ts`](apps/portal-admin/src/app/pages/audit/audit.spec.ts) Three new assertions under a `charts` describe block: 1. The three `<lib-*-chart>` elements render when the page has data. 2. `<section class="charts">` is **absent** when the page is empty (no half-rendered donut on `{ total: 0, items: [] }`). 3. The donut's `.donut-center-label` reads the page's item count — verifies the `[centerLabel]` binding wired correctly. ## Notes for the reviewer - **Why charts only on the current page, not on the full result set?** Per the "Don't add features beyond what the task requires" rule from CLAUDE.md. The user's brief was "tester les composants sur la page Audit log", not "design a full analytics dashboard". The `.charts-note` paragraph explicitly says "computed from the events currently loaded on this page only" so the limitation is **disclosed**, not hidden. A future server-side `/api/admin/audit/stats` (with `audit_reader`-scoped queries + caching) is the natural follow-up if real investigative use exposes the per-page scope as a friction point. - **Why `<section>` with its own `<h2>` rather than just inline tiles?** A11y. Per [ADR-0016](docs/decisions/0016-accessibility-baseline-wcag-aa-targeted-aaa.md), document structure is a first-class concern. The charts are a meaningful sub-region of the page; landmark + heading help screen-reader users navigate. - **Why no i18n marks on the captions?** Portal-admin chrome stays in source locale per ADR-0020. The audit page has zero i18n markers today (filter labels, table headers, error messages are all plain English); the chart captions land in the same posture for consistency. If portal-admin grows i18n later, the captions get marked in the same sweep as the rest of the page. - **Bundle impact reality-check**: the chart-bearing lazy `audit` chunk grew from ~4.4 KB gzip to **84 KB gzip** with the chart deps loaded. ADR-0023 estimated ~65 KB; the actual is higher because Plot pulls in more `d3-scale-chromatic` interpolators than I projected. Still well under the 100 KB cap from [ADR-0017](docs/decisions/0017-performance-budgets-lighthouse-ci.md), but worth noting if a fourth chart type with its own d3 submodule lands (line / scatter / heatmap will push it further). - **No anonymous-state regression on the table**: the existing trace-link + actor-pivot behaviours from #163 are unaffected — the charts section sits between the status bar and the table, doesn't share any markup. ## Test plan - [x] `pnpm nx test portal-admin` — **57 specs pass** (was 54; +3 for the new charts assertions). - [x] `pnpm nx run-many -t lint test build --projects=portal-shell,portal-admin,shared-charts` — 9/9 tasks green. - [x] Audit lazy chunk: **84 KB gzip** (was 4.4 KB before the chart deps loaded); under the 100 KB lazy-chunk budget. - [ ] **Manual visual smoke** — sign in to portal-admin with `Portal.Admin` → navigate `/audit`: - The three charts render above the filter form, in a 2-column grid (stacked-bar spanning both). - The donut centre label reads the current page's event count. - Apply a filter that narrows the page to ~3 events → charts re-render with the narrowed dataset. - Click the `<details>` "Data table" disclosure under each chart → the tabular fallback expands with the exact rows. - Toggle dark mode in the footer → axis text + chart envelope swap; the donut's Cividis-equivalent palette kicks in on the categorical chart too (palette already swapped by `resolveTheme()` in the lib). - Filter to an empty result set → the `.charts` section disappears, the "No audit events match the current filters" empty-state stays. ## What's next Chantier closed. Three light follow-ups stay open but optional: - **Server-side aggregations** if the per-page scope becomes a real friction point. New BFF endpoint + likely a fourth `time-range` selector on the SPA. - **More chart types** (line / scatter / heatmap) when business modules ask for them. The lib's `_internal/` carries the a11y plumbing already; each new component is the ~50 LOC Plot wrapper + spec. - **Promote shared chart styling** to a fourth shared concern in `libs/shared/ui/` if a third app ever joins. Not urgent. --------- Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr> Reviewed-on: #172 |
||
|
|
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
|