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
71 lines
2.3 KiB
TypeScript
71 lines
2.3 KiB
TypeScript
// Session-derived types & helpers — keep all session-shape knowledge in
|
|
// one place so route components don't all repeat the same casts.
|
|
//
|
|
// The breakpilot-dev realm projects these claims into every JWT via
|
|
// protocol mappers (see platform/orca-platform/dev/keycloak/realm-export.json).
|
|
// Auth.js v5 callbacks copy them onto the session in src/auth.ts.
|
|
|
|
import type { Session } from "next-auth";
|
|
|
|
export type OrgRole = "IT_ADMIN" | "CXO" | "FINANCE" | "LEGAL" | "USER";
|
|
export type TenantStatus = "demo" | "trial" | "active" | "frozen" | "archived";
|
|
export type Plan = "starter" | "professional" | "enterprise";
|
|
|
|
export type SessionExtras = {
|
|
tenant_id?: string;
|
|
tenant_slug?: string;
|
|
org_roles?: OrgRole[];
|
|
products?: string[];
|
|
plan?: Plan;
|
|
tenant_status?: TenantStatus;
|
|
};
|
|
|
|
export type SessionWithExtras = Session & SessionExtras;
|
|
|
|
export function hasOrgRole(s: SessionWithExtras | null, role: OrgRole): boolean {
|
|
return !!s?.org_roles?.includes(role);
|
|
}
|
|
|
|
export function hasAnyOrgRole(s: SessionWithExtras | null, roles: OrgRole[]): boolean {
|
|
if (!s?.org_roles) return false;
|
|
return roles.some((r) => s.org_roles?.includes(r));
|
|
}
|
|
|
|
export function hasProduct(s: SessionWithExtras | null, product: string): boolean {
|
|
return !!s?.products?.includes(product);
|
|
}
|
|
|
|
// Permission matrix per PLATFORM_ARCHITECTURE.md §5a "Operating principles":
|
|
// hide what the user can't access. Each portal surface declares which
|
|
// org_roles can see it; the nav uses this to filter links.
|
|
export type Surface =
|
|
| "dashboard"
|
|
| "products"
|
|
| "projects"
|
|
| "settings"
|
|
| "users"
|
|
| "api-keys"
|
|
| "integrations"
|
|
| "billing"
|
|
| "audit"
|
|
| "support"
|
|
| "catalog";
|
|
|
|
export const surfaceRoles: Record<Surface, OrgRole[]> = {
|
|
dashboard: ["IT_ADMIN", "CXO", "FINANCE", "LEGAL", "USER"],
|
|
products: ["IT_ADMIN", "CXO", "FINANCE", "LEGAL", "USER"],
|
|
projects: ["IT_ADMIN", "CXO"],
|
|
settings: ["IT_ADMIN"],
|
|
users: ["IT_ADMIN"],
|
|
"api-keys": ["IT_ADMIN"],
|
|
integrations: ["IT_ADMIN"],
|
|
billing: ["IT_ADMIN", "CXO", "FINANCE"],
|
|
audit: ["IT_ADMIN", "CXO", "LEGAL"],
|
|
support: ["IT_ADMIN", "CXO", "FINANCE", "LEGAL", "USER"],
|
|
catalog: ["IT_ADMIN", "CXO"],
|
|
};
|
|
|
|
export function canSee(s: SessionWithExtras | null, surface: Surface): boolean {
|
|
return hasAnyOrgRole(s, surfaceRoles[surface]);
|
|
}
|