Skip to content

Routing

The Cloud Frontend uses React Router 6 with nested routes. The root route tree is defined in Frontend/src/App.tsx.

ComponentFilePurpose
ProtectedRoutesrc/routes/ProtectedRoute.tsxRequires authenticated user. Waits for Zustand persistence hydration before redirecting.
PublicOnlyRoutesrc/routes/PublicOnlyRoute.tsxRedirects authenticated users away from login/register.
PlatformAdminRoutesrc/routes/PlatformAdminRoute.tsxRequires user email domain to match VITE_PLATFORM_ADMIN_EMAIL_DOMAIN.
PermissionGuardsrc/routes/PermissionGuard.tsxWraps individual routes that require a specific RBAC permission.
SessionRestoresrc/components/SessionRestore.tsxRuns POST /api/v1/auth/refresh on reload if user is known but access token is missing. Blocks <Outlet /> until token is restored.
/ → Dashboard (ProtectedRoute)
├── /login → PublicOnlyRoute → Login page
├── /register → PublicOnlyRoute → Registration
├── /auth/callback → OAuth callback handler (stores token, redirects)
├── /invite/:token → Invitation acceptance
├── /assets/* → ProtectedRoute + PermissionGuard(assets:read)
├── /sites/* → ProtectedRoute + PermissionGuard(sites:read)
├── /zones/* → ProtectedRoute + PermissionGuard(assets:read)
├── /vlans/* → ProtectedRoute + PermissionGuard(assets:read)
├── /incidents/* → ProtectedRoute + PermissionGuard(incidents:read)
├── /problems/* → ProtectedRoute + PermissionGuard(problems:read)
├── /changes/* → ProtectedRoute + PermissionGuard(changes:read)
├── /releases/* → ProtectedRoute + PermissionGuard(releases:read)
├── /service-requests/* → ProtectedRoute + PermissionGuard(service-requests:read)
├── /maintenance/* → ProtectedRoute + PermissionGuard(maintenance:read)
├── /knowledge/* → ProtectedRoute + PermissionGuard(knowledge:read)
├── /diagrams/* → ProtectedRoute + PermissionGuard(diagrams:read)
├── /vulnerabilities/* → ProtectedRoute + PermissionGuard(vulnerabilities:read)
├── /vendors/* → ProtectedRoute + PermissionGuard(vendors:read)
├── /security/* → ProtectedRoute + FeatureGuard(security) + PermissionGuard(security:read)
├── /vpn/* → ProtectedRoute + FeatureGuard(vpn) + PermissionGuard(vpn:read)
├── /backup/* → ProtectedRoute + FeatureGuard(backup) + PermissionGuard(backup:read)
├── /settings/* → ProtectedRoute + PermissionGuard(settings:read)
├── /admin/* → PlatformAdminRoute + VITE_ADMIN_PANEL_ENABLED
└── /public/vendor-assessments/:token → No auth (vendor portal)

React Router 6 renders routes synchronously on mount. Without guarding against hydration delay, protected routes redirect to /login on page reload before the Zustand store is populated from localStorage.

The fix: ProtectedRoute, PublicOnlyRoute, and PlatformAdminRoute all call useAuthStore.persist.hasHydrated() and render a loading spinner until hydration is complete. After hydration, if user exists but access_token is absent (common after F5), SessionRestore fires a refresh request before <Outlet /> renders.

All internal imports use the ~/ alias, which maps to src/:

import { incidentsApi } from '~/lib/api/incidents';
import { authStore } from '~/lib/stores/authStore';

Configured in vite.config.ts and tsconfig.json.