feat(portal-admin): profile page on /profile guarded by authGuard #150

Merged
julien merged 1 commits from feat/portal-admin-profile-page into main 2026-05-15 15:42:53 +02:00
Owner

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/

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

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

route.profile.title added to 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

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

  • pnpm nx test portal-admin50 specs pass (was 46, +4 for the new ProfilePage spec).
  • 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 (AnonymousAuthenticated / 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.

## 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.
julien added 1 commit 2026-05-15 15:42:38 +02:00
feat(portal-admin): profile page on /profile guarded by authGuard
CI / scan (pull_request) Successful in 2m38s
CI / commits (pull_request) Successful in 2m47s
CI / check (pull_request) Successful in 4m31s
CI / a11y (pull_request) Successful in 1m51s
CI / perf (pull_request) Successful in 5m35s
3b3cd1dc66
PR 2 of 3 from the user-menu chantier.

* New lazy-loaded `/profile` route in `apps/portal-admin`. Renders
  the active admin session's identity card (displayName, username,
  oid, tid) plus an "App roles" card listing the Entra app-role
  claims attached to the session — `Portal.Admin` today, additional
  business roles when the strategic security baseline ADR lands.
* `authGuard` (from `feature-auth`) gates the route — anonymous
  visitors bounce through the BFF's `/api/admin/auth/login`. The
  template's `@if (user())` then narrows the type so the page can
  drop the anonymous branch.
* The `User Menu`'s Profile entry (PR 1) already points at `/profile`
  so the link starts working on this surface as soon as this lands.
* `CurrentUser.roles` extended with an optional `readonly string[]`
  — the admin-side `/api/admin/auth/me` already returns the claim;
  the type now captures it. User-portal `/me` keeps omitting it per
  ADR-0009 (PR 3 surfaces it via the dedicated capabilities endpoint).
* Lazy chunk lands at 5.37 KB raw / 1.57 KB gzip — comfortably under
  the lazy-chunk budget.
julien merged commit 1160771061 into main 2026-05-15 15:42:53 +02:00
julien deleted branch feat/portal-admin-profile-page 2026-05-15 15:42:55 +02:00
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: julien/apf_portal#150