Wave 2 of replacing the fixed-data mock adapter: the auth domain joins the live list, so credentials, roles and tokens belong to the real user. - session: validate a restored token through /auth/me instead of trusting localStorage, clearing it on 401/403 but keeping it when the API is merely unreachable; the route guard now uses the validated session - login: report a 401 as invalid credentials rather than a generic failure, and drop the 6-character client rule so the API owns the password policy - register: raise the rule to the API's 8 characters, remove the verification-code field (its button only counted down and the value was never sent), and report a duplicate email (409) distinctly - TopBar and the user profile render their session-dependent branch client-only: validating the session before hydration made those localStorage-backed branches report hydration mismatches the previous code did not Verified against the running backend: wrong password rejected, real JWT issued, /user reachable, a short password refused with no network call, duplicate email reported, a tampered token cleared and bounced to sign-in, a stale token kept when the API is down, and the fixed-data rollback still signs in with the backend stopped. Also re-cuts docs/TBD-migrate-wave.md: auth is done, and cart, orders, shipments and invoices must move together, because the mock adapter keeps their state in one shared object and a partial flip fails at checkout. OpenSpec change: openspec/changes/replace-mock-api-wave-2
4.2 KiB
Design
Context
See proposal.md — Why. After wave 1 the mall selects adapters per domain through liveDomains (apps/mall/plugins/api.ts); auth, cart, orders, shipments and invoices are what remain on fixed data.
Session truth today lives entirely in localStorage: vmall.token is read by the plugin's getToken, vmall.user is read by middleware/auth.ts, and both are written by stores/session.ts. The fixed-data me() returns a constant MOCK_USER, so nothing ever proves a stored token is still valid.
The live contract this wave targets (apps/api/src/routes/auth.rs):
| Endpoint | Behaviour |
|---|---|
POST /auth/register |
400 on invalid email, password under 8 characters, or blank display name; 409 on duplicate email |
POST /auth/login |
401 on bad credentials; returns { token, user } |
GET /auth/me |
requires a bearer token; returns the User |
ApiError already carries status and code (packages/shared/src/api.ts:26-34).
Goals / Non-Goals
Goals:
- Credentials, roles and tokens become the real user's, with failures reported distinctly.
- The flip stays confined to the
authdomain, so every other domain keeps working.
Non-Goals:
- No refresh-token or cookie/session-server move: the JWT stays in
localStorage. - No password-reset backend;
/forgot-passwordstays presentational, which is a known gap rather than something this wave fixes. - No change to what the buyer center displays — its data is still fixed.
Decisions
1. Validate a restored token once per load through me(), and only clear the session on 401/403.
middleware/auth.ts currently trusts localStorage, so a stale or forged token reaches protected pages and fails later, further from the cause. A single me() call on hydrate puts the check where the session is established.
Alternatives: a 401 interceptor inside the shared request() helper (touches all three apps — a shared-package change outside this wave); validating in every guarded page (one call per navigation).
A network failure is deliberately not treated as a rejected token, so an unreachable API does not silently log the shopper out.
2. The 8-character rule lives in the panel and mirrors the API. The API is the authority; the client check exists only to avoid a pointless round trip and a generic error. Relaxing the API to accept 6 would weaken the backend and needs a change outside this wave.
3. Errors are mapped on ApiError.status/code, never on message text.
Matching the API's prose would break whenever its wording changes. New strings go in apps/mall/locales-extra.ts, because AGENTS.md reserves the shared locale bundle for contract changes.
4. The register panel drops the verification-code field. It is a mandatory no-op: the button only starts a countdown, the value is never sent, and no endpoint issues a code. Keeping it would demand input the API never checks.
5. liveDomains gains auth; rollback stays a one-string edit.
The fixed-data adapter keeps serving auth, so removing the entry restores the previous behaviour with no data migration.
Risks / Trade-offs
- [A 401 from a still-mocked domain could be mistaken for a session failure] → only
me()drives validation; cart, orders and invoices stay fixed-data and never answer 401. - [Validation adds a round trip before the session is usable] → it runs only when a stored token exists, and it replaces blind trust rather than adding a second source of truth.
- [The demo loses "any password works"] → intended and marked BREAKING; the seeded
customer@vmall.localaccount still demonstrates sign-in. - [The register panel becomes shorter than the backend's own validation] → the panel mirrors the API rule, and the API still enforces it independently.
Migration Plan
- Add
authtoliveDomainsand land the panel, session and middleware changes in the same commit, so no user can submit the old 6-character rule against the new API. - Verify against the running backend with the seeded customer, then a wrong password, then a tampered token.
- Rollback: drop
authfromliveDomains. The fixed-data adapter still serves the domain, and no stored data needs reverting.