2026-09-21 18:09:46 +03:30
|
|
|
|
# CHANGELOG — parity changes
|
|
|
|
|
|
|
|
|
|
|
|
Every change made to `gold-banking-monorepo` during the parity project, with the
|
|
|
|
|
|
reason and the reference path that justifies it.
|
|
|
|
|
|
|
|
|
|
|
|
Format:
|
|
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
## <date> — <phase> — <group title>
|
|
|
|
|
|
- **What:** <the change>
|
|
|
|
|
|
- **Why:** <the parity gap it closes>
|
|
|
|
|
|
- **Reference:** <henza_* path:line>
|
|
|
|
|
|
- **Target:** <gold-banking-monorepo path:line>
|
|
|
|
|
|
- **Matrix row:** <id in 01-ADMIN-MATRIX.md / 02-APPS-MATRIX.md>
|
|
|
|
|
|
- **Commit:** <sha>
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 2026-09-21 — Phase 0 — Setup
|
|
|
|
|
|
|
|
|
|
|
|
- **What:** Created branch `parity/phase-0` and `docs/parity/` scaffolding
|
|
|
|
|
|
(`PROGRESS.md`, `ISSUES.md`, `CHANGELOG.md`).
|
|
|
|
|
|
- **Why:** Persistent state required by `CLAUDE.md` §6.
|
|
|
|
|
|
- **Reference:** n/a
|
|
|
|
|
|
- **Target:** `docs/parity/`
|
|
|
|
|
|
- **Matrix row:** n/a
|
|
|
|
|
|
|
|
|
|
|
|
_No source code changed in Phase 0 (read-only phase)._
|
2026-09-22 00:43:11 +03:30
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 2026-09-22 — Security hotfix, stage 1 — close the unauthenticated write paths
|
|
|
|
|
|
|
|
|
|
|
|
Branch `parity/security-hotfix`. Authorised by the user's rulings D7, T1–T3.
|
|
|
|
|
|
|
|
|
|
|
|
### 1. Ledger routes are admin-only (SEC-09)
|
|
|
|
|
|
|
|
|
|
|
|
- **What:** removed the controller-level `@Public()` from `AccountingController`
|
|
|
|
|
|
and gave each route a rid: `wallet.list` (balance), `wallet.charge`
|
|
|
|
|
|
(reconcile), `document.create` (both repayments), `document.list.confirm`
|
|
|
|
|
|
(confirm entry).
|
|
|
|
|
|
- **Why:** four of the five routes WRITE to the ledger and were reachable with no
|
|
|
|
|
|
token at all. Faithful to the reference, whose entire admin route file is
|
|
|
|
|
|
unauthenticated, but contrary to confirmed decision D1.
|
|
|
|
|
|
- **Reference:** `henza_server/routes/admin.php:12,272` (the `auth:api` wrapper is
|
|
|
|
|
|
commented out at both ends)
|
|
|
|
|
|
- **Target:** `apps/api/src/modules/accounting/accounting.controller.ts`
|
|
|
|
|
|
- **Breakage check:** `api.getBalance()` is the only app-side consumer and it has
|
|
|
|
|
|
**zero call sites** in either user app (dead code, already logged as DEAD-01/02).
|
|
|
|
|
|
|
|
|
|
|
|
### 2. Order status change is admin-only (SEC-10)
|
|
|
|
|
|
|
|
|
|
|
|
- **What:** moved `@Public()` from the class to the two customer-facing handlers,
|
|
|
|
|
|
leaving `PATCH orders/:id/status` behind `order.list.sts.submit`.
|
|
|
|
|
|
- **Why:** it let an anonymous caller move any order's status **and** attribute
|
|
|
|
|
|
the change to any admin through the DTO's `actorAdminUserId`.
|
|
|
|
|
|
- **Reference:** an admin operation; the admin panel posts to `admin/orders/status`
|
|
|
|
|
|
(`apps/admin-panel/src/api/endpoints.ts:42,48`), so this route had no caller.
|
|
|
|
|
|
- **Target:** `apps/api/src/modules/orders/orders.controller.ts`
|
|
|
|
|
|
- **Still public:** `POST /orders` and `GET /orders/:id`, because the target has
|
|
|
|
|
|
no customer session yet — closed in stage 2.
|
|
|
|
|
|
|
|
|
|
|
|
### 3. `GET /customers` no longer leaks the customer book (SEC-08)
|
|
|
|
|
|
|
|
|
|
|
|
- **What:** added `LOGIN_LOOKUP_SELECT` (`id`, `mobile`, `isActive`,
|
|
|
|
|
|
`isArchived`) and pointed `list()` at it. Removed `fullName`, `nationalCode`,
|
|
|
|
|
|
`goldDebtGrams`, `fiatDebtRial`, `goldCreditLimitGrams`, `verificationStep`,
|
|
|
|
|
|
`cityId`, `referredByAgentId`, `nationalIdImageUrl`, `businessLicenseUrl`,
|
|
|
|
|
|
`shopImageUrl`, `createdAt`, `updatedAt` from that response.
|
|
|
|
|
|
- **Why:** the route is unauthenticated because both apps match the phone number
|
|
|
|
|
|
in the browser, so it shipped every customer's national ID number, debts,
|
|
|
|
|
|
credit limit and identity-document URLs to any visitor.
|
|
|
|
|
|
- **Reference:** login was `POST userAuth` → SMS → `POST userVerfiy`, returning
|
|
|
|
|
|
exactly one customer after the code matched
|
|
|
|
|
|
(`henza_server/app/Http/Controllers/Api/ApiMobileController.php:39-78`).
|
|
|
|
|
|
- **Target:** `apps/api/src/modules/customers/customers.service.ts`,
|
|
|
|
|
|
plus `CustomerSummary` narrowed in **both** apps' `src/api/client.ts`
|
|
|
|
|
|
(parity rule: the same logic change in both).
|
|
|
|
|
|
- **Route survives** until stage 2 replaces the client-side phone match with OTP.
|
|
|
|
|
|
|
|
|
|
|
|
### 4. Test script
|
|
|
|
|
|
|
|
|
|
|
|
- **What:** added `apps/api/scripts/auth-smoke.sh` — asserts 401 without a token,
|
|
|
|
|
|
403 with a low-privilege token, and pass-through for a super admin, on every
|
|
|
|
|
|
closed route; plus that the public routes still answer 200 and that
|
|
|
|
|
|
`GET /customers` exposes only the four login fields.
|
|
|
|
|
|
- **Result:** **23 passed, 0 failed** against a live server and the dev seed.
|
|
|
|
|
|
|
|
|
|
|
|
### Not changed
|
|
|
|
|
|
|
|
|
|
|
|
`/uploads` — per ruling T2, uploaded files including identity documents stay
|
|
|
|
|
|
public. The filename check requested alongside it passed with no changes needed
|
|
|
|
|
|
(md5 of time + name + UUID; no directory listing).
|