[AZ-447] autodev Steps 1-4 baseline: docs, tests, refactor specs

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>
This commit is contained in:
Oleksandr Bezdieniezhnykh
2026-05-11 00:38:49 +03:00
parent da0a5aa187
commit 510df68bcf
84 changed files with 13065 additions and 0 deletions
@@ -0,0 +1,109 @@
# Module: `src/api/client.ts`
> **Source**: `src/api/client.ts` (65 lines)
> **Topo batch**: B2 (leaf — no internal imports)
## Purpose
Minimal `fetch` wrapper that injects the JWT bearer token, normalises HTTP errors into `Error` throws, and transparently retries a single time after a 401 by attempting a refresh. Acts as the single HTTP entry point for every page; there is no per-service typed client.
## Public interface
Token plumbing:
```ts
export function setToken(token: string | null): void
export function getToken(): string | null
```
HTTP API:
```ts
export const api = {
get: <T>(url) => Promise<T>
post: <T>(url, body?) => Promise<T>
put: <T>(url, body?) => Promise<T>
patch: <T>(url, body?) => Promise<T>
delete: <T>(url) => Promise<T>
upload: <T>(url, formData: FormData) => Promise<T>
}
```
## Internal logic
- Module-level mutable variable `let accessToken: string | null` holds the current bearer token.
- `request<T>(url, options)`:
1. Build a `Headers` from `options.headers`, inject `Authorization: Bearer <token>` if present.
2. If `options.body` is a `string`, set `Content-Type: application/json`. (Crucial: `upload()` passes a `FormData` body, which is **not** a string, so `Content-Type` is left to the browser to set with the multipart boundary.)
3. `fetch(url, ...)`.
4. On `401` *and* a present token: call `refreshToken()`. On success, set the new bearer and retry the same request once. On failure, clear the token and `window.location.href = '/login'`, then throw "Session expired".
5. Hand off to `handleResponse<T>`.
- `handleResponse<T>(res)`:
- `204``undefined as T`.
- `!res.ok` → throw `new Error(\`${status}: ${text || statusText}\`)`. Body text is read defensively (`.catch(() => '')`).
- Otherwise → `res.json()` (no schema validation — caller types the response).
- `refreshToken()``POST /api/admin/auth/refresh` with `credentials: 'include'`. On 200, expects `{ token: string }` and stores it. Returns boolean.
## Dependencies
- **Internal**: none.
- **External**: `fetch`, `Headers`, `FormData`, `Response` (browser globals). No npm runtime dependency.
## Consumers (intra-repo)
The module-level `api` object is imported by:
- `src/auth/AuthContext.tsx` (login / logout / initial refresh)
- `src/components/FlightContext.tsx` (flights list, user settings get/put)
- `src/components/DetectionClasses.tsx` (admin classes load)
- `src/features/admin/AdminPage.tsx`
- `src/features/settings/SettingsPage.tsx`
- `src/features/dataset/DatasetPage.tsx`
- `src/features/flights/FlightsPage.tsx`
- `src/features/annotations/{AnnotationsPage,AnnotationsSidebar,MediaList}.tsx`
`setToken` is imported by `AuthContext` (login / refresh / logout).
`getToken` is imported by `src/api/sse.ts` (to append the token to SSE URLs).
## Data models
None defined here. The generic `T` parameter is supplied by call sites.
## Configuration
URLs are **string literals** at every call site (`/api/admin/...`, `/api/flights?...`, etc.). There is no base-URL constant. The `vite.config.ts` dev proxy and `nginx.conf` production rules forward `/api/*` to per-service backends.
A `VITE_API_BASE_URL` env-var fix is the canonical Step 4 testability candidate (workspace `README.md` calls this out).
## External integrations
Every backend the SPA talks to flows through this module. See `nginx.conf` for the routing table:
| Path prefix | Backend service |
|---|---|
| `/api/admin/*` | `admin/` (.NET) |
| `/api/annotations/*` | `annotations/` (.NET) |
| `/api/flights/*` | `flights/` (.NET) |
| `/api/resource/*` | `satellite-provider/` |
| `/api/detect/*` | `detections/` (Cython) |
| `/api/loader/*` | `loader/` (Cython) |
| `/api/gps-denied-desktop/*` | `gps-denied-desktop/` |
| `/api/gps-denied-onboard/*` | `gps-denied-onboard/` |
| `/api/autopilot/*` | `autopilot/` |
## Security
- **Token storage**: in-memory only (`accessToken: string | null` at module scope). Survives in-tab navigations but not full reloads. The refresh path (`POST /api/admin/auth/refresh` with `credentials: 'include'`) implies the refresh token rides in an HttpOnly cookie set by the `admin/` service. The bearer access token is therefore short-lived and never persisted to `localStorage`. Acceptable XSS posture.
- **401 handling**: redirects to `/login` via `window.location.href` (full page reload) — clears any in-memory state including the bearer.
- **Race condition**: two parallel requests that both 401 will both call `refreshToken()` independently — one will succeed, one may receive a stale token mid-flight. Track for B3/B4 audit; minor under the current usage but should be serialised in Step 8.
- **No CSRF token**: relies on the bearer scheme only; `credentials: 'include'` is set only on `/refresh`, so other endpoints don't carry the cookie. Verify with `admin/` service contract during Step 6 `security_approach.md`.
## Tests
None.
## Notes / open questions
- The `Authorization` header is set BEFORE `refreshToken()` in the retry path, but `refreshToken()` mutates the module-level `accessToken` and the retry then `headers.set('Authorization', \`Bearer ${accessToken}\`)` reads the NEW token. Correct, but worth a comment.
- `request` is typed `<T>` and trusts callers; a runtime schema validation layer (Zod, valibot) would be the right Step 8 hardening but is too heavy for testability scope.
- `upload(url, formData)` does NOT set `Content-Type`, allowing the browser to compute the multipart boundary. This is intentional and correct.