import { useEffect, useState, type ReactNode } from "react"; import { Navigate } from "react-router-dom"; import type { AuthInfo } from "../types"; import { isApiError } from "../api/client"; import DismissibleAlert from "./DismissibleAlert"; import { hasAnyScope, hasScope } from "../utils/permissions"; type PermissionBoundaryProps = { auth: AuthInfo; anyOf?: string[]; allOf?: string[]; fallback: string; children: ReactNode; }; export function PermissionBoundary({ auth, anyOf = [], allOf = [], fallback, children }: PermissionBoundaryProps) { if (allOf.length && !allOf.every((scope) => hasScope(auth, scope))) return ; if (anyOf.length && !hasAnyScope(auth, anyOf)) return ; return <>{children}; } type ResourceAccessBoundaryProps = { probe: () => Promise; resetKey: string; fallback: string; children: ReactNode; }; /** * Keeps resource routes tenant-agnostic. After a tenant switch the current URL * is probed in the new tenant context; inaccessible or missing resources fall * back to their parent collection instead of requiring switch-specific routing. */ export function ResourceAccessBoundary({ probe, resetKey, fallback, children }: ResourceAccessBoundaryProps) { const [state, setState] = useState<"checking" | "allowed" | "denied" | "error">("checking"); const [message, setMessage] = useState(""); useEffect(() => { let cancelled = false; setState("checking"); setMessage(""); probe() .then(() => { if (!cancelled) setState("allowed"); }) .catch((error) => { if (cancelled) return; if (isApiError(error, 403, 404)) { setState("denied"); return; } setMessage(error instanceof Error ? error.message : String(error)); setState("error"); }); return () => { cancelled = true; }; }, [probe, resetKey]); if (state === "denied") return ; if (state === "error") return
{message || "The resource could not be loaded."}
; if (state === "checking") return

Checking access…

; return <>{children}; }