'use client' import { usePathname, useRouter } from 'next/navigation' import { useEffect, useState } from 'react' import { apiFetch } from '@/lib/api' import { buildHomepageSignInPath, toDashboardAppPath, toPublicDashboardPath } from '@/lib/dashboardPaths' import { getDashboardFallbackRoute, resolveDashboardRoutePolicy, roleCanAccessPolicy, } from '@/lib/dashboardRoutePolicies' type GeneratedMenuItem = { id: string itemType: 'INTERNAL_PAGE' | 'EXTERNAL_LINK' | 'PARENT_MENU' | 'SECTION_LABEL' | 'DIVIDER' routeOrUrl: string | null children: GeneratedMenuItem[] } type EmployeeMenuResponse = { items: GeneratedMenuItem[] subscriptionAccessLevel?: 'full' | 'limited' | 'read_only' | 'none' } type EmployeeProfileResponse = { employee: { role: string } } const ROLE_RANK: Record = { OWNER: 3, MANAGER: 2, AGENT: 1 } const BASELINE_MENU_ROUTES = [ { route: '/', minRole: 'AGENT' }, { route: '/reservations', minRole: 'AGENT' }, { route: '/contracts', minRole: 'AGENT' }, { route: '/fleet', minRole: 'AGENT' }, { route: '/customers', minRole: 'AGENT' }, { route: '/reports', minRole: 'MANAGER' }, { route: '/billing', minRole: 'MANAGER' }, { route: '/settings', minRole: 'OWNER' }, ] as const const MENU_RECOVERY_ROUTES = new Set(['/subscription', '/subscription/success', '/subscription/cancel']) function hasMinRole(employeeRole: string, minRole: string): boolean { return (ROLE_RANK[employeeRole] ?? 0) >= (ROLE_RANK[minRole] ?? 0) } export function getBaselineInternalRoutes(role: string): string[] { return BASELINE_MENU_ROUTES .filter((item) => hasMinRole(role, item.minRole)) .map((item) => item.route) } export function flattenInternalRoutes(items: GeneratedMenuItem[]): string[] { const routes: string[] = [] const walk = (entry: GeneratedMenuItem) => { if (entry.itemType === 'INTERNAL_PAGE' && entry.routeOrUrl) { routes.push(toDashboardAppPath(entry.routeOrUrl)) } entry.children.forEach(walk) } items.forEach(walk) return Array.from(new Set(routes)) } export function resolveAllowedRoutes(menu: EmployeeMenuResponse, role: string): string[] { const routes = flattenInternalRoutes(menu.items) const featureRoutes = routes.filter((route) => route !== '/' && !MENU_RECOVERY_ROUTES.has(route)) const shouldUseBaselineFallback = menu.subscriptionAccessLevel !== 'none' && featureRoutes.length === 0 const resolvedRoutes = shouldUseBaselineFallback ? getBaselineInternalRoutes(role) : routes if (resolvedRoutes.includes('/reservations') && !resolvedRoutes.includes('/contracts')) { return [...resolvedRoutes, '/contracts'] } return resolvedRoutes } export function isAllowedRoute(currentPath: string, allowedRoutes: string[]) { return allowedRoutes.some((route) => { if (route === '/') return currentPath === '/' return currentPath === route || currentPath.startsWith(`${route}/`) }) } export function resolveAccessRedirect(currentPath: string, allowedRoutes: string[]) { if (allowedRoutes.length === 0) return null if (isAllowedRoute(currentPath, allowedRoutes)) return null const fallbackRoute = allowedRoutes[0] ?? '/' return fallbackRoute === currentPath ? null : fallbackRoute } export function buildSignInRedirect(currentPath: string) { return buildHomepageSignInPath(currentPath) } export default function DashboardAccessGuard({ children }: { children: React.ReactNode }) { const pathname = usePathname() const router = useRouter() const [ready, setReady] = useState(false) const [error, setError] = useState(null) useEffect(() => { let cancelled = false async function enforceAccess() { const currentPath = toDashboardAppPath(pathname) const policy = resolveDashboardRoutePolicy(currentPath) try { setReady(false) setError(null) const { employee } = await apiFetch('/auth/employee/me') if (cancelled) return if (!roleCanAccessPolicy(employee.role, policy)) { const fallbackRoute = getDashboardFallbackRoute(employee.role) if (fallbackRoute !== currentPath) { router.replace(fallbackRoute) return } } if (policy.menuRegistrationRequired) { const menu = await apiFetch('/auth/employee/menu') if (cancelled) return if (menu.subscriptionAccessLevel === 'none') { router.replace('/subscription') return } const allowedRoutes = resolveAllowedRoutes(menu, employee.role) const redirectPath = resolveAccessRedirect(currentPath, allowedRoutes) if (redirectPath) { router.replace(redirectPath) return } } setReady(true) } catch (error: any) { if (cancelled) return if (error?.statusCode === 401) { const target = buildSignInRedirect(currentPath) if (target !== toPublicDashboardPath(currentPath)) window.location.replace(target) return } if (error?.statusCode === 402) { if (policy.billingRecoveryRoute) setReady(true) else router.replace('/subscription') return } if (error?.statusCode === 403) { if (currentPath !== '/') router.replace('/') else setReady(true) return } setError(error?.message ?? 'Unable to verify dashboard access. Please try again.') } } enforceAccess() return () => { cancelled = true } }, [pathname, router]) if (error) { return (
{error}
) } if (!ready) { return (
) } return <>{children} }