feat(app): M5.2 — customer-area route shells + role-gated nav
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:
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
// Reusable empty-state for a customer-area route shell. Every M5.2 route
|
||||
// renders one of these; real content lands in M10.1 / M11.x / M12.x /
|
||||
// M14.x / etc.
|
||||
|
||||
export function ShellEmpty({
|
||||
title,
|
||||
description,
|
||||
milestone,
|
||||
}: {
|
||||
title: string;
|
||||
description: string;
|
||||
milestone: string;
|
||||
}) {
|
||||
return (
|
||||
<section style={{ maxWidth: 720 }}>
|
||||
<h1 style={{ fontSize: 28, marginBottom: 8 }}>{title}</h1>
|
||||
<p style={{ color: "#444", marginBottom: 24 }}>{description}</p>
|
||||
<div
|
||||
style={{
|
||||
padding: 16,
|
||||
border: "1px dashed #ddd",
|
||||
borderRadius: 8,
|
||||
background: "#fafafa",
|
||||
color: "#666",
|
||||
fontSize: 14,
|
||||
}}
|
||||
>
|
||||
This surface is a route shell. Real implementation lands in{" "}
|
||||
<code>{milestone}</code>. See{" "}
|
||||
<a
|
||||
href="https://gitea.meghsakha.com/platform/docs/src/branch/main/PLATFORM_ARCHITECTURE.md"
|
||||
style={{ color: "#0070f3" }}
|
||||
>
|
||||
PLATFORM_ARCHITECTURE.md §5a
|
||||
</a>{" "}
|
||||
for the spec.
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export function NotAuthorized() {
|
||||
return (
|
||||
<section style={{ maxWidth: 720 }}>
|
||||
<h1 style={{ fontSize: 28, marginBottom: 8 }}>403 — Not authorized</h1>
|
||||
<p style={{ color: "#444" }}>
|
||||
This surface requires a role your account doesn't have. If you think
|
||||
that's a mistake, ask an IT_ADMIN on your tenant to invite you with
|
||||
the right role.
|
||||
</p>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user