import { useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; import { useStaffPermissionServiceContext } from '../services'; import type { StaffPermissionSection } from '../utils/staffAccess'; type Workspace = string; interface UseRouteAuthorizationOptions { // Compared against the caller's ACTIVE WORKSPACE (values are USER_ROLES, e.g. ADMIN / GENERIC / RECEPTIONIST). // Optional when `section` is given. allowedRoles?: Workspace[]; // Gate on a PERMISSION SECTION instead of a fixed workspace list - the same signal, and the same // section/level, that `RequireStaffPermission` uses on the route. Use this wherever the route is // section-gated, so the two gates cannot disagree (see the header comment). section?: StaffPermissionSection; manage?: boolean; redirectPath?: string; enabled?: boolean; } // Gates a page on the caller's ACTIVE WORKSPACE (the workspace switcher's current selection, falling back to // their role when nothing is selected) - the SAME signal the route-level guards use (RequireAdminWorkspace / // RequireStaffPermission, via StaffPermissionService). This previously checked the raw `userRole`, which does // NOT change on a workspace switch, so the two guard systems desynced: a user who switched workspaces got // bounced by these component guards even though the route admitted them (audit #10). Patients / related-persons // have no workspace, so activeWorkspace === userRole for them and their gating is unchanged. // // A SECOND DESYNC OF THE SAME FAMILY - fixed 2026-08-10 ( audit, Spencer's ruling "make it work"). // Aligning the SIGNAL was not enough: a component can gate on a fixed WORKSPACE LIST while its route gates on // a configurable SECTION, and then the two disagree by construction. `locations/:locationId` and // `healthcare-services/:healthcareServiceId` are route-gated on `organization` + manage - which an org may // delegate to ANY workspace - while their components allowed only [ADMIN, GENERIC]. Granting // `organization: manage` to Receptionist / Practitioner / HR / Billing therefore produced a route that // admitted the user and a component that immediately redirected them to '/', with the LIST pages // (`organization: view`) still rendering - so the delegation looked configured and half-worked. // // Hence `section`/`manage`: a section-gated route gets a section-gated component. When BOTH are supplied the // check is an OR (either grants), matching the authorizer's grant model - but prefer `section` alone wherever // the route is section-gated, because a workspace list re-introduces exactly the divergence above. export const useRouteAuthorization = (options: UseRouteAuthorizationOptions): void => { const { allowedRoles, section, manage = false, redirectPath = '/', enabled = true } = options; const navigate = useNavigate(); const { activeWorkspace, canView, canManage } = useStaffPermissionServiceContext(); useEffect(() => { if (!enabled) return; if (!activeWorkspace) return; const byWorkspace = allowedRoles ? allowedRoles.includes(activeWorkspace as string) : false; const bySection = section ? (manage ? canManage(section) : canView(section)) : false; const isAuthorized = byWorkspace || bySection; if (!isAuthorized) { navigate(redirectPath); } }, [activeWorkspace, allowedRoles, section, manage, canView, canManage, redirectPath, navigate, enabled]); };