Files
ui/_docs/02_document/modules/src__features__login__LoginPage.md
T
Oleksandr Bezdieniezhnykh 510df68bcf [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>
2026-05-11 00:38:49 +03:00

6.5 KiB
Raw Blame History

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 machinestep: UnlockStep cycles through: idle → authenticating → downloadingKey → decrypting → startingServices → ready, with idle also reachable on error from authenticating.

    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):

    1. preventDefault, clear error, set step = 'authenticating'.
    2. await login(email, password) — this throws on bad credentials (AuthContext.login rethrows the api.post error).
    3. On success: call runUnlockSequence().
    4. On failure: reset step to 'idle', set error = t('login.error').
  • runUnlockSequence() — sequentially sets step to each of downloadingKey, decrypting, startingServices, ready, with a 600ms setTimeout delay between each. After ready, 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 inside await 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: a Record<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 during authenticating (real network), so the user sees a single uninterrupted spinner across the entire auth → animation flow.

Dependencies

  • Internal: ../../auth/AuthContextuseAuth().login.
  • External: react (useState, FormEvent type), react-router-dom (useNavigate), react-i18next (useTranslation).

Consumers (intra-repo)

  • src/App.tsx — mounted at the public /login route, outside AuthProvider. (Verify in B8 — currently App.tsx puts AuthProvider inside the protected branch only, so LoginPage reaches useAuth() only because App.tsx actually wraps everything in AuthProvider at a higher level. Confirmed via the §7a graph edge App → 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 in src/i18n/en.json and ua.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 in src/index.css.
  • Hardcoded animation timing: 600 ms per 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 type attribute (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 error state carries only the localized "Invalid credentials" string, never the raw backend error.
  • After successful auth, the bearer is already in memory (AuthContext set 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 via client.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 in solution.md (Step 5).
  • No "loading" while authenticating beyond hiding the form — Submit button shows no spinner of its own; the form simply hides the moment step leaves '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 second login(...) call. Acceptable.
  • runUnlockSequence resolution race: if the user navigates away mid-animation (e.g. closes the tab), the pending setTimeouts still fire but harmlessly. React's StrictMode double-invokes the effect-mount path in dev — but runUnlockSequence is invoked from handleSubmit, not an effect, so no duplication.