- 03 Sep, 2026 5 commits
-
-
Helmet's default frameguard sends X-Frame-Options: SAMEORIGIN on every response. Megapay/the issuing bank embeds /admin/pay/:merTrxId and /admin/epay/return in an iframe as part of the OTP/3DS redirect flow; same-origin-only framing made Chrome show a blank frame with "<domain> đã từ chối kết nối" even though the payment succeeded server-side (IPN still processed correctly - this was a display-only issue). Scoped fix: strip the header only on these two payment-facing routes via a small route-local middleware. Every other /admin/* route (login, transactions, accounts) keeps the clickjacking protection unchanged. Co-Authored-By:
Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017KPzWwuTEeX2vXGXvyGn4q
tdgiang authored -
tdgiang authored
-
Co-Authored-By:
Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017KPzWwuTEeX2vXGXvyGn4q
tdgiang authored -
Correction to the earlier assumption (based on an ad-hoc test during brainstorming) that sale_status=4 meant "paid". The actual mapping is 1=giao dịch thành công, 4=giao dịch thất bại - confirmed directly by the user with real curl examples for both outcomes. SobanleClient.changeSaleStatus now takes the status as an explicit parameter instead of hardcoding 4. epayIPN now calls it for BOTH payment outcomes (previously only on success), sending 1 or 4 to match the real result - so a failed/refunded transaction's Sổ Bán Lẻ order also gets marked accordingly instead of being silently left at "chờ xử lý" forever. Co-Authored-By:
Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017KPzWwuTEeX2vXGXvyGn4q
tdgiang authored -
Replaces the strict >1,000,000/<20,000,000 range with an inclusive >=10,000/<=20,000,000 range, matching the actual usable range confirmed on the real Sổ Bán Lẻ catalog after the earlier per-line-qty fix. Co-Authored-By:
Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017KPzWwuTEeX2vXGXvyGn4q
tdgiang authored
-
- 02 Sep, 2026 3 commits
-
-
Per MGP_Merchant_Interface_V1.4.7VN.md section 5.3.2: resultCd "00_000" alone does not mean "payment success" - MGP can send it for a refund IPN too (status field: "0" = payment, "2" = refund). epayIPN previously set status="success" purely off resultCd, which would have misclassified a refund event on a still-pending transaction. Now requires resultCd "00_000" AND status "0". Also adds an explicit amount cross-check between the IPN body and the stored transaction (spec's "Lưu ý đặc biệt 2") as a defense-in-depth, clearly-diagnosable guard alongside the existing signature verification (which already implicitly enforces this, but with a generic error). Co-Authored-By:
Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017KPzWwuTEeX2vXGXvyGn4q
tdgiang authored -
Live E2E testing against the real Sổ Bán Lẻ catalog (64 usable products, avg price ~24k) showed the fixed maxQtyPerLine=3 capped the achievable order total at ~4.86M regardless of retry count - a structural ceiling, not bad luck. 7/10 test amounts above ~4.2M failed with NO_PRODUCT_COMBO_MATCH every time. Default per-line qty is now bounded only by the product's actual stock (previously min(3, qty), now just qty), raising the real ceiling to ~2B on the current catalog. maxQtyPerLine is still available as an explicit override for callers that want a cap. Verified against the real catalog: all 7 previously-failing amounts (4.2M-19.5M) now succeed, real order totals landing within the 50,000 tolerance band every time. Co-Authored-By:
Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017KPzWwuTEeX2vXGXvyGn4q
tdgiang authored -
Sổ Bán Lẻ rejects customer creation with a 422 ("Vui lòng nhập số CMND/CCCD") when id_card_number is null - discovered via manual E2E testing against the real service. Add a required customerIdCard field to the admin form and controller, persisted on AdminTransaction, and pass through as customer_data.id_card_number instead of null. Also add the requested amount validation: payment amount must be strictly greater than 1,000,000 and less than 20,000,000, enforced both client-side (form) and server-side (createTransaction). Co-Authored-By:Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017KPzWwuTEeX2vXGXvyGn4q
tdgiang authored
-
- 31 Aug, 2026 13 commits
-
-
Fix final-review findings: normalize order total, re-check tolerance against authoritative total, wrap HTTP errors, log orphan orders, add outbound timeout - SobanleClient.createSale: coerce grand_total to a rounded Number and reject non-finite/non-positive values before it crosses into the epay signature and the DB amount, so a decimal-string API response can't desync the signed value from the stored/transmitted one. - productComboPicker.computeLineTotal: coerce tax.rate with Number() instead of a strict typeof check, so a string rate is honored instead of silently defaulting to 0%. - SobanleClient.createAutoOrder: reject with SOBANLE_TOTAL_EXCEEDS_TARGET and log the order context when the POS's authoritative grand_total exceeds targetAmount — the spec's tolerance was only ever checked against the local estimate, never the real total. - SobanleClient.withAuthRetry: wrap non-2xx response bodies into real Error objects with a statusCode, instead of forwarding the raw body as err (previously every real HTTP failure logged `message: undefined` with no status code). - admin.server.controller.js createTransaction: log the Sổ Bán Lẻ sobanleOrderId/total when the follow-up AdminTransaction DB write fails, so an order left orphaned in the POS can be found later. - ApiRequest.js: add timeout: 15000 to the three *OtherUrlWithHeader functions so a hung Sổ Bán Lẻ call can't hang the admin's request indefinitely (also benefits the existing Appota caller). Co-Authored-By:
Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017KPzWwuTEeX2vXGXvyGn4q
tdgiang authored -
Co-Authored-By:
Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017KPzWwuTEeX2vXGXvyGn4q
tdgiang authored -
createTransaction now calls SobanleClient.createAutoOrder(customerData, targetAmount, cb) before writing an AdminTransaction. The transaction's amount is set from the real Sổ Bán Lẻ order total (order.orderTotal) rather than the admin's raw input amount, and sobanleOrderId is stored for reconciliation. On Sổ Bán Lẻ failure, returns 502 {code:"99", data:"SOBANLE_ORDER_FAILED"} with no DB row written. The success response contract ({code:"00", data:{merTrxId, paymentUrl}}) is unchanged. Co-Authored-By:Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017KPzWwuTEeX2vXGXvyGn4q
tdgiang authored -
Co-Authored-By:
Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017KPzWwuTEeX2vXGXvyGn4q
tdgiang authored -
Orchestrates product selection via productComboPicker and sale creation, failing gracefully if no product combo matches the target amount. Co-Authored-By:
Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017KPzWwuTEeX2vXGXvyGn4q
tdgiang authored -
tdgiang authored
-
Two rulings made during Task 4 execution (see ledger): the early-refresh headroom now scales with token TTL instead of a flat 60s (a flat skew made the test's short-TTL token already "expired" the instant it was cached), and every scratch script that requires config/config.js now sets NODE_ENV before requiring it (the app's real entrypoint always resolves this via config/init first; the plan's scratch scripts had bypassed that). Co-Authored-By:
Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017KPzWwuTEeX2vXGXvyGn4q
tdgiang authored -
tdgiang authored
-
Co-Authored-By:
Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017KPzWwuTEeX2vXGXvyGn4q
tdgiang authored -
tdgiang authored
-
tdgiang authored
-
Co-Authored-By:
Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017KPzWwuTEeX2vXGXvyGn4q
tdgiang authored -
Adds GET /admin index redirect, createdAt date-range filtering on the transactions list with a stat strip (total/success/pending/revenue) and client-side search, plus inline validation and quick amount chips on the new-transaction form. Co-Authored-By:
Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017KPzWwuTEeX2vXGXvyGn4q
tdgiang authored
-
- 30 Aug, 2026 4 commits
-
-
Trust the single nginx reverse-proxy hop (confirmed in deploy.md) so express-rate-limit's per-client bucketing on POST /admin/login sees the real client IP instead of nginx's. Also fix a dead cookieSecure check (NODE_ENV === 'secure' never matched) so the session cookie actually gets Secure in production, verified live with X-Forwarded-Proto: https. Co-Authored-By:Claude Sonnet 5 <noreply@anthropic.com>
tdgiang authored -
# Conflicts: # app/views/admin/transactions-list.server.view.html # app/views/admin/transactions-new.server.view.html
tdgiang authored -
Applies a cohesive visual system to the admin panel: a navy/blue dashboard style (Inter font) for the internal staff pages, and a navy/gold fintech-trust style (IBM Plex Sans) for the customer-facing payment pages. Preserves all existing form fields, MegaPay integration markup, and Swig template bindings unchanged.
tdgiang authored -
Async bcryptjs measured to still not yield the event loop for realistic hash costs (~70ms compares finish before its 100ms yield threshold), so a login flood could still stall the same process's live MegaPay payment webhooks. Caps each client (or shared-proxy-bucket, see known limitation below) to 5 POST /admin/login attempts per rolling 60s window via express-rate-limit; requests over the limit get a 429 with a Vietnamese error and never reach adminAuth.login, so the bcrypt compare never runs. Known limitation: this app has no app.set('trust proxy', ...) configured and runs behind a reverse proxy in production, so express-rate-limit's default req.ip-based bucketing will key off the proxy's address, not the real client IP. In production this enforces "5 attempts/minute in aggregate behind the proxy" rather than "5 per real client IP" — an accepted trade-off for this low-traffic internal tool, but not the same guarantee trust proxy + per-IP limiting would give. Configuring trust proxy is an infrastructure change, left out of scope here.tdgiang authored
-
- 29 Aug, 2026 13 commits
-
-
tdgiang authored
-
Co-Authored-By:Claude Sonnet 5 <noreply@anthropic.com>
tdgiang authored -
tdgiang authored
-
tdgiang authored
-
Appends accountsList, createAccount, toggleAccount, and resetAccountPassword to adminAuth.server.controller.js, adds the accounts-list view, and wires the /admin/accounts* routes behind requireLogin + requireAdmin. Co-Authored-By:Claude Sonnet 5 <noreply@anthropic.com>
tdgiang authored -
createTransaction now stamps createdByUsername from the session. listTransactions scopes results to the logged-in staff's own transactions, or (for admins) all transactions with an optional ?staff= filter sourced from real AdminUser records - the filter is ignored for non-admin sessions so staff cannot view another account's data by editing the query string. Transaction routes now use requireLogin instead of basicAuth, which is now unused and deleted. Co-Authored-By:Claude Sonnet 5 <noreply@anthropic.com>
tdgiang authored -
Co-Authored-By:Claude Sonnet 5 <noreply@anthropic.com>
tdgiang authored -
Adds AdminTransaction.createdByUsername (default null) so future permission-scoped views can attribute data to a user, plus a new app/libs/adminBootstrap.js that seeds the first admin account from ADMIN_USER/ADMIN_PASSWORD on boot and backfills any pre-existing transaction lacking createdByUsername to that admin. Wired into server.js's mongoose 'open' handler. Idempotent across repeated boots. Co-Authored-By:Claude Sonnet 5 <noreply@anthropic.com>
tdgiang authored -
Implement two authentication and authorization middlewares: - requireLogin: checks req.session.userId and responds with 302 redirect to /admin/login for HTML requests, or 401 JSON for API requests - requireAdmin: checks req.session.role === "admin" and responds with 403 for unauthorized access Co-Authored-By:Claude Sonnet 5 <noreply@anthropic.com>
tdgiang authored -
Add passwordHash utility module wrapping bcryptjs for secure password hashing, and AdminUser Mongoose model with username (unique, lowercase), passwordHash, role (admin/staff), and active fields. Co-Authored-By:Claude Sonnet 5 <noreply@anthropic.com>
tdgiang authored -
tdgiang authored
-
tdgiang authored
-
# Conflicts: # .gitignore
tdgiang authored
-
- 28 Aug, 2026 2 commits
-
-
tdgiang authored
-
Adds listTransactions controller action and its Swig view, and creates app/routes/admin.server.routes.js to finally wire every admin controller function (Tasks 7-9) to real HTTP routes via the app's glob-based route loader. basicAuth protects /admin/transactions and /admin/transactions/new; the pay page, epay return, and epay IPN endpoints stay public. Co-Authored-By:Claude Sonnet 5 <noreply@anthropic.com>
tdgiang authored
-