UNPKG

@kilpi/react-client

Version:

Kilpi React Client Components · Kilpi is the authorization framework for full-stack TypeScript applications, designed for flexible, powerful, agnostic, intuitive and developer friendly authorization.

195 lines (179 loc) 5.92 kB
import { type AnyKilpiClient, type KilpiClientPolicy } from "@kilpi/client"; import type { Decision, DeniedDecision, GrantedDecision, PolicysetActions } from "@kilpi/core"; import { useEffect, useState } from "react"; import { useUnmountAbortSignalRef } from "./useUnmountAbortSignalRef"; /** * Input type */ export type UseAuthorizeOptions = { isDisabled?: boolean; }; /** * UseAuthorizeStatus */ export type UseAuthorizeStatus = "idle" | "pending" | "error" | "success"; /** * Sub-utility type for just the status and flags */ type UseAuthorizeStatusWithFlags<TStatus extends UseAuthorizeStatus> = { status: TStatus; isIdle: TStatus extends "idle" ? true : false; isError: TStatus extends "error" ? true : false; isSuccess: TStatus extends "success" ? true : false; isPending: TStatus extends "pending" ? true : false; }; /** * Utility type to get status with flags to an object */ type UseAuthorizeReturnForStatus< TStatus extends UseAuthorizeStatus, // eslint-disable-next-line @typescript-eslint/no-explicit-any TDecision extends null | Decision<any>, > = UseAuthorizeStatusWithFlags<TStatus> & { error: TStatus extends "error" ? unknown : null; isDisabled: boolean; decision: TDecision; granted: TDecision extends { granted: true } ? true : false; }; /** * Discriminated return type by status. Implemented this way to ensure typescript * is better able to deduce the types when narrowing or using `Extract<...>`. Two * separate discriminators for `status = success` based on `granted` flag. */ export type UseAuthorizeReturn< TClient extends AnyKilpiClient, // @ts-expect-error Unused variable for now // eslint-disable-next-line @typescript-eslint/no-unused-vars TAction extends PolicysetActions<TClient["$$infer"]["policies"]>, > = | UseAuthorizeReturnForStatus<"idle", null> | UseAuthorizeReturnForStatus<"pending", null> | UseAuthorizeReturnForStatus<"error", null> | UseAuthorizeReturnForStatus<"success", GrantedDecision<TClient["$$infer"]["subject"]>> | UseAuthorizeReturnForStatus<"success", DeniedDecision>; /** * Factory for useAuthorize hook given a policy */ export function create_useAuthorize< TClient extends AnyKilpiClient, TAction extends PolicysetActions<TClient["$$infer"]["policies"]>, >(Client: TClient, policy: KilpiClientPolicy<TClient, TAction>) { void Client; // Unused, but needed to keep generic type /** * Utility to return status with flags */ function getStatusWithFlags<TStatus extends UseAuthorizeStatus>(status: TStatus) { return { status, isIdle: status === "idle", isError: status === "error", isSuccess: status === "success", isPending: status === "pending", } as UseAuthorizeStatusWithFlags<TStatus>; } /** * Construct the useAuthorize hook. */ return function useAuthorize( options: UseAuthorizeOptions = {}, ): UseAuthorizeReturn<TClient, TAction> { // Destructure options with defaults const isDisabled = options.isDisabled || false; // States initialized with idle state const [status, setStatus] = useState<"idle" | "pending" | "error" | "success">("idle"); const [error, setError] = useState<unknown>(null); const [decision, setDecision] = useState<Decision<TClient["$$infer"]["subject"]> | null>(null); // Get abort signal that aborts on unmount const signalRef = useUnmountAbortSignalRef(); // Reset state on cache invalidate, automatically triggers re-fetch (unless disabled). Supports // fine-grained invalidation by comparing cache keys. useEffect(() => { return Client.$hooks.onCacheInvalidate(({ matches }) => { if (matches(policy)) { setStatus("idle"); setError(null); setDecision(null); } }); }, []); // Run fetch useEffect(() => { // If disabled, reset to initial idle state if (isDisabled) { setStatus("idle"); setError(null); setDecision(null); return; } // If not idle, already fetching / fetched if (status !== "idle") { return; } // Fetch and go to pending setStatus("pending"); policy // Fetch decision .authorize({ signal: signalRef.current }) // Handle successs .then((value) => { if (signalRef.current.aborted) return; setDecision(value); setStatus("success"); }) // Handle error .catch((err) => { if (signalRef.current.aborted) return; setError(err); setStatus("error"); }); }, [isDisabled, status, signalRef]); /** * Return by status to ensure type safety */ switch (status) { case "idle": return { ...getStatusWithFlags(status), decision: null, granted: false, error: null, isDisabled, }; case "pending": return { ...getStatusWithFlags(status), decision: null, granted: false, error: null, isDisabled, }; case "error": return { ...getStatusWithFlags(status), decision: null, granted: false, error, isDisabled, }; case "success": if (decision?.granted) { return { ...getStatusWithFlags(status), decision: decision as GrantedDecision<TClient["$$infer"]["subject"]>, granted: true, error: null, isDisabled, }; } else { return { ...getStatusWithFlags(status), // `decision` should never be null here, but we need to satisfy TypeScript decision: decision || { granted: false }, granted: false, error: null, isDisabled, }; } } }; }