Completes the arcadia-admin operator surface for the integration registry and the capability/route-guard framework it depends on. - Integration registry: route + Data-group nav entry + `platform.integrations` capability; the in-app client now delegates to the shared `@crema/integration-registry-client` lib (vite alias + tsconfig); the operator Integrations page (committed earlier) is now reachable. - Capability gating: capabilities map + route-guard + jwt helpers + the apps/plan/entitlements routes and supporting tenants/session changes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
51 lines
1.9 KiB
TypeScript
51 lines
1.9 KiB
TypeScript
// Per-route capability guard. Wrap the page body — if the active
|
|
// session doesn't hold the route's capability, render a 403 instead of
|
|
// the page. Server-side authz is still the real gate; this is UX so a
|
|
// deep link doesn't 500 inside a route loader that assumes access.
|
|
|
|
import { useLocation } from "react-router"
|
|
import { ShieldAlert } from "lucide-react"
|
|
|
|
import {
|
|
capabilityForPath,
|
|
useCapabilities,
|
|
type Capability,
|
|
} from "~/lib/capabilities"
|
|
import { Card, CardContent } from "~/components/ui/card"
|
|
|
|
type RouteGuardProps = {
|
|
children: React.ReactNode
|
|
/** Override the capability derived from the current path. Useful for
|
|
* nested routes where you want to check a specific cap. */
|
|
capability?: Capability
|
|
}
|
|
|
|
export function RouteGuard({ children, capability }: RouteGuardProps) {
|
|
const caps = useCapabilities()
|
|
const location = useLocation()
|
|
const required = capability ?? capabilityForPath(location.pathname)
|
|
// No mapping = route is intentionally unguarded (e.g. login flows
|
|
// never reach AppShell anyway).
|
|
if (!required) return <>{children}</>
|
|
if (caps.has(required)) return <>{children}</>
|
|
return <Forbidden capability={required} />
|
|
}
|
|
|
|
function Forbidden({ capability }: { capability: Capability }) {
|
|
return (
|
|
<div className="flex min-h-[60vh] items-center justify-center">
|
|
<Card className="max-w-md">
|
|
<CardContent className="flex flex-col items-center gap-3 py-10 text-center">
|
|
<ShieldAlert className="size-10 text-muted-foreground" />
|
|
<h2 className="text-lg font-semibold">You can't access this page</h2>
|
|
<p className="text-sm text-muted-foreground">
|
|
This view requires the <code className="font-mono text-xs">{capability}</code>{" "}
|
|
capability on your active tenant. If you think you should have it,
|
|
switch tenants from the avatar menu or ask an admin.
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
)
|
|
}
|