Stage M2
This commit is contained in:
@@ -2,9 +2,10 @@
|
||||
|
||||
A modern sports-fashion e-commerce platform.
|
||||
|
||||
**Status: milestone 1 — catalog read API, live end to end, in Vietnamese and English.**
|
||||
The storefront renders real products from the database through the REST API. Cart, checkout,
|
||||
orders and auth are still ahead; see [Roadmap](#roadmap).
|
||||
**Status: milestone 2 — auth and RBAC, on top of a live bilingual catalog.**
|
||||
The storefront renders real products in Vietnamese and English; the admin has working sign-in
|
||||
with rotating refresh tokens and permission-filtered navigation. Cart, checkout and orders are
|
||||
next; see [Roadmap](#roadmap).
|
||||
|
||||
```
|
||||
Storefront (Next.js) ─┐
|
||||
@@ -97,8 +98,13 @@ pnpm db:generate # regenerate Prisma Client
|
||||
pnpm db:seed # reconcile permissions + roles (idempotent)
|
||||
pnpm db:studio # Prisma Studio
|
||||
pnpm db:reset # drop, re-migrate, re-seed
|
||||
pnpm db:create-admin # create/repair a SUPER_ADMIN (prints a generated password)
|
||||
```
|
||||
|
||||
`pnpm db:seed` also creates three **development** sign-in accounts and prints their generated
|
||||
passwords once. They are skipped when `NODE_ENV=production` or `SEED_DEMO=false`; real
|
||||
environments use `pnpm db:create-admin`, which is also the lockout-recovery path.
|
||||
|
||||
### Running one app manually
|
||||
|
||||
```bash
|
||||
@@ -153,7 +159,7 @@ sport-store/
|
||||
│
|
||||
├── docs/
|
||||
│ ├── architecture.md Boundaries, conventions, risks — read this first
|
||||
│ └── adr/ 14 decision records
|
||||
│ └── adr/ 15 decision records
|
||||
│
|
||||
├── docker-compose.yml Backing services; `--profile full` runs everything
|
||||
├── turbo.json pnpm-workspace.yaml package.json
|
||||
@@ -194,6 +200,11 @@ Full detail in [`docs/architecture.md`](./docs/architecture.md). The rules that
|
||||
is why most "add a language" projects end up half-translated.
|
||||
([ADR-0013](./docs/adr/0013-content-translations-in-typed-tables-ui-strings-in-message-catalogs.md))
|
||||
|
||||
9. **The browser always calls the API on its own origin** — Nginx in production, a Next rewrite
|
||||
in development. That is what makes the httpOnly refresh cookie first-party, and what keeps
|
||||
dev and production authenticating identically.
|
||||
([ADR-0015](./docs/adr/0015-frontends-reach-the-api-through-their-own-origin.md))
|
||||
|
||||
### Languages
|
||||
|
||||
Vietnamese is the default and is served from clean URLs; English is prefixed with `/en`.
|
||||
@@ -231,7 +242,7 @@ locale-in-path would buy nothing.
|
||||
| --------- | -------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **M0** ✅ | Architecture, tooling, schema, health check, Docker, CI |
|
||||
| **M1** ✅ | Catalog read API + Redis caching + vi/en localisation + storefront wired to real data |
|
||||
| **M2** | Auth: login, refresh rotation, RBAC admin, user/role management |
|
||||
| **M2** ✅ | Auth: login, refresh rotation with reuse detection, RBAC admin, user & role management |
|
||||
| **M3** | Admin catalog: product editor, variant matrix, media uploads, inventory |
|
||||
| **M4** ◐ | Storefront catalog — listings, PDP, variant selector and filters landed with M1; sort UI, pagination and a mobile filter drawer remain |
|
||||
| **M5** | Cart, checkout, orders |
|
||||
@@ -240,10 +251,10 @@ locale-in-path would buy nothing.
|
||||
| **M8** | Customer account |
|
||||
| **M9** | Payments (VNPay, MoMo, ZaloPay, COD), shipping, notifications |
|
||||
|
||||
**Recommended next step: M2 (auth).** The enforcement half already exists — global access-token
|
||||
guard, RBAC permissions guard, audience separation — so only issuance is missing: login, refresh
|
||||
rotation, and the admin user/role screens. Everything after it (cart ownership, orders, the admin
|
||||
write path) depends on knowing who is asking.
|
||||
**Recommended next step: M3 (admin catalog write path).** Reads, auth and RBAC are in place, so
|
||||
the product editor and variant matrix now have everything they need — a known operator, a
|
||||
permission to check, and a catalog to edit. It is also what makes the seed replaceable by real
|
||||
merchandising.
|
||||
|
||||
---
|
||||
|
||||
@@ -253,17 +264,35 @@ Everything below was run, not assumed:
|
||||
|
||||
- `pnpm lint` · `pnpm typecheck` · `pnpm test` · `pnpm build` — 25/25 Turborepo tasks pass;
|
||||
`pnpm format:check` clean
|
||||
- 5 migrations applied; 32 tables; seed loads 36 permissions, 6 roles, 3 brands, 8 categories,
|
||||
3 collections, 12 products, **155 variants** and 64 generated images uploaded to MinIO
|
||||
- `pnpm test` — 17 passing (RBAC guards, translation fallback, `Accept-Language` negotiation)
|
||||
- API: listings with filters/facets/cursor paging, PDP, navigation, brands and collections all
|
||||
return correctly localised payloads in both `vi` and `en`
|
||||
- Storefront: every route returns 200 in both locales; PDP renders translated options, spec
|
||||
table and variant titles; `/en/products/<vi-slug>` → 307 → `/en/products/<en-slug>`;
|
||||
`hreflang` + canonical emitted per locale
|
||||
- Money formats per locale from one integer: `690.000 ₫` (vi) / `₫690,000` (en)
|
||||
- 5 migrations, 32 tables; seed loads 36 permissions, 6 roles, 3 brands, 8 categories,
|
||||
3 collections, 12 products, **155 variants**, 64 uploaded images and 3 dev accounts
|
||||
- **25 tests** — RBAC guards, password hashing, translation fallback, `Accept-Language`
|
||||
- Catalog: listings with filters/facets/cursor paging, PDP, navigation — correctly localised in
|
||||
both `vi` and `en`; money formats per locale from one integer (`690.000 ₫` / `₫690,000`)
|
||||
- Storefront: every route 200 in both locales; `/en/products/<vi-slug>` → 307 →
|
||||
`/en/products/<en-slug>`; `hreflang` + canonical emitted per locale
|
||||
- **Auth:** admin sign-in works through the app's own origin; the refresh cookie is httpOnly and
|
||||
scoped to `/api/v1/auth`; refresh rotates the token; **replaying a rotated token is rejected and
|
||||
revokes the entire family** (verified: 2 of 3 sessions revoked)
|
||||
- **Audience isolation:** a customer cannot sign in at the admin endpoint and an admin cannot sign
|
||||
in at the storefront endpoint — both return the same `INVALID_CREDENTIALS` as a wrong password,
|
||||
so the form cannot be used to enumerate accounts
|
||||
- **RBAC:** a `catalog_manager` receives `PERMISSION_DENIED` on `/admin/users` and `/admin/roles`;
|
||||
no token gives `UNAUTHENTICATED`
|
||||
- Admin: renders Vietnamese by default and English with `sport_admin_locale=en`
|
||||
|
||||
### Verified in a real browser
|
||||
|
||||
Server-side checks and curl are not sufficient for client behaviour — three bugs proved it.
|
||||
Confirmed by clicking through Chrome with the console and network panel open:
|
||||
|
||||
- Admin sign-in issues exactly **one** `POST /auth/admin/login`, then redirects to the dashboard
|
||||
- Sidebar is filtered by the signed-in operator's permissions; users table and role viewer load
|
||||
real data; language switch preserves the session and the current page; sign-out returns to login
|
||||
- Storefront PDP: gallery swaps with the colourway, per-variant stock disables the right sizes,
|
||||
SKU updates, and switching language moves between translated slugs
|
||||
- Filters apply (`/men?colors=black&onSale=true`), and all 16 grid images load
|
||||
|
||||
Known benign noise: NestJS logs two `Unsupported route path: "/api/*"` warnings at boot. They
|
||||
come from Nest's own global-prefix handling under Express 5 / path-to-regexp v8, are
|
||||
auto-converted correctly, and routing is verified working. Nothing in this repository registers
|
||||
|
||||
Reference in New Issue
Block a user