Files
resolutionflow/frontend/src/components/layout/ProtectedRoute.tsx
Michael Chihlas 890cb80bef
All checks were successful
Mirror to GitHub / mirror (push) Successful in 5s
CI / frontend (pull_request) Successful in 7m2s
CI / e2e (pull_request) Successful in 10m27s
CI / backend (pull_request) Successful in 12m0s
fix(l1): confine L1 techs to their surface + accessible rail nav labels
Two regressions surfaced by running the L1 e2e suite against current main
(which carries PR #174's /home routing migration):

1. L1 post-login redirect keyed off `pathname === '/'`, but the authed index
   moved to /home in #174 — so L1 users landed on the engineer dashboard
   instead of /l1. Replace the ad-hoc '/' and /pilot|/assistant checks with a
   single allowlist: l1_tech users may only reach /l1*, /guides, /account,
   /change-password; everything else (incl. /home, /pilot, /trees/*,
   /escalations) bounces to /l1. Runs before the requiredRole check so L1
   users never trip the engineer-route role logic.

2. Rail nav Links exposed only the truncated shortLabel as their accessible
   name (title= is not an accessible-name source when visible text exists), so
   the "L1 Workspace" coverage-engineer link was unreachable by role+name. Add
   aria-label={item.label} for an accurate accessible name on every rail link.

Fixes all 3 failing cases in e2e/l1-workspace.spec.ts. tsc + eslint clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 01:06:02 -04:00

68 lines
2.3 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 />
}
// L1 techs are confined to their focused surface. The sidebar only exposes
// /l1*, /guides, and /account for them, so any other authed path (the engineer
// dashboard at /home, /pilot, /trees/*, /escalations, …) bounces to /l1. This
// also covers post-login landing: auth sends users to /home, which is not in
// the allowlist, so l1_tech users end up on /l1. Engineer-only AI surfaces
// (/pilot, /assistant) would 403 at POST /api/v1/ai-sessions anyway — this
// turns that backend error into a clean redirect. Runs before the requiredRole
// check so L1 users never trip the engineer-route role logic.
if (effectiveRole === 'l1_tech') {
const L1_ALLOWED_PREFIXES = ['/l1', '/guides', '/account', '/change-password']
const allowed = L1_ALLOWED_PREFIXES.some(
(p) => location.pathname === p || location.pathname.startsWith(p + '/'),
)
if (!allowed) {
return <Navigate to="/l1" 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 />
}
}
return <>{children}</>
}
export default ProtectedRoute