feat(app): M5.2 — customer-area route shells + role-gated nav #6

Closed
sharang wants to merge 2 commits from feat/m5.2-shells into main
Owner

What

M5.2 in full: 10 customer-area route shells + a role-gated nav + a backstage stub.

  • Routes (under /[slug]/): products, projects, catalog, settings, settings/users, settings/api-keys, settings/integrations, billing, audit, support. Each renders an empty-state pointing at the milestone that ships its real content. 403 if the user's org_roles don't grant the surface (per the matrix in src/lib/session.ts).
  • Nav component reads session.org_roles and filters links so users only see what they're allowed to access (PLATFORM_ARCHITECTURE.md §5a 'hide what the user can't access').
  • Dashboard now reads session.products and renders one tile per entitlement. Empty-state links into the catalog flow.
  • Backstage stub at /__backstage__ — middleware already rewrites backstage.<apex>/* to this prefix. Real RBAC against BREAKPILOT_ADMIN / SUPPORT_ENGINEER / SALES_REP lands in M13.2.
  • Tenant-slug match guard in [slug]/layout.tsx: a session with tenant_slug=A trying to view /B/... gets redirected to /A/dashboard. Defence-in-depth; the real guard is at the API layer (M4.3).

Why

After M4.1–M4.3 there's a real backend that knows everything about a tenant + their entitlements + their KC user. M5.2 makes the portal actually navigable. Every M10.x / M11.x / M12.x / M14.x milestone will replace one of these shells with real content — by landing the URLs + RBAC gates now, none of those PRs needs to touch routing.

Linked milestone: M5.2

How

  • src/lib/session.ts is the single source of truth for the role → surface matrix. canSee(session, surface) is the only check pages do — never enumerate roles inline. 13 vitest cases assert every role × surface combination.
  • Per-route shells use a shared <ShellEmpty title description milestone /> component so every surface looks identical and the cross-link to the right milestone is consistent.
  • Backstage path is a deliberate ugly /__backstage__/ so it can't be confused with a tenant slug (the host parser already filters it via { kind: 'backstage' }, but the path prefix is the second wall).
  • All four CI gates green locally: pnpm lint + typecheck + test --coverage (100% on src/lib) + build. 24 tests now, was 13.

Test plan

  • pnpm lint clean
  • pnpm typecheck clean
  • pnpm test — 24 tests, 100% src/lib coverage (host + session + tenant-registry)
  • pnpm build — all routes compile in standalone mode
  • Manual: bring up dev stack + tenant-registry, log in as test@breakpilot.dev (IT_ADMIN), see settings/api-keys/billing in nav; log in as a USER, see only dashboard/products/support

Risk

Blast radius: portal-only. Defaults are conservative — shells render "this is M-whatever" placeholders, no destructive surfaces.

What could break:

  • The role matrix is hand-curated. If a future role (e.g., DPO for audit-only) needs a different combination, update src/lib/session.ts + add a test case.
  • canSee() is the only RBAC primitive in the portal. It is not an auth check — it just hides links. The API layer (tenant-registry M4.3+) does the real enforcement. A motivated user pasting URLs gets to the page; without a valid JWT scope the API returns 401/403 and the page renders an error state.

Rollback plan: revert. Existing portal routes (/ apex, /[slug], /[slug]/dashboard) keep working.

Checklist

  • Unit tests added (13 new for session, exhaustive role × surface)
  • Docs updated (CHANGELOG)
  • Secrets via Infisical — n/a (no new secrets)
  • Migration — n/a
  • Tenant scoping — slug-match guard in layout + canSee on every page
  • OpenAPI spec — n/a (consumer)
  • CHANGELOG entry under "Added"
## What M5.2 in full: 10 customer-area route shells + a role-gated nav + a backstage stub. - **Routes** (under `/[slug]/`): products, projects, catalog, settings, settings/users, settings/api-keys, settings/integrations, billing, audit, support. Each renders an empty-state pointing at the milestone that ships its real content. 403 if the user's `org_roles` don't grant the surface (per the matrix in `src/lib/session.ts`). - **Nav** component reads `session.org_roles` and filters links so users only see what they're allowed to access (PLATFORM_ARCHITECTURE.md §5a 'hide what the user can't access'). - **Dashboard** now reads `session.products` and renders one tile per entitlement. Empty-state links into the catalog flow. - **Backstage stub** at `/__backstage__` — middleware already rewrites `backstage.<apex>/*` to this prefix. Real RBAC against BREAKPILOT_ADMIN / SUPPORT_ENGINEER / SALES_REP lands in M13.2. - **Tenant-slug match guard** in `[slug]/layout.tsx`: a session with `tenant_slug=A` trying to view `/B/...` gets redirected to `/A/dashboard`. Defence-in-depth; the real guard is at the API layer (M4.3). ## Why After M4.1–M4.3 there's a real backend that knows everything about a tenant + their entitlements + their KC user. M5.2 makes the portal actually navigable. Every M10.x / M11.x / M12.x / M14.x milestone will replace one of these shells with real content — by landing the URLs + RBAC gates now, none of those PRs needs to touch routing. Linked milestone: **M5.2** ## How - **`src/lib/session.ts`** is the single source of truth for the role → surface matrix. `canSee(session, surface)` is the only check pages do — never enumerate roles inline. 13 vitest cases assert every role × surface combination. - **Per-route shells** use a shared `<ShellEmpty title description milestone />` component so every surface looks identical and the cross-link to the right milestone is consistent. - **Backstage** path is a deliberate ugly `/__backstage__/` so it can't be confused with a tenant slug (the host parser already filters it via `{ kind: 'backstage' }`, but the path prefix is the second wall). - All four CI gates green locally: `pnpm lint` + `typecheck` + `test --coverage` (100% on `src/lib`) + `build`. 24 tests now, was 13. ## Test plan - [x] `pnpm lint` clean - [x] `pnpm typecheck` clean - [x] `pnpm test` — 24 tests, 100% `src/lib` coverage (host + session + tenant-registry) - [x] `pnpm build` — all routes compile in standalone mode - [ ] Manual: bring up dev stack + tenant-registry, log in as test@breakpilot.dev (IT_ADMIN), see settings/api-keys/billing in nav; log in as a USER, see only dashboard/products/support ## Risk **Blast radius:** portal-only. Defaults are conservative — shells render "this is M-whatever" placeholders, no destructive surfaces. **What could break:** - The role matrix is hand-curated. If a future role (e.g., DPO for audit-only) needs a different combination, update `src/lib/session.ts` + add a test case. - `canSee()` is the only RBAC primitive in the portal. **It is not an auth check** — it just hides links. The API layer (tenant-registry M4.3+) does the real enforcement. A motivated user pasting URLs gets to the page; without a valid JWT scope the API returns 401/403 and the page renders an error state. **Rollback plan:** revert. Existing portal routes (`/` apex, `/[slug]`, `/[slug]/dashboard`) keep working. ## Checklist - [x] Unit tests added (13 new for session, exhaustive role × surface) - [x] Docs updated (CHANGELOG) - [x] Secrets via Infisical — n/a (no new secrets) - [ ] Migration — n/a - [x] Tenant scoping — slug-match guard in layout + canSee on every page - [ ] OpenAPI spec — n/a (consumer) - [x] CHANGELOG entry under "Added"
sharang added 1 commit 2026-05-19 11:55:14 +00:00
feat(app): M5.2 — customer-area route shells + role-gated nav
ci / e2e (pull_request) Has been skipped
ci / shared (pull_request) Successful in 4s
ci / test (pull_request) Successful in 26s
ci / image (pull_request) Has been skipped
60209428b5
Adds the M5.2 surface set per PLATFORM_ARCHITECTURE.md §5a. Every route
is a navigable skeleton with a per-route empty-state pointing at the
milestone that ships the real content; the Nav component filters links
by session.org_roles so an IT_ADMIN sees settings + api-keys, a CXO
sees billing, a USER sees only dashboard + products + support, etc.

New surfaces (10):
  /[slug]/products              M10.1
  /[slug]/projects              M10.1
  /[slug]/catalog               M11.1
  /[slug]/settings              M10.1
  /[slug]/settings/users        M10.1
  /[slug]/settings/api-keys     M15.1
  /[slug]/settings/integrations M15.2
  /[slug]/billing               M8.3
  /[slug]/audit                 M10.2
  /[slug]/support               M9.1

Dashboard upgraded: reads session.products, renders one tile per
entitled product (real tile content lands in M10.1). Empty-state when
the user has no entitlements yet — links into the catalog flow.

Backstage stub at /__backstage__ — middleware already rewrites
backstage.<apex>/* to this prefix; real RBAC against BREAKPILOT_ADMIN /
SUPPORT_ENGINEER / SALES_REP lands in M13.2.

Layout enforces tenant-slug match: a session with tenant_slug=A trying
to view /B/... gets redirected to /A/dashboard. Prevents JWT-replay
across tenants (defence in depth; the real guard is at the API layer,
which M4.3 adds in tenant-registry).

src/lib/session.ts is the single source of truth for the role matrix
+ canSee(surface) helper. 13 vitest cases, 100% coverage of src/lib.

Refs: M5.2
CODEOWNERS rules requested review from Benjamin_Boenisch 2026-05-19 11:55:14 +00:00
sharang closed this pull request 2026-05-19 11:58:23 +00:00
sharang reopened this pull request 2026-05-19 11:58:26 +00:00
sharang added 1 commit 2026-05-19 11:59:06 +00:00
chore: trigger ci
ci / test (pull_request) Successful in 27s
ci / e2e (pull_request) Has been skipped
ci / shared (pull_request) Successful in 3s
ci / image (pull_request) Has been skipped
2a12f2f7e4
sharang closed this pull request 2026-05-19 12:02:30 +00:00
All checks were successful
ci / test (pull_request) Successful in 27s
ci / e2e (pull_request) Has been skipped
ci / shared (pull_request) Successful in 3s
Required
Details
ci / image (pull_request) Has been skipped

Pull request closed

Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: platform/portal#6