Captures the full output of autodev existing-code Phase A through Step 4 (Code Testability Revision) for the Azaion UI workspace: - Step 1 Document: _docs/02_document/ (FINAL_report, architecture, glossary, components/, modules/, diagrams/, system-flows, module-layout) plus _docs/00_problem/ + _docs/01_solution/ + _docs/legacy/ + _docs/how_to_test + README. - Step 2 Architecture Baseline: architecture_compliance_baseline.md. - Step 3 Test Spec: _docs/02_document/tests/ (environment, test-data, blackbox/performance/resilience/security/ resource-limit tests, traceability-matrix), enum_spec_snapshot, expected_results/results_report.md (98 rows), plus the run-tests.sh + run-performance-tests.sh runners. - Step 4 Code Testability Revision: 01-testability-refactoring/ run dir (list-of-changes C01-C07, deferred_to_refactor, analysis/research_findings + refactoring_roadmap) and the 7 child task specs AZ-448..AZ-454 under _docs/02_tasks/todo/ plus _dependencies_table.md. - _docs/_autodev_state.md pins the cursor at Step 4 / refactor Phase 4 entry so /autodev resumes cleanly. Epic AZ-447 (UI testability gates) tracks the 7 child tasks that will land in subsequent commits. Co-authored-by: Cursor <cursoragent@cursor.com>
3.7 KiB
Module: src/auth/ProtectedRoute.tsx
Source:
src/auth/ProtectedRoute.tsx(19 lines) Topo batch: B4 (depends on B3:auth/AuthContext)
Purpose
A tiny route guard that gates its children behind an authenticated session.
While AuthContext is bootstrapping (loading === true) it shows a spinner;
when the bootstrap finishes with no user it redirects to /login; otherwise
it renders its children. Mounted exactly once in App.tsx between
AuthProvider and FlightProvider so every authenticated page benefits
from it. WPF parallel: the implicit "no LoginWindow open ⇒ MainWindow"
gate (_docs/legacy/wpf-era.md §4).
Public interface
interface Props { children: ReactNode }
export default function ProtectedRoute(props: Props): JSX.Element
Always returns a JSX.Element — never null. The three rendered shapes
are:
- Spinner (centered
<div>with the orange ring) whileloading. <Navigate to="/login" replace />whenloading === false && user == null.<>{children}</>otherwise.
replace is intentional: it rewrites the history entry so the back
button does not return to a protected route the user was bounced off.
Internal logic
- Single hook call:
const { user, loading } = useAuth(). No local state. - Branch order matters —
loadingis checked beforeuserso a freshly reloaded tab never renders the login redirect during the in-flight refresh attempt. (See the openAuthContextbootstrap-vs-refresh divergence flagged insrc__auth__AuthContext.md; if that bug is fixed the spinner duration becomes accurate.)
Dependencies
- Internal:
./AuthContext—useAuth. - External:
react-router-dom(Navigate),react(ReactNodetype only).
Consumers (intra-repo)
From the §7a dependency graph:
src/App.tsx— wraps the entire authenticated route tree:AuthProvider → ProtectedRoute → FlightProvider → Header + nested Routes.
Not used anywhere else; the SPA has a single protected zone.
Data models
None.
Configuration
The /login redirect target is hardcoded. Matches the public route
declared in App.tsx. If the public route is ever renamed, update both
sites.
The spinner uses Tailwind tokens bg-az-bg, border-az-orange
(defined in src/index.css).
External integrations
None directly. Indirectly relies on whatever AuthContext calls during
bootstrap — currently GET /api/admin/auth/refresh.
Security
- No token check is duplicated here — relies entirely on
AuthContext. The component cannot be confused into rendering protected children before the bootstrap resolves becauseloadingdefaults totrueinAuthProvider. - Backend authority is unchanged — this is a UI affordance only. Every request the children make MUST also be enforced server-side.
- The redirect uses
Navigate, so query params on the original URL are lost. Acceptable today (no protected route relies on them); flag if a future "deep-link after login" UX appears.
Tests
None.
Notes / open questions
- The spinner has no
role="status"or accessible label — screen readers hear nothing while the bootstrap runs. Cosmetic; flag for Step 4 verification against_docs/ui_design/README.mdaccessibility notes. - No timeout on the
loadingstate — ifAuthContext's bootstrap somehow never resolves (e.g., the refresh endpoint hangs), the user sees an infinite spinner.client.tsdoes not currently set a request timeout either; flag jointly with Step 4. - The
<>{children}</>Fragment wrap is intentional: returningchildrendirectly would make the typeReactNoderather thanJSX.Elementand would not satisfy the route element slot inreact-router-dom@7.