From 2de90c6b356f3e7bac26254db5240f656ace7ccc Mon Sep 17 00:00:00 2001 From: "s.esmaeili" Date: Wed, 23 Sep 2026 02:38:00 +0330 Subject: [PATCH] docs(delivery): add a step-by-step quick start and a full pre-release section MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. "0. Quick start — run these in order" at the top of DELIVERY.md: five steps from an empty machine to a verified running stack, copy-pasteable, with no decisions to make. Covers which env vars must have a value (and how to generate the two JWT secrets), `pnpm build:release` BEFORE `docker compose build`, what the boot log should say, which banner is expected and which means a step was missed, and how to run the end-to-end check. Every command in it was executed verbatim to confirm it works. 2. "Before the real release" replaces the old three-item list: - A. blocking config: real JWT_SECRET and a SEPARATE JWT_CUSTOMER_SECRET, remove OTP_MASTER_CODE, configure the SMS credentials - B. credentials the reference leaked and that must be rotated (SEC-12, SEC-06) — same live accounts, never copied into this repo - C. parity work not finished: admin (financial documents, users and permissions incl. the still-open PERM-01, reports) and apps (MON-08 first, then STUB-05 SMS recipients, STUB-04, STUB-03, analytics), pointing at ISSUES.md for the rest Also removed the obsolete `version:` key from docker-compose.yml, which printed a deprecation warning on every compose command and made the checklist's output noisy. Config re-validated. Verified after these changes: container bundle is byte-identical to the local release build (sha256 match), and that container was re-tested end to end: compose e2e 32/32, complete-profile 38/38, report 40/40, expert login 36/36. Locally: jest 54/54, build 5/5, typecheck clean on all four apps. Co-Authored-By: Claude Opus 5 --- DELIVERY.md | 169 ++++++++++++++++++++++++++++++++++++++++----- docker-compose.yml | 4 +- 2 files changed, 153 insertions(+), 20 deletions(-) diff --git a/DELIVERY.md b/DELIVERY.md index aafef94..d063d65 100644 --- a/DELIVERY.md +++ b/DELIVERY.md @@ -8,25 +8,158 @@ Both merged with **no conflicts** (`phase-1-admin` already contained --- -## ⚠ Before the real release — do these three things +## 0. Quick start — run these in order -1. **Set `JWT_CUSTOMER_SECRET` to its own random value.** This one is a real - security boundary, not a default. If it is unset, the API signs customer and - sales-expert **app** tokens with `JWT_SECRET` — *the same key admin tokens - use* — so the two token realms stop being cryptographically separate. The - only thing still keeping an app token off the admin routes is the `typ` - check in `jwt.strategy.ts`, and a sales expert's app token carries their - **AdminUser id** in `sub`. Two independent defences become one. The API - prints a loud banner at boot while it is missing. Tracked as **REL-03 - (Critical)**. -2. **Remove `OTP_MASTER_CODE` from the environment.** While it is set, **any - active customer can sign in with that one fixed code**. Unset or empty - disables it completely, and the API prints a warning banner at boot for as - long as it is armed. Tracked as **REL-01 (Critical)** in - `docs/parity/ISSUES.md`. -3. **Configure the SMS credentials** (`SMS_TREZ_*`). Until then no customer can - receive a real login code — messages are written to the log instead of sent. - Tracked as **REL-02**. +Copy-paste, top to bottom. Nothing here needs a decision. + +### Step 1 — fill in the environment + +Create `.env` in the repo root. These **must** have a value: + +```dotenv +# Database +POSTGRES_USER=postgres +POSTGRES_PASSWORD= +POSTGRES_DB=goldbank +DATABASE_URL=postgresql://postgres:@db:5432/goldbank?schema=public + +# Auth — two DIFFERENT random values. Generate each with: +# node -e "console.log(require('crypto').randomBytes(48).toString('base64url'))" +JWT_SECRET= +JWT_CUSTOMER_SECRET= +``` + +Inside Docker the database host is `db`, not `localhost`. + +Everything else is optional and may be left blank. Two you may want now: + +```dotenv +# Delivery only — a fixed OTP that signs in ANY active customer. +# Exactly 4 digits, or it cannot be typed into the apps. REMOVE before the +# real release (see "Before the real release" below). +OTP_MASTER_CODE=1379 + +# Leave blank and SMS is logged, not sent. The server still starts. +SMS_TREZ_USERNAME= +SMS_TREZ_PASSWORD= +SMS_TREZ_SENDER_LINE= +``` + +### Step 2 — build + +```bash +pnpm install +pnpm build:release +``` + +**Do not skip `pnpm build:release`, and never run `docker compose build` +without it first.** The API image copies a bundle built here on the machine; it +does not build inside the image. Skipping this step produces an image from +**old code** and still reports success. This is real — it happened during this +delivery's own test run, with a bundle four days stale. (DEPLOY-03) + +### Step 3 — start + +```bash +docker compose up -d --build +``` + +Database migrations and seeding run **automatically inside the API container** +at boot. There is no manual migration step. Never run `prisma migrate reset`. + +### Step 4 — check it came up + +```bash +docker compose ps # db healthy, api up +docker compose logs api | tail -40 +``` + +In the log you should see `Nest application successfully started`, and +`Applied migrations: …` (first run) or `No pending migrations` (later runs). + +An `OTP_MASTER_CODE IS ACTIVE` banner is **expected** while that variable is +set — it is a reminder, not an error, and it disappears once you remove the +variable before the real release. + +A `JWT_CUSTOMER_SECRET IS NOT SET` banner means **Step 1 was not done**. That +one is a real security problem, not a warning to ignore — go back and set it. + +### Step 5 — prove the whole flow works + +```bash +# Publishes the API on host port 3100 so the test can reach it. +docker compose -f docker-compose.yml -f docker-compose.delivery-test.yml up -d api +node apps/api/scripts/test-compose-e2e.cjs http://localhost:3100/api +``` + +Expect **32 passed, 0 failed**. It signs a sales expert and an ordinary +customer into the app, has both place an order, confirms both orders appear in +the admin panel, and re-checks the token boundaries — all against the real +container. + +`docker-compose.delivery-test.yml` exists **only** for that check. The normal +deployment uses `docker-compose.yml` alone, where the API has no host port and +Dokploy/Traefik routes to it by domain. + +--- + +## ⚠ Before the real release + +Tonight's build is a **delivery build**. These are the things that must change +before it is a production release. The first three are blocking. + +### A. Configuration — blocking + +| # | Do this | Why it matters | +|---|---|---| +| **1** | **Set `JWT_CUSTOMER_SECRET` to its own random value**, different from `JWT_SECRET`. | Not a cosmetic default. Unset, the API signs customer and sales-expert **app** tokens with `JWT_SECRET` — *the same key admin tokens use* — and the two token realms stop being cryptographically separate. The only remaining barrier is the `typ` check in `jwt.strategy.ts`, and a sales expert's app token carries their **AdminUser id** in `sub`. Two independent defences collapse into one. A loud boot banner fires while it is missing. **REL-03 (Critical).** | +| **2** | **Remove `OTP_MASTER_CODE` from the environment.** | While set, **any active customer signs in with that one fixed code**. Unset or empty disables it completely; the API prints a banner at boot for as long as it is armed. **REL-01 (Critical).** | +| **3** | **Set real `JWT_SECRET`.** | Without it the API falls back to a well-known development string, so anyone can forge an admin token. | +| **4** | **Configure the SMS credentials** (`SMS_TREZ_USERNAME`, `SMS_TREZ_PASSWORD`, `SMS_TREZ_SENDER_LINE`). | Until then **no customer can receive a real login code** — messages are written to the log instead of sent. The server still boots (D20). **REL-02.** | + +After changing any of these: `docker compose up -d --build` and confirm the +`OTP_MASTER_CODE` banner is **gone** from `docker compose logs api`. + +### B. Credentials to rotate — the reference leaked them + +These are committed in plaintext in `henza_server` and must be assumed +compromised by anyone who has ever had repo access. None were copied into this +repository, but they are **the same live accounts**. + +- **SMS panel** username + password + sender line (`app/Models/Sms.php:73-77`) — **SEC-12 (Critical)** +- PayPing bearer token, Tahesab bearer token + IP allowlist, wallet hash pepper — **SEC-06** + +### C. Parity work not finished + +Phase 1 stopped after the Persons section when delivery mode began (D19). The +**Phase 1 gate still stands** as the next milestone. + +**Admin — not yet audited field-by-field.** Each has had only the breakage scan +(`scan-admin-b2b.cjs`, 34 endpoints, 0 broken), not a parity audit: + +- **Financial documents** (اسناد) — the money path; highest priority of the three +- **Users & permissions** (کاربران و دسترسی‌ها) — includes **PERM-01**, still + open: the database holds **178** distinct permission ids against the UI's + **174**. Unresolved because fixing it means **deleting rows**, which needs + your approval. +- **Reports** (گزارش‌ها) +- Also untouched: products/categories/variants, SMS templates, promotions, + cities, branches, workshops. + +**Apps — known gaps.** + +| ID | What | +|---|---| +| **MON-08** | `wagePercent`, `discountPercent` and `flatRialCharge` are taken from the **client** on order creation, so a caller can set their own making charge. `rashWeightGrams` is correctly read from the variant. The reference is *worse* (it trusts client `rash_weight` too), so this is already a partial improvement — but it is still a money path trusting client input. **Fix this first in Phase 2.** | +| **STUB-05** | Admin SMS recipients are hardcoded mobile numbers scattered across six places in the reference. Port as `SMS_ADMIN_RECIPIENTS` (comma-separated), the way `PROFILE_ADMIN_MOBILES` already works — not by copying the literals. Staff changes should not need a deploy. | +| **STUB-04** | Live gold price is **pull-on-mount only** — no socket, no polling. `quotedAt` renders as a Jalali **date**, so a stale rate looks current. | +| **STUB-03** | All list filtering and paging is **client-side** over one flat `GET /catalog/products`; no server `page`/`limit`. | +| **Analytics** | The reference PWA calls `useCustomAnalytics` on every route change (`henza_pwa/src/router/index.ts:249`); neither target app has any analytics. Deliberate — it needs a provider decision from you, not a port. | + +**`docs/parity/ISSUES.md`** holds the full list with evidence and file +references — currently **40+ entries**, including everything above plus the +reference bugs deliberately not ported (SEC-14, SEC-16, APP-03, and the two +D21 items). Phase 3 is the pass over that file. --- diff --git a/docker-compose.yml b/docker-compose.yml index ca275f4..6b8cda9 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,5 +1,5 @@ -version: '3.8' - +# No `version:` key — it is obsolete in Compose v2 and only prints a warning on +# every command, which made the delivery checklist's output noisy. services: db: image: docker.arvancloud.ir/postgres:15-alpine