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:
+33
-18
@@ -1,5 +1,8 @@
|
||||
import { notFound } from "next/navigation";
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
import type { ReactNode } from "react";
|
||||
import { auth } from "@/auth";
|
||||
import { Nav } from "@/components/Nav";
|
||||
import type { SessionWithExtras } from "@/lib/session";
|
||||
import { fetchTenantBySlug } from "@/lib/tenant-registry";
|
||||
|
||||
export default async function TenantLayout({
|
||||
@@ -13,24 +16,36 @@ export default async function TenantLayout({
|
||||
const tenant = await fetchTenantBySlug(slug);
|
||||
if (!tenant) notFound();
|
||||
|
||||
const session = (await auth()) as SessionWithExtras | null;
|
||||
|
||||
// Tenant mismatch guard — a JWT scoped to tenant A must not be allowed
|
||||
// to view tenant B. If the slug in the path doesn't match the session
|
||||
// tenant_slug, redirect back to whatever this user CAN see.
|
||||
if (session && session.tenant_slug && session.tenant_slug !== slug) {
|
||||
redirect(`/${session.tenant_slug}/dashboard`);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<header
|
||||
style={{
|
||||
padding: "12px 24px",
|
||||
borderBottom: "1px solid #eaeaea",
|
||||
background: "white",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
<strong>{tenant.name}</strong>
|
||||
<span style={{ fontSize: 12, color: "#666" }}>
|
||||
{tenant.plan} · {tenant.status}
|
||||
</span>
|
||||
</header>
|
||||
<main style={{ padding: 24 }}>{children}</main>
|
||||
<div style={{ display: "flex", minHeight: "100vh" }}>
|
||||
{session ? <Nav slug={slug} session={session} /> : null}
|
||||
<div style={{ flex: 1 }}>
|
||||
<header
|
||||
style={{
|
||||
padding: "12px 24px",
|
||||
borderBottom: "1px solid #eaeaea",
|
||||
background: "white",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
<strong>{tenant.name}</strong>
|
||||
<span style={{ fontSize: 12, color: "#666" }}>
|
||||
{tenant.plan} · {tenant.status}
|
||||
</span>
|
||||
</header>
|
||||
<main style={{ padding: 24 }}>{children}</main>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user