feat(app): M5.2 — customer-area route shells + role-gated nav
ci / shared (push) Successful in 4s
ci / test (push) Successful in 29s
ci / e2e (push) Has been skipped
ci / image (push) Has been skipped

10 route shells under /[slug]/, role-filtered Nav, backstage stub at /__backstage__, dashboard reads session.products to render tiles. src/lib/session.ts is the canonical role × surface matrix; canSee() is the only RBAC primitive in the portal (real enforcement remains at the API layer). 24 vitest tests; 100% src/lib coverage.

Refs: M5.2
This commit was merged in pull request #7.
This commit is contained in:
2026-05-19 14:47:15 +00:00
parent 2961f36cca
commit fe139332ee
18 changed files with 524 additions and 32 deletions
+40
View File
@@ -0,0 +1,40 @@
import Link from "next/link";
import type { Surface, SessionWithExtras } from "@/lib/session";
import { canSee } from "@/lib/session";
type NavLink = {
href: string;
label: string;
surface: Surface;
};
export function Nav({ slug, session }: { slug: string; session: SessionWithExtras }) {
const links: NavLink[] = [
{ href: `/${slug}/dashboard`, label: "Dashboard", surface: "dashboard" },
{ href: `/${slug}/products`, label: "Products", surface: "products" },
{ href: `/${slug}/catalog`, label: "Catalog", surface: "catalog" },
{ href: `/${slug}/projects`, label: "Projects", surface: "projects" },
{ href: `/${slug}/settings`, label: "Settings", surface: "settings" },
{ href: `/${slug}/settings/users`, label: "Users", surface: "users" },
{ href: `/${slug}/settings/api-keys`, label: "API keys", surface: "api-keys" },
{ href: `/${slug}/settings/integrations`, label: "Integrations", surface: "integrations" },
{ href: `/${slug}/billing`, label: "Billing", surface: "billing" },
{ href: `/${slug}/audit`, label: "Audit log", surface: "audit" },
{ href: `/${slug}/support`, label: "Support", surface: "support" },
];
const visible = links.filter((l) => canSee(session, l.surface));
return (
<nav style={{ width: 220, padding: 16, borderRight: "1px solid #eaeaea", background: "white" }}>
<ul style={{ listStyle: "none", padding: 0, margin: 0 }}>
{visible.map((l) => (
<li key={l.href} style={{ margin: "8px 0" }}>
<Link href={l.href} style={{ color: "#0070f3", textDecoration: "none" }}>
{l.label}
</Link>
</li>
))}
</ul>
</nav>
);
}