parity(api,apps): security hotfix stage 1 - close unauthenticated write paths

Authorised by rulings D7 and T1-T3 in docs/parity/DECISIONS.md. This is the
CLAUDE.md section 3 carve-out: matching the reference here would keep a money
write path open to anonymous callers.

Closed, each behind the rid the legacy vocabulary already had:
  GET   accounting/customers/:id/balance    -> wallet.list
  POST  accounting/customers/:id/reconcile  -> wallet.charge
  POST  accounting/repayments/gold          -> document.create
  POST  accounting/repayments/rial          -> document.create
  POST  accounting/entries/:id/confirm      -> document.list.confirm
  PATCH orders/:id/status                   -> order.list.sts.submit

Four of the accounting routes WRITE to the ledger. The order-status route was
worse in kind: its DTO takes a client-supplied actorAdminUserId, so an
anonymous caller could move any order and pin it on any admin. It had no
caller - the admin panel posts to admin/orders/status.

GET /customers kept its public status (both apps' login still matches the
phone number in the browser) but now selects only id/mobile/isActive/
isArchived. It previously returned every active customer's full name, national
code, both debts, credit limit and the URLs of their identity-document scans.
CustomerSummary narrowed in BOTH user apps in the same commit, per the
v1/v2 parity rule.

Not changed: /uploads stays public per ruling T2. The filename check requested
alongside it passed unchanged - md5(time + name + uuid), no directory listing.

Tests: apps/api/scripts/auth-smoke.sh asserts 401 without a token, 403 with a
low-privilege token (ut 88, 5 unrelated rids) 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. 23 passed, 0 failed
against a live server. Typecheck clean on api, user-app and user-app-v2.

[ref: henza_server/routes/admin.php:12,272; ApiMobileController.php:39-78]

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
s.esmaeili
2026-09-22 00:43:11 +03:30
co-authored by Claude Opus 5
parent 004a3945c2
commit fdf833565f
11 changed files with 356 additions and 23 deletions
+11 -4
View File
@@ -70,13 +70,20 @@ export interface LedgerEntrySummary {
note: string | null;
}
/**
* What `GET /customers` returns — the login lookup, and nothing more.
*
* It used to carry fullName, national ID, both debts and the credit limit. The
* route is unauthenticated (the login screen matches the phone number in the
* browser), so that shipped the whole customer book to any visitor; the API now
* selects only these four fields. See SEC-08 in docs/parity/ISSUES.md.
*
* The route disappears altogether once OTP verify returns the one authenticated
* customer, the way the reference did it.
*/
export interface CustomerSummary {
id: string;
fullName: string;
mobile: string;
goldDebtGrams: string;
fiatDebtRial: string;
goldCreditLimitGrams: string;
isActive: boolean;
isArchived: boolean;
}