Files
tehran_taj/docs/parity/ISSUES.md
T
s.esmaeiliandClaude Opus 5 fdf833565f 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>
2026-09-22 00:43:11 +03:30

15 KiB
Raw Blame History

ISSUES — logged, NOT fixed during parity

Anything found during the parity audit that is a bug, risk, smell or improvement opportunity goes here. Do not fix these while parity work is in progress (see CLAUDE.md §3). They are worked through in Phase 3, only when the user asks.

Severity: Critical (money loss / security / data loss) · High · Medium · Low

Paths are relative to the workspace root (gold bank/). "REF" = reference project (read-only). "TGT" = gold-banking-monorepo. Items marked [replicated] exist in the reference and were faithfully ported — fixing them is a product decision, not a parity fix.

Security & auth

ID Severity Where Description Evidence Suggested fix
SEC-01 Critical REF henza_server/routes/admin.php:12,272 The whole admin route file is unauthenticated — the Route::middleware('auth:api')->group(...) wrapper is commented out at both ends, leaving all ~215 admin routes public (incl. wallet/charge, users, roles). Verified by reading both lines. n/a in reference (read-only). Drives TGT decision below.
SEC-02 Critical TGT apps/api/src/modules/accounting/accounting.controller.ts:7, orders/orders.controller.ts:7, customers/customers.controller.ts:5, catalog/catalog.controller.ts:5, pricing/pricing.controller.ts:5 15 routes carry @Public(), including ledger writes (POST accounting/repayments/*, entries/:id/confirm) and POST /api/orders + PATCH /api/orders/:id/status. The public status DTO accepts a client-supplied actorAdminUserId, so a caller can attribute an action to any admin. grep -rn "@Public()"; source comments say "pre-existing unauthenticated endpoint". Introduce customer-session auth for the app-facing routes; move ledger/status writes behind the admin guard. [replicated] from SEC-01.
SEC-03 Critical TGT apps/user-app/src/views/user/LoginView.vue, apps/user-app-v2/ same OTP is not verified — any 4-digit code logs the user in. This is a parity gap: the reference does verify it (ApiMobileController.php:51-57, mismatch → HTTP 422). DECISIONS.md D6 FIXING — D6 (plus expiry + attempt limits, which exceed the reference)
SEC-08 Critical MITIGATED — hotfix stage 1 TGT apps/api/src/modules/customers/customers.service.ts:6-23,44-50 + customers.controller.ts:10 (@Public()) Full customer book exposed to anonymous callers. Login downloads GET /customers and matches the phone client-side, so the endpoint returns every active customer with fullName, mobile, nationalCode, goldDebtGrams, fiatDebtRial, goldCreditLimitGrams, and the URLs of national-ID / business-licence / shop scans (and GET uploads/:filename is public too, so those URLs resolve). passwordHash is correctly excluded. Not possible in the reference, where userVerfiy returned one customer after the code matched. Read customers.service.ts:5-23; DECISIONS.md D8 ✅ Stage 1: narrowed to id/mobile/isActive/isArchived. National ID, debts, credit limit and document URLs no longer leave the API. Route closes in stage 2 with OTP.
SEC-09 Critical FIXED — hotfix stage 1 TGT apps/api/src/modules/accounting/accounting.controller.ts:7 @Public() on the whole accounting controller exposes 5 routes, 4 of them writes: reconcile a balance, record a gold repayment, record a rial repayment, confirm a ledger entry. Faithful to the reference (whose admin routes were all unauthenticated) but contradicts the confirmed decision D1. DECISIONS.md §"@Public audit" group A ✅ Done: rids wallet.list, wallet.charge, document.create ×2, document.list.confirm. Verified 401/403/pass-through by apps/api/scripts/auth-smoke.sh.
SEC-10 High FIXED — hotfix stage 1 TGT apps/api/src/modules/orders/orders.controller.ts:22 PATCH orders/:id/status is public and its DTO accepts a client-supplied actorAdminUserId, so an anonymous caller can move an order's status and attribute it to any admin. In the reference this was an admin operation. DECISIONS.md §"@Public audit" B3 ✅ Done: order.list.sts.submit. Had no caller — the admin panel uses admin/orders/status.
SEC-04 High TGT apps/api (two files) JWT_SECRET falls back to 'dev-only-insecure-secret' and is absent from .env.example. enableCors() is wide open. Reported by api inventory; see _inv-tgt-api.md §11. Require the env var at boot; restrict CORS origins.
SEC-05 High TGT apps/api/prisma/seed.ts Seed creates a SUPER_ADMIN (09128081392) whose password equals the phone number, holding all 144 rids. Marked DEV-ONLY in comments but lives in the normal seed. docs/CHANGELOG-PORT-HISTORY.md §4 documents this as intentional for dev. Gate behind an explicit env flag so it can never run outside dev.
SEC-06 High REF henza_server Hardcoded secrets committed to the repo: SMS panel password, PayPing bearer token, Tahesab bearer + IP allowlist, wallet hash pepper ("yhoenszeaf"). See _inv-ref-server.md §11.4. Rotate all of them before any deployment that reuses these credentials.
SEC-07 Medium TGT apps/api/src/modules/admin-customers/admin-customers.service.ts setRoles stores arbitrary strings as rids with no validation against ALL_RIDS. api inventory §9. Validate against the rid catalogue.

Customer / person module (found during the user-requested deep check)

These are parity defects being fixed, not deferred items — see DECISIONS.md D0, D9, D10, D11. Listed here so the trail is complete.

ID Severity Where Description Evidence Status
CUST-01 Critical TGT apps/api/src/modules/admin-customers/admin-customers.service.ts:231-233 First and last name collapse. The reference stores fname/lname as two columns; the target merged them into fullName and returns fname: c.fullName, lname: ''. Opening any edit form puts the whole name in the first-name box and blanks the surname; saving then persists that. singleCustomer.vue tries to compensate with split(' '), which mis-splits multi-part names. REF database/migrations/2020_10_01_120430_customers_table.php:18-19 (two columns) vs TGT :231-233. Reported by the user from live use. FIXING — D0
CUST-02 Critical TGT same file :245, :600-658 percent read from the wrong entity and never written. Target returns c.referredByAgent?.wagePercent; the reference reads the customer's own percent column. update() ignores data.percent entirely, so editing the field does nothing. The create-time default (3) and the admin-path required rule are both missing, as is the app-side filterPercentPay offset. REF database/migrations/2020_10_19_153228_customer_add.php:32; AdminController.php:338,354; ApiMobileController.php:391-396,76-77. Full trace in DECISIONS.md §percent. FIXING — D9
CUST-03 High TGT apps/admin-panel/src/views/apps/customer/ReagentCustomer.vue:97-110 Sales-expert form is missing 5 reference fields: ncode, phone, city_id, gram_level, reagent_id. The reference form had 15; the target has 8. Likely because AdminUser lacks those columns. REF henza_admin/src/views/apps/customer/ReagentCustomer.vue (15 v-decorator fields) vs TGT form object. INVESTIGATING — D10
CUST-04 Low TGT apps/admin-panel/src/views/apps/customer/singleCustomer.vue:91 Still spreads the whole form on save while the other two customer forms send a diff. Harmless today (4 fields, all repopulated) but the same shape that caused the earlier data loss. read at :80-106 FIXING — D11
CUST-05 Low TGT apps/admin-panel/src/views/apps/customer/bulkCustomer.vue Diff-send on edit is a deliberate deviation from the reference, which posts all 15 fields on every save. Recorded so it is not "fixed" back. DECISIONS.md D11 accepted deviation

Money, stock & data integrity

ID Severity Where Description Evidence Suggested fix
MON-01 Critical TGT apps/user-app-v2/src/stores/cartStore.ts v2 has no capQty / maxQty stock ceiling, which user-app does have (apps/user-app/src/stores/cartStore.ts:62,65; components/product/CartItem.vue:82-83). A v2 user can order more units than exist. Verified by grep across both apps — zero hits in v2. Port the ceiling to v2 (this is also a Phase 2 parity gap, row to be added to 02-APPS-MATRIX.md).
MON-02 High REF henza_server wallet package The wallet balance column mixes grams and toman in one field, and money operations are not wrapped in DB transactions. _inv-ref-server.md §7. Separate the units; wrap mutations in transactions.
MON-03 High TGT apps/api/src/modules/accounting/ledger.service.ts:189 GRAMS_PER_MESGHAL = 4.331802 is a JS float used inside an otherwise-Prisma.Decimal CASH repayment formula, so the divisor is pre-rounded before the Decimal division. Read at ledger.service.ts:182-190; the source comment already flags the formula for stakeholder review. Make the constant a Decimal (or a string literal passed to Decimal).
MON-04 High TGT apps/api/src/modules/orders/orders.service.ts Debt is recognised at DELIVERED_TO_AGENT, but the idempotency guard checks deliveredAt, which is only set by DELIVERED_TO_CUSTOMER — so the debt trigger is not reliably idempotent. There is also no status-transition validation anywhere. api inventory §8. Guard on the same field the trigger fires on; add an explicit transition table.
MON-05 Medium TGT apps/api canPlaceOrders and verificationStep are stored and toggled by the admin panel but never enforced on any order path. api inventory §8. Enforce at order creation, or remove the toggles.
MON-06 Medium TGT apps/api Setting rows are never seeded — wallet.config, wallet.birthday, promotion.auto, mobile_main_slider only materialise on first write, so a fresh install has undefined behaviour until an admin saves each one. api inventory §7. Seed the defaults.
MON-07 Medium REF henza_server/app/Models/ProductVariant.php:183-190 vs SingleCustomerOrders.php:67, Order.php:66 Two divergent order-status enums coexist (0–6 and 1–7) for what is conceptually one flow. Read both models. Unify, or document the mapping explicitly.

Validation & contracts

ID Severity Where Description Evidence Suggested fix
VAL-01 High REF henza_server Zero FormRequests; all validation is inline, and setOrder, wallet charge, setConst and file upload have no validation at all. The wallet package's own validator is dead code (it calls failed() instead of fails()). _inv-ref-server.md §12. n/a in reference; informs how much validation the target must add.
VAL-02 High TGT apps/api Only 8 DTO classes exist; most admin request bodies are untyped Record<string, unknown>, so class-validator never runs on them. api inventory §5. Add DTOs for the admin write paths.
VAL-03 Medium TGT both user apps HTTP client is a bare fetch wrapper: no interceptors, no Authorization header, no refresh, no 401 handling, no error toasts. Non-2xx throws a generic Error; call sites mostly console.log(e). Order-submit failure is silent to the user. apps/user-app/src/api/client.ts, apps/user-app-v2/src/api/client.ts:3. Add a shared client with error surfacing; compare against henza_pwa/src/service/* in Phase 2.

Parity-relevant divergence between user-app and user-app-v2

CLAUDE.md §2: the only allowed difference is visual design. These are logic differences, so they are Phase 2 parity gaps as well as issues.

ID Severity Where Description Evidence
V2-01 Critical v2 missing stock ceiling see MON-01 grep
V2-02 Medium v2 missing getHomeSlider() / GET /home/slider admin banner v1 api/client.ts:152-170 defines AdminSlide + getHomeSlider; v2 has neither diff apps/user-app/src/api/client.ts apps/user-app-v2/src/api/client.ts
V2-03 Medium v2 missing per-variant images v1 defines ProductImageSummary (id/url/sortOrder/role) and imageId/image on the variant; v2's images is just Array<{url:string}> same diff
V2-04 Medium v2 missing orderable-first variant defaulting v1 composables/useProduct.ts:26 picks the first orderable variant; v2 has no equivalent grep
V2-05 Low v2 missing variant-chip preview cap + per-chip stock badges v1 components/product/SingleProduct.vue:388-392 grep

Dead code & maintenance

ID Severity Where Description
DEAD-01 Low TGT apps/user-app 8 components with no importers, loadingStore, api.getBalance (zero call sites), unused gsap dependency.
DEAD-02 Low TGT apps/user-app-v2 11 dead components, loadingStore, getBalance, priceUnit/priceUnitRialLess, 12 of 15 validators, unused gsap + @ant-design/icons-vue deps.
DEAD-03 Low TGT gold-banking-monorepo/user-app-v2/ (repo root, outside apps/) Stray leftover folder containing only package.json and an older vite.config.ts (hardcoded port 5179, hardcoded proxy, imports a themeConfig that doesn't exist there). Not matched by pnpm-workspace.yaml, so pnpm ignores it. Left in place — deleting it needs user approval.
DEAD-04 Low REF henza_server/app/Console/Kernel.php schedule() is empty — no cron. Price refresh / expiry / birthday logic must be driven by OS cron or the GET / web route.
DEAD-05 Low REF henza_server PayPing returnUrl points at site/transaction/verify, which is not registered in any route file — the payment callback is unreachable.

Known stubs in the target (not bugs, but not parity either)

ID Where Description
STUB-01 TGT both user apps Registration persists nothing; favourites are in-memory only (lost on reload); notifications is a static empty state; home slides are 3 hardcoded placeholders in v2.
STUB-02 TGT apps/api No OTP, no SMS provider, no payment gateway/callbacks, no cron, no websockets. SMS and notifications are DB-persistence only.
STUB-03 TGT both user apps All list filtering and "pagination" is client-side over one flat GET /catalog/products; no server page/limit params. "Pagination" is a 20-item IntersectionObserver window.
STUB-04 TGT both user apps Live price is pull-on-mount only (GET /pricing/latest?karatPerMille=750) — no socket, no SSE, no polling interval. quotedAt renders as a Jalali date only, so a stale rate looks current.