This commit is contained in:
Nông Đức Huy
2026-08-13 23:20:22 +07:00
parent 3d6b0e0d4e
commit 5386bc51d1
65 changed files with 4058 additions and 161 deletions
+47 -18
View File
@@ -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