Midtrans-Middleware/server
Tengku Achmad 0936b0e84f fix: default LOG_EXPOSE_API to closed (false) when unset
parseEnable()'s generic fallback (return true for an unset var) is correct
for the payment-method toggles it's shared with, but wrong for a security
gate: LOG_EXPOSE_API controlled whether /api/logs*, /openapi.json, and /docs
serve anything at all, and previously defaulted to true (open) when the env
var was absent, contradicting what .env.example already documented as the
safe default. Now explicitly defaults to 'false'.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-03 23:06:36 +07:00
..
README.md fix: default LOG_EXPOSE_API to closed (false) when unset 2026-08-03 23:06:36 +07:00
api-docs.html feat: add internal admin tooling — log viewer and Scalar API docs 2026-08-03 22:49:26 +07:00
index.cjs fix: default LOG_EXPOSE_API to closed (false) when unset 2026-08-03 23:06:36 +07:00
logviewer.html feat: add internal admin tooling — log viewer and Scalar API docs 2026-08-03 22:49:26 +07:00
openapi.json fix: scrub real customer PII and a captured webhook payload from docs 2026-08-03 23:01:16 +07:00

README.md

Simaya Midtrans Payment Server

Backend Express.js server untuk integrasi pembayaran Midtrans dengan sistem ERP.

Dokumentasi endpoint yang selalu sinkron dengan kode: buka /docs (Scalar, HTTP Basic Auth) atau /openapi.json. Untuk testing manual, import postman/Midtrans-Middleware.postman_collection.json — mencakup seluruh route di bawah ini.

Daftar Isi

Fitur Utama

1. Dual Mode Payment

  • CORE API: Bank Transfer, Credit Card, GoPay/QRIS, Convenience Store
  • SNAP: Hosted payment interface dengan UI Midtrans
  • Dua bentuk payload: langsung (order_id/nominal/customer/expire_at) atau ERP (mercant_id/item[])
  • Signature HMAC-SHA256 pada token, TTL configurable
  • order_id yang mengandung karakter selain A-Za-z0-9-_~. (mis. :) otomatis disanitasi jadi . sebelum dikirim ke Midtrans

3. ERP Integration

  • Notifikasi otomatis ke sistem ERP setelah pembayaran sukses
  • Multi-endpoint support (comma-separated URLs)
  • Signature verification untuk keamanan

4. Webhook Handler

  • Unified webhook untuk CORE dan SNAP
  • Signature verification
  • Idempotent notification handling

5. State Persisten

  • activeOrders, notifiedOrders, orderRetryCount, orderMerchantId disimpan ke server/data/state.json (auto-save), jadi tidak hilang saat server restart

6. Advanced Logging

  • Level-based logging (debug, info, warn, error), file harian (LOGS_DDMMYYYY.log)
  • Auto-delete log lebih tua dari LOG_RETENTION_DAYS
  • Log viewer interaktif di /api/logs/view (search, filter level, click-to-trace)
  • Payload masking untuk sensitive data, Jakarta timezone (WIB/UTC+7)

Konfigurasi Environment

Referensi lengkap ada di .env.example di root project. Ringkasan per kategori:

Midtrans Configuration

MIDTRANS_SERVER_KEY=your-server-key
MIDTRANS_CLIENT_KEY=your-client-key
MIDTRANS_IS_PRODUCTION=false

ENABLE_BANK_TRANSFER=true
ENABLE_CREDIT_CARD=true
ENABLE_GOPAY=true
ENABLE_CSTORE=true
EXTERNAL_API_KEY=your-api-key            # X-API-KEY untuk /createtransaksi & POST /api/payment-links
PAYMENT_LINK_SECRET=your-signing-secret  # HMAC secret untuk token link
PAYMENT_LINK_TTL_MINUTES=1440
PAYMENT_LINK_BASE=http://localhost:5174/pay

ERP Integration

ERP_NOTIFICATION_URL=https://your-erp.com/api/payment-notification
ERP_NOTIFICATION_URLS=https://erp1.com/api/notif,https://erp2.com/api/notif  # multi-URL, opsional
ERP_CLIENT_ID=your-erp-client-id         # juga dipakai sebagai secret HMAC (fallback ERP_CLIENT_SECRET)
ERP_ENABLE_NOTIF=true

Logging & Admin Access

LOG_LEVEL=info
LOG_EXPOSE_API=false            # default sudah tertutup (false) kalau tidak di-set
LOG_BASIC_AUTH_USER=            # wajib diisi di production untuk lindungi /api/logs* dan /docs
LOG_BASIC_AUTH_PASS=
LOG_RETENTION_DAYS=30
LOG_BUFFER_SIZE=1000

CORS & Rate Limiting

CORS_ALLOWED_ORIGINS=https://your-frontend.example.com   # kosong = izinkan semua origin (dev default)
RATE_LIMIT_WINDOW_MS=60000
RATE_LIMIT_MAX=20

Server

PORT=8000
NODE_ENV=production   # HARUS "production" di server production — banyak gerbang keamanan bergantung pada ini

API Endpoints

Health & Config

GET /api/health

Health check endpoint.

GET /api/config

Ambil payment toggles & Midtrans client key saat ini.

POST /api/config (non-production only)

Update payment toggles saat runtime. Return 403 kalau NODE_ENV=production.

Payment Operations

POST /api/payments/charge

Buat transaksi Core API (bank transfer, card, GoPay/QRIS, cstore). Dipanggil langsung dari browser checkout, tanpa perlu pra-registrasi order. Rate-limited.

POST /api/payments/snap/token

Generate Snap token untuk hosted payment popup. Dipanggil langsung dari browser (PayPage/CheckoutPage). Rate-limited.

GET /api/payments/:orderId/status

Cek status transaksi (passthrough Midtrans). Juga memicu fallback notifikasi ERP kalau status sudah sukses tapi belum ter-notify.

POST /api/payment-links — payload langsung

Requires X-API-KEY. Rate-limited.

{
  "order_id": "DEMO-MERCHANT-0001:ITEM-0001",
  "nominal": 179000,
  "customer": { "name": "Budi Santoso", "phone": "081234567890", "email": "budi.santoso@example.com" },
  "expire_at": 1785852063058
}

order_id dipakai apa adanya untuk tracking internal; versi yang dikirim ke Midtrans disanitasi (: dan karakter lain → .). expire_at dipakai kalau valid & di masa depan, kalau tidak fallback ke PAYMENT_LINK_TTL_MINUTES.

Response:

{ "status": "200", "messages": "SUCCESS", "data": { "url": "...", "order_id": "...", "midtrans_order_id": "...", "expire_at": 1785852063058 } }

POST /createtransaksi — payload ERP (mercant_id/item)

Requires X-API-KEY. Rate-limited.

{
  "mercant_id": "merchant-001",
  "nominal": 150000,
  "nama": "John Doe",
  "email": "john@example.com",
  "no_telepon": "081234567890",
  "item": [{ "item_id": "product-123", "nama": "Product Name", "harga": 150000, "qty": 1 }],
  "allowed_methods": ["bank_transfer", "gopay"]
}

order_id diturunkan sebagai mercant_id.item[0].item_id (dot-joined). expire_at TIDAK bisa di-supply client — dihitung server dari PAYMENT_LINK_TTL_MINUTES.

Response:

{ "status": "200", "messages": "SUCCESS", "data": { "url": "http://localhost:5174/pay/eyJ2Ijox..." } }

GET /api/payment-links/:token

Resolve token jadi detail order: { order_id, nominal, customer, expire_at, allowed_methods }.

Webhook

POST /api/payments/notification

Handler webhook Midtrans (unified CORE & SNAP). Verifikasi signature_key (SHA512(order_id+status_code+gross_amount+MIDTRANS_SERVER_KEY)), lalu meneruskan notifikasi ke ERP. Return non-2xx kalau notifikasi ERP gagal supaya Midtrans retry.

Logs & Docs (internal — HTTP Basic Auth di production)

Endpoint Keterangan
GET /api/logs Log in-memory terbaru (?limit, ?level, ?q)
GET /api/logs/files Daftar file log harian
GET /api/logs/files/:filename Isi satu file log (JSON), auto-redirect ke viewer HTML kalau diakses dari browser
GET /api/logs/view?file=... Viewer interaktif: filter level, cari teks, klik nilai apa pun untuk trace
GET /openapi.json Spec OpenAPI mentah
GET /docs Dokumentasi API interaktif (Scalar)

Dev/Test (aktif hanya kalau LOG_EXPOSE_API=true)

  • POST /api/echo, POST /api/echo2 — mock endpoint untuk testing callback ERP
  • POST /api/test/notify-erp — trigger notifyERP() langsung, bypass webhook

Payment Flow

ERP System → POST /createtransaksi atau POST /api/payment-links → Server generate token → Payment URL

2. Payment Execution Flow

Customer → Payment URL → Frontend resolve token →
Pilih metode → POST /api/payments/charge atau /api/payments/snap/token → Midtrans

3. Payment Completion Flow

Midtrans → Webhook → Verifikasi signature → Update ledger →
Notify ERP → Tandai order selesai

4. Format Notifikasi ke ERP

{
  "mercant_id": "merchant-001",
  "status_code": "200",
  "nominal": "150000",
  "signature": "sha512-hash"
}

Perhitungan Signature: SHA512(mercant_id + status_code + nominal + ERP_CLIENT_ID)

Catatan: payload ke ERP tidak membawa order_id — pencocokan transaksi murni lewat mercant_id. Kalau satu mercant_id bisa punya lebih dari satu order aktif bersamaan, ERP perlu strategi lain untuk membedakannya.

Testing

Lihat folder tests/ untuk file-file testing:

node tests/test-create-payment-link.cjs
node tests/test-frontend-payload.cjs
node tests/test-snap-token.cjs

Atau pakai postman/Midtrans-Middleware.postman_collection.json — mencakup seluruh endpoint di atas, dikelompokkan per folder, termasuk request untuk mensimulasikan webhook Midtrans dari payload asli.

Logging

Log Levels

  • debug: Detail untuk diagnosa masalah
  • info: Informasi umum
  • warn: Peringatan
  • error: Error yang masih memungkinkan aplikasi jalan

Format Log (file, satu baris per event)

[2026-08-03T21:35:54.746+07:00] [INFO ] webhook.notifying_erp | {"order_id":"DEMO-MERCHANT-0002.ITEM-0002","mercant_id":"DEMO-MERCHANT-0002"}

Paling nyaman dibaca lewat GET /api/logs/view (filter level, cari teks, klik nilai untuk trace) daripada membaca file mentah.

Event Penting

Payment Lifecycle: charge.request, charge.success, charge.error, status.request, webhook.received

Payment Link: payment-links.create.issued, createtransaksi.issued, paymentlink.resolve.success

ERP Integration: erp.notify.start, erp.notify.success, erp.notify.error, erp.notify.skip

Security: webhook.signature.invalid, createtransaksi.unauthorized, payment-links.create.unauthorized, admin.auth.failed, rate_limit.blocked

Keamanan

  1. Signature Verification — webhook, token payment link, notifikasi ERP semuanya ditandatangani/diverifikasi.
  2. Idempotency — cegah duplikasi order/notifikasi ERP; state disimpan konsisten pakai order_id yang sudah disanitasi (Midtrans-safe) di semua map internal (activeOrders, notifiedOrders, orderRetryCount, orderMerchantId), sehingga tidak ada mismatch walau order_id asli mengandung karakter seperti :.
  3. API Key AuthenticationX-API-KEY untuk /createtransaksi dan POST /api/payment-links. Dev mode fallback (bypass) hanya aktif kalau NODE_ENV !== 'production' dan key belum di-set.
  4. HTTP Basic Auth untuk admin tooling/api/logs*, /openapi.json, /docs dilindungi LOG_BASIC_AUTH_USER/PASS, dan tertutup total secara default lewat LOG_EXPOSE_API=false (harus di-set eksplisit true untuk mengaktifkan). Wajib set LOG_BASIC_AUTH_USER/PASS di production sebelum mengaktifkan LOG_EXPOSE_API, karena gerbang Basic Auth-nya sendiri bergantung pada NODE_ENV=production untuk benar-benar dipaksa (lihat poin 8).
  5. Rate Limiting/createtransaksi, /api/payment-links, /api/payments/charge, /api/payments/snap/token dibatasi per-IP (RATE_LIMIT_MAX request per RATE_LIMIT_WINDOW_MS) untuk mencegah bot abuse / card-testing pada endpoint yang memang harus tetap bisa diakses langsung dari browser tanpa API key.
  6. CORS — dibatasi lewat CORS_ALLOWED_ORIGINS (comma-separated). Kosong = izinkan semua origin (default dev).
  7. Payload Masking — field sensitif (card number, CVV, token, server key) otomatis disamarkan di log.
  8. NODE_ENV production — banyak gerbang keamanan (API key, Basic Auth) bergantung pada isDevEnv(). Pastikan proses production (ecosystem.config.cjs) benar-benar set NODE_ENV=production, lalu pm2 restart <app> --update-env setelah mengubahnya.

Running the Server

npm install
npm run server        # atau: node server/index.cjs
# Server jalan di http://localhost:8000

Common Issues

"Transaction already pending"

Penyebab: Order ID sudah punya transaksi pending di Midtrans. Solusi: Pakai instruksi pembayaran yang ada, atau buat order baru dengan ID berbeda.

"ERP notification failed"

Penyebab: Endpoint ERP tidak bisa diakses, atau signature mismatch (ERP_CLIENT_ID beda dengan yang dipakai ERP). Solusi: Cek ERP_NOTIFICATION_URL(S) dan ERP_CLIENT_ID, replay payload webhook asli lewat request "Simulate Midtrans Notification (Local)" di Postman collection untuk debug.

"Invalid signature on webhook"

Penyebab: MIDTRANS_SERVER_KEY salah, atau webhook dari sumber tak sah. Solusi: Pastikan MIDTRANS_SERVER_KEY sama dengan akun Midtrans yang memproses transaksi tersebut (sandbox vs production beda key).

Log//docs mengembalikan 401 atau 503

Penyebab: LOG_BASIC_AUTH_USER/PASS belum di-set di production (NODE_ENV=production). Solusi: Set kedua env var tersebut, restart server.

Support