61 lines
2.3 KiB
TypeScript
61 lines
2.3 KiB
TypeScript
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 <Navigate to={fallback} replace />;
|
|
if (anyOf.length && !hasAnyScope(auth, anyOf)) return <Navigate to={fallback} replace />;
|
|
return <>{children}</>;
|
|
}
|
|
|
|
type ResourceAccessBoundaryProps = {
|
|
probe: () => Promise<unknown>;
|
|
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 <Navigate to={fallback} replace />;
|
|
if (state === "error") return <div className="content-pad"><DismissibleAlert tone="danger" dismissible={false}>{message || "The resource could not be loaded."}</DismissibleAlert></div>;
|
|
if (state === "checking") return <div className="content-pad"><p className="muted">Checking access…</p></div>;
|
|
return <>{children}</>;
|
|
}
|