Files
tehran_taj/docs/parity/PROGRESS.md
T
s.esmaeiliandClaude Opus 5 4efe543d62 parity(api,apps): security hotfix stage 2 - real customer sessions, close IDOR
Stage 1 narrowed GET /customers but did not close the leak: the response still
carried every customer id, and /customers/:id, /:id/orders and
/:id/ledger-entries were still public, so the full customer book was still two
requests away. The user caught this. Stage 2 deletes the id-bearing surface.

Customer JWT, separate from the admin one (T1):
  - new src/modules/customer-auth/ with its own passport strategy,
    CustomerJwtGuard, @CustomerAuth() and @CurrentCustomer()
  - signed with JWT_CUSTOMER_SECRET, and every token carries typ:'customer'
    which the strategy verifies; both checks are present deliberately
  - customer state re-read per request, so deactivating takes effect at once
  - mirrors the reference's own split: auth:api vs auth('api-customer')

Real OTP (D6), porting ApiMobileController::userAuth/userVerfiy (:39-64):
  - new CustomerOtp table, additive migration, nothing existing touched
  - parity behaviour: rand(1000,9999), active-customer filter, faTOen digit
    normalisation on both number and code, rejection on mismatch
  - approved improvements over the reference: bcrypt-hashed code, 2-minute
    expiry, 5 attempts then lockout, single use, send throttle (60s per
    number, 5/number and 20/IP per 10 min), and no number enumeration

Routes removed, replaced by token-derived equivalents:
  GET customers, customers/:id, /:id/orders, /:id/ledger-entries
    -> GET customer/me, customer/me/orders, customer/me/ledger-entries
No route accepts a customer id any more, so the IDOR is closed structurally
rather than by a check. The admin panel was unaffected (admin/customers-omde/*).

Both apps migrated in the same commit per the v1/v2 parity rule: token-based
session store, Bearer header, 401 -> clear session and route to login, real
two-step OTP login. Each app keeps its own markup and styling; only logic moved.

Tests: auth-smoke.sh extended with the stage-2 assertions, 46 passed 0 failed
against a live server. jest installed dev-only (user-approved) so pnpm test
works for the first time: 7 passed covering faToEn, which moved to its own
digits.ts to be testable without booting Nest. Typecheck clean on api,
user-app and user-app-v2; both apps build.

[ref: henza_server ApiMobileController.php:39-78]

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 01:20:00 +03:30

141 lines
8.0 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# PROGRESS — Reference Parity Project
> Read this file FIRST at the start of every session and after every `/compact`.
> Rules live in `CLAUDE.md` (workspace root). Method lives in the `reference-parity-audit` skill.
## Current state
| Field | Value |
|---|---|
| **Phase** | Security hotfix — **stages 1 and 2 ✅ complete** |
| **Branch** | `parity/security-hotfix` |
| **Status** | Both stages done and verified live: **46/46** smoke assertions, **7/7** jest unit tests, typecheck + build clean on all three projects. |
| **Next step** | Show the user the **D0 (firstName/lastName)** and **`percent`** migrations for approval, then start `01-ADMIN-MATRIX.md` field-by-field (D12) |
| **Blocked on** | user approval of those two migrations before they run |
| **Last updated** | 2026-09-22 |
### Security hotfix — final result
| Route | Was | Now |
|---|---|---|
| `GET accounting/customers/:id/balance` | public | admin, rid `wallet.list` |
| `POST accounting/customers/:id/reconcile` | public **write** | admin, rid `wallet.charge` |
| `POST accounting/repayments/gold` | public **write** | admin, rid `document.create` |
| `POST accounting/repayments/rial` | public **write** | admin, rid `document.create` |
| `POST accounting/entries/:id/confirm` | public **write** | admin, rid `document.list.confirm` |
| `PATCH orders/:id/status` | public **write** | admin, rid `order.list.sts.submit` |
| `GET customers` | every customer, 18 fields | **route deleted** |
| `GET customers/:id` | public | **route deleted** → `GET customer/me` (token) |
| `GET customers/:id/orders` | public | **route deleted** → `GET customer/me/orders` (token) |
| `GET customers/:id/ledger-entries` | public | **route deleted** → `GET customer/me/ledger-entries` (token) |
| — | — | **new** `POST customer/auth/request-otp` · `verify-otp` |
Still public by design: `catalog/products`, `pricing/latest`, `home/slider`,
`uploads/:filename` (ruling T2), and `POST /orders` + `GET /orders/:id` — the
last two are logged as SEC-02/B1-B2 and want a customer-token pass of their own.
**Tests**
- `apps/api/scripts/auth-smoke.sh` (live server): **46 passed, 0 failed**.
Run it with `API=http://localhost:3000/api CODE_LOG=/path/to/api.log`.
- `pnpm test` in `apps/api` (jest, newly installed): **7 passed**.
- Typecheck: api ✅ · user-app ✅ · user-app-v2 ✅. Both apps build ✅.
### ⚠️ Correction worth remembering
Stage 1 reported the customer-book leak as "mitigated" after narrowing the
`GET /customers` field list. That was wrong: the response still returned every
customer **id**, and the three `/customers/:id*` routes were still public, so the
full data was still two requests away. The user caught it. Stage 2 deleted the
whole id-bearing surface instead. **Narrowing a response is not closing a leak
while the id and the id-addressed routes remain.**
## ⚠️ Read `DECISIONS.md` before doing anything
All user rulings live there (D0–D12). Highlights that change how work is done:
- **D12 — the matrix is FIELD-BY-FIELD, not page-by-page**, and a 4-step
round-trip test per entity is mandatory. Both rules were added to the
`reference-parity-audit` skill (Step C rule C0, Step E rule E0).
- **D0** — split `fullName` into `firstName`/`lastName` via an additive migration;
**show the migration and get approval before running it**.
- **D4** — never port broken behaviour, but match calculations/rounding/messages
exactly; annotate each non-replication «عمداً تکرار نشد — ISSUE #n».
- **D2** — target-only features get status ➕ and are left alone.
## Phase checklist
- [x] P0.1 — All 3 reference folders readable (`henza_server`, `henza_admin`, `henza_pwa`)
- [x] P0.2 — Branch `parity/phase-0` + `docs/parity/` created
- [x] P0.3 — Inventory all 7 projects → `_inv-*.md` (8111 lines total) → consolidated in `00-INVENTORY.md`
- [x] P0.4 — Mapping tables, missing list, extra list (`00-INVENTORY.md` §2–4)
- [x] P0.5 — Draft `PROTECTED.md`
- [x] **P0.6 — GATE passed** — `PROTECTED.md` confirmed, D0–D12 recorded
- [x] P0.7 — Urgent traces delivered: `percent`, `@Public` list, login leak
- [x] P0.8 — Skill updated with rules C0 (field-by-field) and E0 (round-trip test)
- [ ] **P1.0 — user approves: 5 ledger routes, `percent` column, D0 migration** ← HERE
- [ ] Phase 1 — Admin parity (branch `parity/phase-1-admin`, `01-ADMIN-MATRIX.md`)
- [ ] **GATE:** "Admin parity is complete and tested. Shall we move on to the apps (user-app & user-app-v2)?"
- [ ] Phase 2 — Apps parity (`02-APPS-MATRIX.md`, both user apps)
- [ ] Phase 3 — Work through `ISSUES.md` (only when user asks)
## Answered — see `DECISIONS.md` for the full text
| # | Ruling |
|---|---|
| D0 | Split `fullName` → `firstName`/`lastName`, additive migration, backfill, sync, report multi-token rows. Approval needed before running. |
| D1 | Target's stricter auth stands. Build the "reference operation ↔ rid" matrix; explain 172 vs 174. |
| D2 | Audit only what the reference has. Target-only features → ➕, untouched. |
| D3 | Build the 5 missing screens in Phase 2, both apps, plus analytics, `isLoaded`/`sts_verify_step_two` guards, guest toast+home, and v2's cart ceiling. |
| D4 | Don't port breakage; do match calculations/rounding/messages. Unit-test the price formula. |
| D5 | Leave the stray folder; delete last, own commit. |
| D6 | **OTP must verify** — parity gap, fix it (plus expiry + attempt limits, which exceed the reference). |
| D7 | `@Public` ledger routes: list delivered, awaiting approval to protect. |
| D8 | Login leak: **target**, full customer book incl. national IDs and debts. |
| D9 | `percent`: trace delivered — **the target's implementation is wrong**. |
| D10 | Sales expert: keep `AdminUser`; investigate the 5 missing fields; propose `SalesExpertProfile`. |
| D11 | `singleCustomer.vue` → diff-send. |
| D12 | **Field-by-field matrix + mandatory round-trip test.** Added to the skill. |
## Open — awaiting user approval
1. **Protect the 5 `@Public` ledger-write routes** (`DECISIONS.md` group A).
2. **`percent` schema change** — add `makingChargeOffsetPercent` to `Customer`.
3. **D0 migration file** — must be shown and approved before it runs.
## Key context (survives context loss)
- **Domain correction:** the reference is a **gold-jewellery B2B/B2C e-commerce**
system, not a gold bank. No buy/sell order book, no spread, no deposit/withdraw,
no P2P transfer, no KYC levels, no daily limits (only `gram_level`), no payment
callback route, no cron, no websockets. See `00-INVENTORY.md` §0.
- **The gold price formula** (Phase 1 row #1) —
`henza_server/app/Models/ProductVariant.php:183-190`: `+extra%` → `+7%` margin →
`+9%` VAT **on the non-metal portion only** → `×grams` → `floor(/1000)*1000`.
Three superseded commented-out variants above it apply VAT to the whole price —
do **not** port those.
- **A large prior port already happened** — see `docs/ADMIN-PORT-PLAN.md` and
`docs/CHANGELOG-PORT-HISTORY.md` (80 views, 25 Nest modules, 29 Prisma models,
154 admin routes, 144/144 rids). Prior port notes **no view was ever visually
verified in a browser**.
- **Stack changed across the port:** Laravel→NestJS/Prisma, Vue2/Vuesax→Vue3/antd.
Port behaviour, not code style.
- **Reference scale is inflated:** 541 admin `.vue` files but only ~69 reachable
(rest are Vuexy template demos); 9 reference router files are dead (not imported);
~28 legacy Django-era DB tables have no migrations.
- **v1 ⇄ v2 logic gaps** (5, all v2 behind v1) are parity work, not design:
`ISSUES.md` MON-01, V2-01..05.
## Inventory files (Phase 0 working files)
| File | Scope | Lines |
|---|---|---|
| `_inv-ref-server.md` | henza_server | 1313 |
| `_inv-tgt-api.md` | apps/api | 1179 |
| `_inv-ref-admin.md` | henza_admin | 1705 |
| `_inv-tgt-admin.md` | apps/admin-panel | 1261 |
| `_inv-ref-pwa.md` | henza_pwa | 1178 |
| `_inv-tgt-userapp.md` | apps/user-app | 612 |
| `_inv-tgt-userappv2.md` | apps/user-app-v2 | 700 |
Consolidated into `00-INVENTORY.md`. Issues found: `ISSUES.md` (26 entries).