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>
6.5 KiB
Module: src/features/login/LoginPage.tsx
Source:
src/features/login/LoginPage.tsx(95 lines) Topo batch: B4 (depends on B3:auth/AuthContext)
Purpose
The single public route of the SPA. Collects email + password, calls
AuthContext.login(...), and on success runs a four-step "unlock"
animation (download key → decrypting → starting services → ready)
before navigating to /flights. Replaces the WPF LoginWindow.xaml
including its multi-step progress UI (_docs/legacy/wpf-era.md §3 / §4).
Public interface
export default function LoginPage(): JSX.Element
No props. Reads useAuth().login and react-router-dom's useNavigate.
Internal logic
-
State machine —
step: UnlockStepcycles through:idle → authenticating → downloadingKey → decrypting → startingServices → ready, withidlealso reachable on error fromauthenticating.type UnlockStep = 'idle' | 'authenticating' | 'downloadingKey' | 'decrypting' | 'startingServices' | 'ready' -
Local state:
email: string,password: string— controlled inputs.error: string— empty unless the previous attempt failed.step: UnlockStep— drives both the form/spinner branch and the spinner caption.
-
handleSubmit(e):preventDefault, clearerror, setstep = 'authenticating'.await login(email, password)— this throws on bad credentials (AuthContext.loginrethrows theapi.posterror).- On success: call
runUnlockSequence(). - On failure: reset
stepto'idle', seterror = t('login.error').
-
runUnlockSequence()— sequentially setsstepto each ofdownloadingKey,decrypting,startingServices,ready, with a 600mssetTimeoutdelay between each. Afterready, navigates to/flights. The unlock steps are purely presentational — there is no real downloadKey/decrypt work happening; the SPA's bearer token is already set insideawait login(...). The animation reproduces the WPF "vault unlocking" UX for continuity, not for any cryptographic operation. Total minimum wait after a successful auth: 4 × 600 ms = 2.4 s. Document explicitly to avoid future "what does this decrypt?" confusion. -
STEP_KEYS: aRecord<UnlockStep, string>mapping each step to a translation key.'idle'maps to the empty string (the spinner caption is not rendered when idle). -
Render: when
step === 'idle', the form is shown (email + password + error + submit). Otherwise the form is hidden and a centered spinner with the localized step caption is shown. Note the spinner is rendered even duringauthenticating(real network), so the user sees a single uninterrupted spinner across the entire auth → animation flow.
Dependencies
- Internal:
../../auth/AuthContext—useAuth().login. - External:
react(useState,FormEventtype),react-router-dom(useNavigate),react-i18next(useTranslation).
Consumers (intra-repo)
src/App.tsx— mounted at the public/loginroute, outsideAuthProvider. (Verify in B8 — currentlyApp.tsxputsAuthProviderinside the protected branch only, soLoginPagereachesuseAuth()only becauseApp.tsxactually wraps everything inAuthProviderat a higher level. Confirmed via the §7a graph edgeApp → AuthProvider.)
Data models
UnlockStep and STEP_KEYS are module-private. No DTOs or shared
types.
Configuration
- i18n keys:
login.title,login.email,login.password,login.submit,login.error,login.authenticating,login.downloadingKey,login.decrypting,login.startingServices,login.ready. (All present insrc/i18n/en.jsonandua.json— confirmed.) - Tailwind tokens:
bg-az-bg,bg-az-panel,border-az-border,text-az-orange,text-az-text,text-az-muted,text-az-red. Defined insrc/index.css. - Hardcoded animation timing:
600 msper step. No env override.
External integrations
None directly. Indirect: AuthContext.login calls
POST /api/admin/auth/login (routed by nginx.conf to the admin/
service).
Security
- Both inputs use the right
typeattribute (email,password), giving the browser the chance to mask the password and offer password-manager autofill. - The HTML form does NOT have
autoComplete="off"— autofill is intentionally allowed. - The component does NOT log credentials anywhere. The
errorstate carries only the localized "Invalid credentials" string, never the raw backend error. - After successful auth, the bearer is already in memory (
AuthContextset it). The 2.4s "unlock" animation does NOT extend the auth window — if the bearer expires server-side during the animation the next request retries viaclient.ts's 401 → refresh path. - The
setError(t('login.error'))shows a single generic error for every failure mode (wrong password, account locked, server down). Acceptable for security (no user-enumeration leak), but logs an observability flag — backend should keep specific reasons in its audit log.
Tests
None.
Notes / open questions
- The unlock animation is theatrical — it suggests cryptographic
work that is NOT happening. If a future phase actually adds
client-side key derivation, the animation should reflect real
progress (
'decrypting'would block on actual work). Keep the names but document the placeholder status insolution.md(Step 5). - No "loading" while
authenticatingbeyond hiding the form — Submit button shows no spinner of its own; the form simply hides the momentstepleaves'idle'. Mild UX gap (a quick auth failure shows the form blink). Defer to Step 8. - Caps Lock indicator missing — the WPF version had one
(
_docs/legacy/wpf-era.md). Not currently a goal; flag if the UX spec calls for it. - No "Forgot password" link — out of scope; the
admin/service may or may not support that flow. Verify during Step 6 problem extraction. - Form does not disable Submit while authenticating — but the
form is unmounted as soon as
step !== 'idle', so a double-click cannot fire a secondlogin(...)call. Acceptable. runUnlockSequenceresolution race: if the user navigates away mid-animation (e.g. closes the tab), the pendingsetTimeouts still fire but harmlessly. React's StrictMode double-invokes the effect-mount path in dev — butrunUnlockSequenceis invoked fromhandleSubmit, not an effect, so no duplication.