Files
apf_portal/apps/portal-admin
julien 1160771061
CI / scan (push) Successful in 2m1s
CI / commits (push) Has been skipped
CI / check (push) Successful in 3m40s
CI / a11y (push) Successful in 1m24s
CI / perf (push) Successful in 4m30s
feat(portal-admin): profile page on /profile guarded by authGuard (#150)
## Summary

PR 2 of 3 from the user-menu / profile / cross-app-link chantier. Lands a `/profile` page on `portal-admin` so the Profile entry the user menu wired in PR 1 stops 404-ing. Portal-shell already had a demo `/profile` route that fits this slot — left as-is rather than churned.

| PR | Périmètre |
| --- | --- |
| PR 1  | Shared `UserMenu` dropdown + integration. |
| **PR 2 (this one)** | `/profile` page on `portal-admin`; `CurrentUser.roles` typed (the BFF already returns it). |
| PR 3 | `/api/me/capabilities` + sidebar role widget + cross-app links. |

## What lands

### New page — [`apps/portal-admin/src/app/pages/profile/`](apps/portal-admin/src/app/pages/profile/)

Lazy-loaded at `/profile`, guarded by `authGuard` (from `feature-auth`). Two cards:

- **Identity** — displayName, username, Entra `oid`, tenant `tid`. The two ids are monospaced + break-anywhere so they don't push the layout on narrow viewports.
- **App roles** — chips listing every Entra app-role claim the session carries (`Portal.Admin` for v1, future business roles once ADR-0011 step-up lands its real consumers). Hidden entirely when the `roles` array is empty so anonymous-then-promoted accounts don't see a "no roles" affordance that doesn't help anyone.

The `@if (user())` template guard narrows the type so the page is single-branch — `authGuard` already blocked anonymous traffic upstream.

Lazy chunk lands at **5.37 KB raw / 1.57 KB gzip** — comfortably under the per-chunk lazy budget.

### Route — [`apps/portal-admin/src/app/app.routes.ts`](apps/portal-admin/src/app/app.routes.ts)

```ts
{
  path: 'profile',
  canActivate: [authGuard],
  loadComponent: () => import('./pages/profile/profile').then((m) => m.ProfilePage),
  title: profileTitle,
}
```

`route.profile.title` added to [`messages.fr.xlf`](apps/portal-admin/src/locale/messages.fr.xlf) (FR target retained for parity with the other route titles, even though portal-admin doesn't expose a locale switcher in v1 per ADR-0020).

### Type — [`libs/feature/auth/src/lib/auth.types.ts`](libs/feature/auth/src/lib/auth.types.ts)

```ts
export interface CurrentUser {
  …
  /** Optional; present on `/api/admin/auth/me`, omitted on `/api/auth/me`. */
  readonly roles?: readonly string[];
}
```

`/api/admin/auth/me` already returns `roles` (PR #129), but `CurrentUser` was discarding it through the typed deserialization. Marking the field optional captures the existing BFF response without surfacing the claim on the user portal — ADR-0009's "curated public view" stays intact on `/api/auth/me`, which simply doesn't populate it.

## Notes for the reviewer

- **Why not refresh `portal-shell`'s `/profile` too?** The existing demo page already has the same shape (displayName, username, oid, tid) and is functionally fine. Refreshing for parity would be incidental scope creep against PR 2's goal (unblock the Profile menu entry on the admin surface). PR 3 may revisit when it adds the role display widget.
- **Why no `User profile` entry in the admin sidebar?** The user menu already exposes Profile and ADR-0020's v1 sidebar catalogue is "modules", not "self-service shortcuts". Adding a sidebar entry would duplicate the user-menu access path and break the rule that the sidebar maps to admin business modules.
- **Why a single English-source page, not FR-translated content?** Same posture as the audit + users pages: per ADR-0020 §"No locale switcher in v1", admin chrome stays in source locale. Route titles are i18n-marked because the prod build's `i18nMissingTranslation=error` policy would fail otherwise — the page body text doesn't need to be.
- **Why not show roles on the portal-shell side too?** That's PR 3, gated on the `/api/me/capabilities` design (the user picked the dedicated capabilities endpoint over `roles`-on-user-/me). Adding any role-related rendering here would pre-empt that choice.

## Test plan

- [x] `pnpm nx test portal-admin` — **50 specs pass** (was 46, +4 for the new `ProfilePage` spec).
- [x] `pnpm nx run-many -t lint test build --projects=portal-shell,portal-admin,shared-ui,shared-state,feature-auth` — 15/15 tasks green, including the i18n-strict `portal-shell:build:production`.
- [ ] Manual smoke — sign in on portal-admin, click the avatar → Profile entry, confirm: identity card populated, App roles card shows `Portal.Admin` (if you have the role assigned), Settings entry of the user menu still greyed with the Soon badge.

## What's next

PR 3 lands the cross-app machinery — `GET /api/me/capabilities` endpoint, real role surfacing on the user-portal sidebar widget (`Anonymous` → `Authenticated` / role label, no more hardcode), and the symmetric "Open Portal Admin" / "Open Portal Shell" entries in each app's user menu, gated on the capabilities response.

---------

Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr>
Reviewed-on: #150
2026-05-15 15:42:52 +02:00
..