67 lines
2.1 KiB
TypeScript
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
|