6695909db2be86f8b4dea8351863e2cca44264c4
5 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
6120471b66 |
chore(workspace): tsconfig composite refs + axe-linter false-positive config (#147)
## Summary
Three editor-noise sources flagged by the VS Code TypeScript service + the Deque axe Linter extension, each tamed at the right layer. No runtime behaviour change.
| Source | Fix |
| --- | --- |
| **TS6306** — Referenced project `libs/{shared/ui,shared/state,feature/auth}` must have `composite: true`. Nx 22's lib generator doesn't emit `composite`; modern VS Code TS service flags it. | Add `composite: true` to each lib's `tsconfig.lib.json`, let `nx sync` redirect consumer references in `apps/portal-{shell,admin}/tsconfig.app.json` to point at the `.lib.json` directly. |
| **TS6504** — `moduleResolution: "node"` / `"node10"` deprecated, removed in TS 7.0. Two hits on the BFF tsconfigs. | Add `ignoreDeprecations: "5.0"` on `apps/portal-bff/tsconfig.{app,spec}.json` — the opt-out knob the diagnostic itself suggests. A proper migration to `nodenext`/`node16` is a separate chantier. |
| **axe-core/list (WCAG 1.3.1)** — `<ul>` "must only directly contain `<li>`, `<script>`, or `<template>`" — fires on Angular 17+ `@for` blocks inside lists. Pure static-linter limitation; rendered DOM is fine. | New `.axe-linter.yml` at repo root: `global-disable: [list]`. |
## What lands
### `composite: true` on lib `.lib.json`
[`libs/shared/ui/tsconfig.lib.json`](libs/shared/ui/tsconfig.lib.json), [`libs/shared/state/tsconfig.lib.json`](libs/shared/state/tsconfig.lib.json), [`libs/feature/auth/tsconfig.lib.json`](libs/feature/auth/tsconfig.lib.json) get `composite: true` added. `nx sync` then automatically rewrites consumer references:
```diff
- "path": "../../libs/shared/ui"
+ "path": "../../libs/shared/ui/tsconfig.lib.json"
```
in [`apps/portal-shell/tsconfig.app.json`](apps/portal-shell/tsconfig.app.json) and [`apps/portal-admin/tsconfig.app.json`](apps/portal-admin/tsconfig.app.json). Semantically cleaner — the app references the lib's actual compile config (which produces the `.d.ts` it consumes), not the lib's solution-style root tsconfig.
**Earlier attempt — composite on the solution `tsconfig.json` — silently broke `vitest`**: the Angular Vite plugin chokes on a composite project with `files: []` / `include: []` and falls through, leaving spec files loaded but tests not registered (`"No test suite found in file"`). Moving `composite` to `.lib.json` (the project that actually has inputs) fixes the contract without poking the plugin.
### `ignoreDeprecations: "5.0"`
[`apps/portal-bff/tsconfig.app.json`](apps/portal-bff/tsconfig.app.json) and [`apps/portal-bff/tsconfig.spec.json`](apps/portal-bff/tsconfig.spec.json) — silences `Option 'moduleResolution=node10' is deprecated and will stop functioning in TypeScript 7.0`. The diagnostic suggests `"6.0"` as the value, but TS 5.9 (our pinned version) only accepts `"5.0"`; using `"6.0"` results in `TS5103: Invalid value for '--ignoreDeprecations'` and breaks every spec. `"5.0"` is the current-gen accepted value.
The deprecation is real — TS 7.0 will drop both `"node"` and `"node10"` `moduleResolution` modes. The migration target is `moduleResolution: "nodenext"` paired with matching `module: "nodenext"`, but that interacts non-trivially with Nest's CommonJS pipeline and the BFF's import semantics. Out of scope for a drive-by fix; we'll handle it as a dedicated chantier when TS 7.0 lands on the roadmap.
### `.axe-linter.yml`
New file at repo root:
```yaml
global-disable:
- list
```
The Deque axe Linter VS Code extension reads `.axe-linter.yml` at workspace root. The `list` rule (WCAG 1.3.1) fires false positives on Angular 17+ control-flow syntax — `@for (item of list; ...) { <li>… }` looks like a non-`<li>` child of `<ul>` to a static HTML scanner. The Angular compiler erases those tokens at build time; the rendered DOM is compliant. CI accessibility coverage is provided by `axe-playwright` per [ADR-0016 §"Tooling"](docs/decisions/0016-accessibility-baseline-wcag-aa-targeted-aaa.md) — it runs against the rendered DOM and is unaffected by this disable.
## Notes for the reviewer
- **Why not `composite: true` on every lib?** Per CLAUDE.md "no premature abstractions" — `libs/shared/{tokens,util}` are not currently referenced by any `tsconfig.app.json`, so they don't trigger TS6306. Adding `composite` to them would be future-proofing without a current consumer. When a consumer reference is added, the same one-line fix lands then.
- **Why not migrate `moduleResolution` properly?** The BFF runs on Nest's CommonJS pipeline; `nodenext` brings stricter ESM resolution (`.js` extensions in imports, package `exports` map enforcement) that ripples through. Not a 5-minute change. The `ignoreDeprecations` knob is the textbook defer mechanism for exactly this case.
- **Why disable `list` globally rather than per-file?** The rule's false-positive pattern (`<ul><@for>` / `<ol><@for>`) applies workspace-wide; we use `@for` consistently across `portal-shell` + `portal-admin`. Per-file disables would multiply as new templates land. axe-playwright remains the authoritative check on the rule.
## Test plan
- [x] `pnpm nx run-many -t lint test build --projects=portal-shell,portal-admin,portal-bff,shared-ui,shared-state,feature-auth` — 18/18 tasks pass. **517 specs green** across the affected projects.
- [x] `pnpm nx sync:check` — workspace in sync after the changes; running `sync` again is a no-op.
- [ ] Editor smoke — reopen the workspace in VS Code: the TS6306 errors on lib `tsconfig.json` files should be gone, the two `moduleResolution=node10` deprecation lines on BFF tsconfigs should be silenced, and the `list` rule under `sidebar.html` (`portal-admin`) should no longer surface.
---------
Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr>
Reviewed-on: #147
|
||
|
|
9a9faf9a31 |
feat(portal-shell): wire SPA auth state to BFF /me + header login/logout widget (#113)
## Summary First user-visible piece of the auth track. The portal-shell SPA now consumes the BFF auth surface (`/api/auth/me`, `/api/auth/login`, `/api/auth/logout`) and the header reflects sign-in state. - `libs/feature/auth` ships an `AuthService` that fetches `/auth/me` on first injection, holds a signal-backed `AuthState` (`loading` / `anonymous` / `authenticated` / `error`) and exposes `currentUser` + `isLoading` computed signals plus `login()` / `logout()` / `refresh()` methods. - The header's right-side widget renders four states: a sign-in button when anonymous, the user avatar (initials, `JD` for "Jane Doe") + display name + sign-out button when authenticated, a loading dot before `/me` resolves, and a "Can't reach the server" chip on non-401 failures. - `login()` / `logout()` go through an injected `AUTH_NAVIGATOR` token whose default calls `window.location.assign(url)`. Specs override it with `vi.fn()` — no `window.location` mocking required. ## Notable choices **Auto-bootstrap on construction, not via `provideAppInitializer`.** The service fires `/me` from its constructor (unawaited) so consuming components transition through the explicit `loading` state. Blocking app boot on the round-trip would push the first paint behind the network call — bad for TTFB, especially on slow links. The header handles `loading` as a first-class state. **Discriminated `AuthState` over flat fields.** A single source of truth (`state()`) with four `kind`s lets templates `switch` and narrow automatically. `currentUser` and `isLoading` are computed conveniences but never out of sync with `state`. **`AUTH_BFF_BASE_URL` + `AUTH_NAVIGATOR` injection tokens.** Decouples the lib from the host's `environment.ts` shape and keeps tests free of `window.location` redefinition (which jsdom resists across multiple specs in the same file — first redefine works, second throws "Cannot redefine property"). The host wires both in `app.config.ts`. **Curated public user type.** `CurrentUser` mirrors the BFF's `/me` response (`oid`, `tid`, `username`, `displayName`) — no `amr`, no internal claims. The shape lives in `auth.types.ts` so feature code can import it without depending on Angular HTTP details. **Distinct `error` state.** A network failure / 5xx surfaces a different UI than "not signed in" — "Can't reach the server" chip vs. sign-in button. Avoids the trap of treating any `/me` failure as "anonymous". ## Out of scope (next PRs) - Route guards (protecting routes from anonymous users). For now the header is the only consumer. - Auto-refresh of the session before idle timeout. - HTTP interceptor that redirects to `/auth/login` on a 401 from any other BFF call. - Per-locale styling polish on the new header strings. ## Test plan - [x] `pnpm nx test feature-auth` → **8/8 pass**. - [x] `pnpm nx test portal-shell` → **32/32 pass** (was 27 before). - [x] `pnpm nx lint portal-shell feature-auth` → clean. - [x] `pnpm nx build portal-shell` → main bundle 488 kB raw / 129.94 kB transfer; well under the 300 kB gzip budget from ADR-0017. - [ ] Manual smoke once the BFF is up: - [ ] Anonymous landing → header shows "Sign in". - [ ] Click "Sign in" → BFF /login → Entra → callback → SPA lands with avatar + display name in the header. - [ ] Click "Sign out" → BFF /logout → Entra logout → back at SPA, header back to "Sign in". --------- Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr> Reviewed-on: #113 |
||
|
|
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
|
||
|
|
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
|
||
|
|
bea5e1954f |
chore: generate portal-shell and portal-bff apps per ADR-0004 / ADR-0005
Add the @nx/angular, @nx/nest, @nx/vite, @nx/eslint plugins, then generate the two apps. Adjust the empty-template tsconfig.base.json to be Angular-compatible (drop project references and customConditions that the empty-template defaults to but Angular doesn't support; keep the strict-TS extensions from ADR-0004). apps/portal-shell (Angular 21): - standalone APIs, routing, SCSS, esbuild - vitest-angular as unitTestRunner, playwright for e2e - strict mode - tags scope:portal-shell, type:app - app.config.ts wired with provideZonelessChangeDetection() per ADR-0004 (Angular 21 + Nx 22 generates without zone.js by default) apps/portal-bff (NestJS 11): - Express adapter (default per ADR-0005) - Jest as unitTestRunner - tags scope:portal-bff, type:app - main.ts wired with a global ValidationPipe configured whitelist + forbidNonWhitelisted + transform per ADR-0005 - Phase-2 security additions (helmet, CORS, sessions, CSRF, rate limit, auth guards, error filter) deferred to their respective ADRs - placeholder comment in main.ts Workspace dependencies: class-validator + class-transformer added (required by NestJS ValidationPipe at runtime). Nx-generated .gitignore additions (.angular, __screenshots__) merged into ours. .vscode/extensions.json and launch.json added by Nx are kept (do not override our existing settings.json). |