Commit Graph
7 Commits
Author SHA1 Message Date
s.esmaeiliandClaude Opus 5 f71fd5bfe1 feat(apps): sales-expert screens in both apps + v2 admin home slider (D21, APP-02)
Both expert screens are built in user-app and user-app-v2, with byte-identical
template and script and a per-app style block only — verified by diffing
everything above <style>:

  ChildOrdersView  "سفارش مشتریان"  — legacy ChildOrders.vue. Keeps the
    reference's title, search, infinite paging, and per-card item count, total
    weight, customer name and the fixed «تحویل مسئول فروش» chip (the endpoint
    only returns that status, ApiMobileController:766).
  CustomerChoiceView "لیست مشتریان" — legacy customerChoice.vue. Keeps its
    title, per-search re-query and the reference's choice to LIST inactive
    customers (its active() scope is commented out at :187).

Reachable from inside the app, not just by URL: routes carry the reference's own
paths and names, the dashboard grows the two entries only for an expert (v1 as
tiles, v2 as its own rows), and a router guard sends an ordinary customer back
to their dashboard. None of that is relied on for safety — the server scopes
both endpoints on the AdminUser id in the token and answers 403 otherwise.

Not ported, both recorded in D21: the act-as api_token minting (:197-201) and
the child-order edit drawer, which recomputed an order's money from client-sent
values. The card opens a read-only summary instead.

APP-02 fixed: user-app-v2 gained getHomeSlider and now prefers admin-configured
slides, keeping its category-derived hero as the fallback and its own design.

Session stores stay byte-identical across the apps.
build 5/5, jest 45/45, expert login 36/36, apps smoke 43/43, typecheck clean.

[ref: henza_pwa/src/views/user/src/components/ChildOrders.vue, customerChoice.vue]
[ref: henza_pwa/src/router/index.ts:171-188]

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-23 01:12:53 +03:30
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
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
s.esmaeili a743f6f93c update admin 2026-09-21 17:37:26 +03:30
s.esmaeili ef1d4638cc admin user and ui panel 2026-09-21 08:32:31 +03:30
s.esmaeiliandClaude Opus 5 f2d83c53df user-app: redesigned home screen and product detail
Design language introduced here and reused across both screens:
  - LESS colour tokens (@ink/@inkSoft/@inkMuted/@inkFaint, @wine/@wineSoft,
    @canvas/@canvasSoft, @hairline/@hairlineSoft) added to
    themeConfig/colors.json and injected globally via vite's modifyVars, so
    scoped <style lang="less"> blocks resolve them with no import
  - BEM-ish scoped class names, hairline-bordered cards at 14-18px radii,
    ink surfaces with a wine accent, dashed-border empty states
  - components/home/: SectionHead, ShowcaseCard, ShowcaseSlider, CategoryRail,
    RateStrip, AccountStrip

Data honesty is deliberate throughout: no rial price is printed for a piece,
because a finished item's price is the live gold rate x weight plus wage,
computed at order time and never stored on the product. Only genuinely
present fields are shown (weight, making-charge percent, stock, category,
brand). Categories and the hero are derived from the real catalogue, so an
empty catalogue renders an empty state rather than placeholder tiles.

Also fixes a real bug in the product detail view: it ran, at module scope,
history.pushState + window.onpopstate = () => history.go(1). Because the
drawer mounts its slot eagerly, that fired for every product card on the
home screen and permanently hijacked the back button for the session.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 23:05:29 +03:30
s.esmaeili 0ed6bed028 feat: initial commit 2026-09-09 06:46:27 +03:30