feat(l1): L1 workspace Phase 1 — role, seat enforcement, adhoc walker, audit #189

Merged
chihlasm merged 43 commits from feat/l1-workspace into main 2026-05-29 05:18:48 +00:00
2 changed files with 19 additions and 17 deletions
Showing only changes of commit 890cb80bef - Show all commits

View File

@@ -30,6 +30,24 @@ export function ProtectedRoute({ requiredRole, children }: ProtectedRouteProps)
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,
@@ -43,23 +61,6 @@ export function ProtectedRoute({ requiredRole, children }: ProtectedRouteProps)
}
}
// 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}</>
}

View File

@@ -256,6 +256,7 @@ export function Sidebar() {
: 'text-text-rail-label hover:text-foreground'
)}
title={item.label}
aria-label={item.label}
>
<span className="relative">
<Icon size={24} strokeWidth={1.6} className={active ? 'opacity-100' : 'opacity-60 group-hover:opacity-85'} />