Skip to content

feat(ui): Sign in from the login screen (#360) - #381

Merged
bitbiter-dev merged 4 commits into
masterfrom
feat/360-login
Oct 30, 2026
Merged

bitbiter-dev merged 4 commits into
masterfrom
feat/360-login

Conversation

@bitbiter-dev

Copy link
Copy Markdown
Owner

Closes #360. Implementation decisions: seeding and rate limits, where the code differs from the design.

Summary

src/Anichron.UI/src/
├── api/
│   ├── client.ts           # POST /api/v1/auth/login; holds the access token in memory only
│   ├── problem.ts          # Problem Details → code, field codes, Retry-After
│   └── login-contract.ts   # hand-written types, deleted by #347
├── shared/use-countdown.ts # counts down to a deadline, one tick per second
├── features/login/
│   ├── LoginPage.tsx
│   ├── use-login-form.ts   # React Hook Form + zod/mini, errors, countdown
│   └── messages.ts         # code → catalog key, generic fallback
└── test/handlers/login.ts  # MSW handlers, deleted by #347
/            beforeLoad: no token → redirect /login   (signed-in placeholder)
/login       sign in → navigate /

API: RateLimit:SensitivePermitLimit, a validated setting with the default unchanged (10). Only docker-compose.e2e.yml raises it, because every e2e request reaches the API from the one Docker bridge address.

E2E users are seeded only through the public API:

  • Global setup: rotates the bootstrap admin's first password, and fails if the database isn't fresh.
  • Each test: creates its own user with POST /users and changes its temporary password.
  • Disabled account: PATCH /users/{id}.
  • Locked account: repeated wrong logins.

zod/mini instead of zod, because the full build left the initial JS at 148.5 of the 150 kB budget.

Evidence

  • Before: no login screen. / was an unguarded placeholder.
    After:
    • UI tests (seam 2, MSW, axe): 16 pass. They cover success, required fields, each code, unknown code, non-JSON body, network failure, field errors, the locked countdown, the rate-limited countdown, and a missing Retry-After.
    • Scoped mutation: UI 100% (123 killed), backend 100% (75 killed).
    • API unit tests: 777 pass, including the new permit-limit tests (a configured limit of 3 allows 3, then returns 429; 0 and -1 fail at startup).
    • E2E, run locally against the built image: WebKit iPhone and Chromium Pixel pass 12 of 12. They cover success with axe, reload signs out, wrong password, disabled account, and a locked account that counts down and then signs in, with no CSP violations.
    • Firefox: not run. It would not start on the local machine ("Could not find profile folder", including on the unchanged smoke test). Its first run is this PR's E2E job.
  • Bundle: initial JS is 130.53 kB of 150 kB (master: 109.32 kB), CSS 117 B of 15 kB.

Merge Danger

Door: two-way

Blast Radius: UI routes

/ now needs a token, so the smoke test checks the login page instead of the placeholder. The API change only adds a setting, and its default is unchanged.

🤖 Generated with Claude Code

bitbiter-dev and others added 4 commits October 30, 2026 20:21
A user signs in at /login with username or email and password and lands
on the signed-in placeholder at /, which sends a visitor without a token
back to /login. React Hook Form and zod/mini validate the form. src/api/
calls POST /api/v1/auth/login, keeps the access token in memory only, and
reads the Problem Details: code, field errors and Retry-After. Each code
maps to a catalog message; an unknown code, an unreadable body or a
network failure shows the generic one. A locked or rate-limited response
counts down from Retry-After and blocks the button until it ends.

zod/mini instead of zod: the full build put initial JS at 148.5 of the
150 kB budget; it is 130.5 kB now. zod's eval probe is turned off in
main.tsx, because the CSP forbids eval.

End-to-end flows: success, wrong password, disabled account, and locked
account with its countdown, in all three browsers. No public endpoint
creates invites (#107 is deferred), so users are seeded through the admin
API, as decided on #360. The global setup rotates the bootstrap admin's
password and fails on a database that is not fresh. Each test creates
its own user and changes the temporary password.

Every e2e request reaches the API from the one Docker bridge address, so
the Sensitive rate limit (10 per minute per client) would stop the suite
within seconds. Its permit limit becomes the validated setting
RateLimit:SensitivePermitLimit. The default is unchanged; only the e2e
compose file raises it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MessageKey is derived from the catalog, both rate-limit policies are
configured in one place, the success and disabled-account flows are
checked with axe, an e2e reload proves the token is not persisted, and
the lockout pauses leave a margin past each lock.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The countdown sits beside the alert instead of inside it, so a screen
reader announces the error once rather than on every tick. The success
flow checks for CSP violations before the reload clears them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@bitbiter-dev
bitbiter-dev merged commit 8c0fb72 into master Oct 30, 2026
20 checks passed
@bitbiter-dev
bitbiter-dev deleted the feat/360-login branch October 30, 2026 08:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Sign in from the login screen

1 participant