feat(mall): authenticate against the live API
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
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-09-17
|
||||
@@ -0,0 +1,60 @@
|
||||
# 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 `auth` domain, 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-password` stays 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.local` account 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
|
||||
|
||||
1. Add `auth` to `liveDomains` and 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.
|
||||
2. Verify against the running backend with the seeded customer, then a wrong password, then a tampered token.
|
||||
3. Rollback: drop `auth` from `liveDomains`. The fixed-data adapter still serves the domain, and no stored data needs reverting.
|
||||
@@ -0,0 +1,32 @@
|
||||
# Proposal
|
||||
|
||||
## Why
|
||||
|
||||
Auth is the last thing between the mall and real accounts. The fixed-data adapter accepts any credentials and always returns the same demo user, so login, register and "me" are theatre — and every domain needing a bearer token (cart, orders, invoices) is pinned behind them. `shop-admin` and `admin` already authenticate against the live backend.
|
||||
|
||||
## What Changes
|
||||
|
||||
- Flip the `auth` domain to live via `liveDomains`; cart, orders, shipments and invoices stay on fixed data.
|
||||
- Align the client password rule with the API: the panels require 6 characters, but `/auth/register` requires 8 and answers 400 (`apps/api/src/routes/auth.rs:32`).
|
||||
- Map real failures to distinct messages — 401 for bad credentials, 409 when an email is taken — instead of one generic "request failed".
|
||||
- Drop the register SMS-code field: its button only runs a countdown, there is no endpoint, and the value is never sent, so it is a mandatory no-op once auth is real.
|
||||
- Validate a stored token through `/auth/me` on load rather than trusting `localStorage` alone.
|
||||
- **BREAKING** (auth UX): the demo's "log in as anyone, any password" behaviour ends; wrong credentials now fail.
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
(none)
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
- `frontend-mall`: the "Auth and buyer center" requirement stops being backed by deterministic mock auth and uses the live auth API.
|
||||
|
||||
## Impact
|
||||
|
||||
`apps/mall/plugins/api.ts` and `apps/mall/nuxt.config.ts` (`liveDomains`), `pages/login.vue`, `pages/register.vue`, `stores/session.ts`, `middleware/auth.ts`, and `locales-extra.ts` for the new error strings. No backend or `shop-admin`/`admin` change.
|
||||
|
||||
## Non-goals
|
||||
|
||||
Cart, orders, shipments and invoices stay on fixed data: the mock adapter holds `cart -> orders -> shipments` as one shared state, so flipping cart alone would fail checkout with `EMPTY_CART`. They move together in a later wave. No store, address, favourite or coupon work.
|
||||
@@ -0,0 +1,26 @@
|
||||
# Spec Delta
|
||||
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Auth and buyer center
|
||||
The mall SHALL provide B2B2C mall-style login, register and forgot-password panels backed by the live auth API, so credentials, roles and tokens belong to the real user rather than a fixed demo account. Registration SHALL require a password of at least 8 characters, matching the API's rule, and the register panel SHALL NOT ask for a verification code because no endpoint issues one. Failures SHALL be reported distinctly: invalid credentials on sign-in, and an already-registered email on registration. A token restored from storage SHALL be validated against the auth API on load, and a rejected token SHALL clear the session and return the shopper to sign-in. `/user` SHALL render a two-column buyer center with dashboard, order list/detail, addresses, favorites, coupons and invoices.
|
||||
|
||||
#### Scenario: sign in and inspect buyer data
|
||||
- **WHEN** a shopper signs in with valid credentials and opens `/user`
|
||||
- **THEN** the session carries the authenticated user, and the buyer-center shell and its fixed account/order/address/favorite/coupon/invoice data render
|
||||
|
||||
#### Scenario: wrong password rejected
|
||||
- **WHEN** a shopper submits a password that does not match the account
|
||||
- **THEN** sign-in fails with an invalid-credentials message and no session is established
|
||||
|
||||
#### Scenario: short password refused before the API call
|
||||
- **WHEN** a shopper submits a registration password shorter than 8 characters
|
||||
- **THEN** the panel asks for at least 8 characters without calling the API
|
||||
|
||||
#### Scenario: duplicate email reported
|
||||
- **WHEN** a shopper registers an email that already has an account
|
||||
- **THEN** the panel reports that the email is already registered rather than a generic failure
|
||||
|
||||
#### Scenario: rejected token clears the session
|
||||
- **WHEN** a token restored from storage is rejected by the auth API
|
||||
- **THEN** the stored session is cleared and the shopper is returned to sign-in
|
||||
@@ -0,0 +1,28 @@
|
||||
# Tasks
|
||||
|
||||
## 1. Adapter and configuration
|
||||
|
||||
- [x] 1.1 Add `auth` to the default `liveDomains` in `apps/mall/nuxt.config.ts` and confirm `LIVE_PICKS.auth` in `apps/mall/plugins/api.ts` already covers `register`/`login`/`me`; verify a sign-in request reaches `:8080` while `getCart` still resolves from fixed data
|
||||
- [x] 1.2 Confirm the rollback path: with `NUXT_PUBLIC_LIVE_DOMAINS` set to catalog and currency only, sign-in returns the fixed demo user again; verify by signing in with any password
|
||||
|
||||
## 2. Session and route guard
|
||||
|
||||
- [x] 2.1 Add a session action that validates a restored token through `$api.me()`, clearing the stored session and returning to sign-in on 401/403 but leaving the session intact when the API is merely unreachable; verify with a tampered `vmall.token` and again with the backend stopped
|
||||
- [x] 2.2 Make `middleware/auth.ts` rely on the validated session rather than `localStorage` alone; verify a shopper with a stale token lands on `/login` while a valid one reaches `/user`
|
||||
- [x] 2.3 Render the session-dependent header and profile name client-only (`components/shell/TopBar.vue`, `pages/user.vue`); added during implementation because validating the session before hydration made those `localStorage`-backed branches report hydration mismatches that the pre-change code did not; verified by A/B that the sign-in flow now produces no mismatch warnings
|
||||
|
||||
## 3. Sign-in panel
|
||||
|
||||
- [x] 3.1 Map failures on `ApiError.status`/`code` so a 401 reports invalid credentials rather than the current generic message, adding the key to `apps/mall/locales/auth.ts` in en and zh (the per-domain module behind `locales-extra.ts`); verify with a deliberately wrong password
|
||||
|
||||
## 4. Registration panel
|
||||
|
||||
- [x] 4.1 Raise the client password rule to 8 characters with its own message; verify a 7-character password is refused without any network call. `apps/mall/pages/forgot-password.vue` was aligned to the same rule because it shares the `validationPassword` message
|
||||
- [x] 4.2 Remove the verification-code field and its countdown so the form no longer requires it; verify registration submits with display name, email and password only
|
||||
- [x] 4.3 Report a duplicate email (409) distinctly from other failures, adding the key to `apps/mall/locales/auth.ts` in en and zh; verify by registering an address that already exists
|
||||
|
||||
## 5. Verification
|
||||
|
||||
- [x] 5.1 Run `pnpm --filter @vmall/mall build` and confirm it passes
|
||||
- [x] 5.2 With the backend running and seeded, verify in a browser: sign in as `customer@vmall.local`, see the utility-bar welcome/sign-out state, reach `/user`, and confirm the only console entries are the deliberately triggered 401/409 responses plus a pre-existing hydration warning on the unauthenticated `/user` redirect (A/B verified identical before this change)
|
||||
- [x] 5.3 With every domain set to fixed data and the backend stopped, verify sign-in and browsing still work, so the rollback path is intact
|
||||
Reference in New Issue
Block a user