# 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

```ts
interface Props { children: ReactNode }
export default function ProtectedRoute(props: Props): JSX.Element
```

Always returns a `JSX.Element` — never `null`. The three rendered shapes
are:

1. Spinner (centered `<div>` with the orange ring) while `loading`.
2. `<Navigate to="/login" replace />` when `loading === false && user == null`.
3. `<>{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 — `loading` is checked before `user` so a freshly
  reloaded tab never renders the login redirect during the in-flight
  refresh attempt. (See the open `AuthContext` bootstrap-vs-refresh
  divergence flagged in `src__auth__AuthContext.md`; if that bug is
  fixed the spinner duration becomes accurate.)

## Dependencies

- **Internal**: `./AuthContext` — `useAuth`.
- **External**: `react-router-dom` (`Navigate`), `react` (`ReactNode` type 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 because `loading` defaults to `true` in
  `AuthProvider`.
- 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.md` accessibility notes.
- No timeout on the `loading` state — if `AuthContext`'s bootstrap
  somehow never resolves (e.g., the refresh endpoint hangs), the user
  sees an infinite spinner. `client.ts` does not currently set a
  request timeout either; flag jointly with Step 4.
- The `<>{children}</>` Fragment wrap is intentional: returning `children`
  directly would make the type `ReactNode` rather than `JSX.Element` and
  would not satisfy the route element slot in `react-router-dom@7`.
