Files
vmall/docs/TBD-migrate-wave.md
T
james 0ceb4a2b25 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
2026-09-17 16:13:16 +00:00

6.3 KiB

TBD — migrate the mall off the mock API (waves 3+)

Waves 1 and 2 are captured in openspec/changes/replace-mock-api-wave-1/ (catalog + currency) and replace-mock-api-wave-2/ (auth). This file tracks what remains.

How to use: check a box only once the behaviour is implemented and verified against the live backend (cargo run -p vmall-api, node scripts/seed-demo.mjs). The per-domain switch in apps/mall/plugins/api.ts means most of these are adding a domain name to the live list — each still needs its own verification below.

Delete this file once every box in Wave 3 is checked. Wave 4 is optional: if you decide against it, delete this file anyway and record that decision wherever you like.


Wave 2 — auth (done)

Captured in openspec/changes/replace-mock-api-wave-2/; auth is live, with the session validated through /auth/me rather than trusted from localStorage.

  • Flip auth to live and verify login / register / me against :8080 using the seeded customer@vmall.local / customer123.
  • Confirm bad credentials now produce a real 401 — the mock accepted any input (apps/mall/mock/api.ts:125), so this is a deliberate UX change.
  • Confirm the JWT round-trips through the vmall.token localStorage key, shared with shop-admin and admin, and that logout clears it.

Wave 3 — the transaction chain: cart + orders + shipments + invoices

These move together, not one at a time. The mock adapter keeps state.cart -> state.orders -> state.shipments in a single shared state, so a partial flip leaves the mock half reading state the live half never populates:

  • live cart + mock checkout() fails with EMPTY_CART (apps/mall/mock/api.ts:194),

  • mock requestInvoice() 404s on any live order id (mock/api.ts:285),

  • mock listMyShipments() returns shipments whose order_id matches no live order, so the shipment block is silently empty (mock/api.ts:282, pages/user/orders/[id].vue:25).

  • Flip cart, orders, shipments and invoices in the same change, then verify one purchase end to end: add to cart, check out into per-shop orders, pay, ship, confirm delivery, request an invoice.

  • Verify cancel restores stock and payOrder only accepts pending_payment (both enforced live; the mock only mimicked them).

  • Verify a company invoice requires a tax number, and that one order can hold only one active invoice.

  • Remove cart's mock display coupling: apps/mall/pages/cart.vue:4,34,41,56,60 uses productById / storeById for shop name, image and stock. The live CartView already returns product_name, image and unit_price_minor (apps/api/src/cart.rs:44-53) — but not the shop, so decide whether to add a shop field to CartItem in the shared contract or accept ungrouped lines until the public store read exists.

  • Decide what caps cart quantity: CartItem carries no stock, so maxFor falls back to 999 (pages/cart.vue:60). The live cart does not enforce stock either — only checkout does (409). Either add stock to CartItem or keep the cap at checkout and say so.

  • Gate add-to-cart for anonymous shoppers: a live addCartItem on the public product page returns 401, and pages/goods/[id].vue is not behind the auth middleware.

  • Checkout keeps sourcing shipping_address from MOCK_ADDRESSES (pages/checkout/index.vue:4,23,37,127) — intentional; see the out-of-scope note below.

  • Fix contract debt so the TS types stop lying: Shipment.items is required in packages/shared/src/types.ts but the live struct has no items field (apps/api/src/models.rs:172-182), and Invoice.invoice_no is nullable live (models.rs:194) but non-null string in TS.

  • Remove the remaining storeById mock usage on the order pages (apps/mall/pages/user/orders/index.vue:4).

  • Confirm the mall still renders when the live API is down, with every domain configured to fixed data.

Wave 4 — optional, mostly new backend capabilities

Only if you want more of the storefront backed by real data. Each is a new capability, not a flip.

  • Storefront content — banners, promos, quick links and floor advert art. Needs real tables, admin CRUD and i18n JSONB. Do this first if you want the home page fully live; it is the most visible remaining mock surface.
  • Public store read — a buyer-facing shop endpoint so stores/index, stores/[id] and the cart's shop grouping leave mock. Small: products already carry shop_id, and the public catalog already joins shops for the active check.
  • Brand model + sales/comments sorts — restores the brand facet and the sorts removed in Wave 1. Needs a brands table (products.brand_id + i18n) plus sales and comments data, neither of which exists today.
  • Extend the ORDER BY whitelist if more sorts are wanted beyond the sort=price added in Wave 1.
  • (adjacent, not part of the migration) Move the session token to a cookie so SSR knows whether anyone is signed in. Today a full page load of a guarded route renders the page and then redirects on the client, which logs a hydration mismatch; it is pre-existing (verified identical before Wave 2) and harmless, but it is the real fix for the ClientOnly workarounds in components/shell/TopBar.vue and pages/user.vue.

Deliberately out of scope — not tracked here, and they do not block deleting this file

These have no API contract and no backend model. Leaving them on ~/mock/data is a decision, not a backlog item.

  • Addresses — never a blocker: Address is embedded in the order and live checkout takes it in the request body, so no addresses table is needed. MOCK_ADDRESSES can stay behind checkout indefinitely.
  • Favorites, coupons, account stats — pure presentation, no transactional impact.
  • seckill / collective / integral marketing pages — display-only mock content.

Decisions already made (do not relitigate)

  • Category filtering is by subtree; the backend exact-match filter was the bug (fixed in Wave 1).
  • A migration wave must not change what the UI claims: facets without a backing model are removed rather than left matching nothing.
  • The mall is the last mock holdout; shop-admin and admin already run live against the same backend.
  • Auth flips independently, but the transaction domains do not: the mock's shared cart/order/shipment state makes any partial flip fail loudly.