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>
This commit is contained in:
50
app/components/route-guard.tsx
Normal file
50
app/components/route-guard.tsx
Normal file
@@ -0,0 +1,50 @@
|
||||
// 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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user