Files
resolutionflow/frontend/src/components/layout/ProtectedRoute.tsx
Michael Chihlas 4c83cebfca
Some checks failed
Mirror to GitHub / mirror (push) Successful in 4s
CI / frontend (pull_request) Failing after 1m52s
CI / e2e (pull_request) Failing after 6m6s
CI / backend (pull_request) Successful in 12m15s
Merge branch 'main' into feat/l1-workspace
# Conflicts:
#	frontend/src/router.tsx
2026-05-29 00:24:54 -04:00

67 lines
2.1 KiB
TypeScript

import { Navigate, useLocation } from 'react-router-dom'
import { useAuthStore } from '@/store/authStore'
import { usePermissions, type EffectiveRole } from '@/hooks/usePermissions'
import { Spinner } from '@/components/common/Spinner'
interface ProtectedRouteProps {
requiredRole?: EffectiveRole
children: React.ReactNode
}
export function ProtectedRoute({ requiredRole, children }: ProtectedRouteProps) {
const { isAuthenticated, isLoading, user } = useAuthStore()
const location = useLocation()
const { effectiveRole } = usePermissions()
if (isLoading) {
return (
<div className="flex h-screen items-center justify-center">
<Spinner className="border-t-foreground" />
</div>
)
}
if (!isAuthenticated) {
return <Navigate to="/" state={{ from: location }} replace />
}
// Enforce must_change_password — redirect unless already on /change-password
if (user?.must_change_password && location.pathname !== '/change-password') {
return <Navigate to="/change-password" replace />
}
if (requiredRole) {
const ROLE_HIERARCHY: Record<EffectiveRole, number> = {
super_admin: 5,
owner: 4,
engineer: 3,
l1_tech: 2,
viewer: 1,
}
if (ROLE_HIERARCHY[effectiveRole] < ROLE_HIERARCHY[requiredRole]) {
return <Navigate to="/trees" replace />
}
}
// L1 users landing on / (e.g. post-login) get redirected to their workspace.
// Does not fire when already on /l1 or any other path, preventing loops.
if (effectiveRole === 'l1_tech' && location.pathname === '/') {
return <Navigate to="/l1" replace />
}
// L1 users hitting engineer-only AI surfaces (Pilot / Assistant) get pushed
// back to /l1 — POST /api/v1/ai-sessions rejects them with 403 anyway, so
// this just turns a backend error into a clean route-level redirect.
if (
effectiveRole === 'l1_tech' &&
(location.pathname.startsWith('/pilot') ||
location.pathname.startsWith('/assistant'))
) {
return <Navigate to="/l1" replace />
}
return <>{children}</>
}
export default ProtectedRoute