Files
arcadia-admin/app/components/route-guard.tsx
jules 4b817b85ff Wire operator Integrations page + capability-gating framework
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>
2026-06-09 23:09:24 +10:00

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>
)
}