From 8c42768ec341c159eb0aaa1df1c2b74f676c48ab Mon Sep 17 00:00:00 2001 From: CIFO Dev Date: Mon, 17 Nov 2025 10:58:44 +0700 Subject: [PATCH] ERP: gunakan ERP_CLIENT_SECRET untuk signature; perbaiki fallback; tambah log detail HTTP request/response; endpoint GET /api/logs untuk akses log via browser; log payload ERP dengan signature length dan presence --- scripts/midtrans-sig.cjs | 15 ------ server/index.cjs | 99 ++++++++++++++++++++++++++++++++++------ 2 files changed, 86 insertions(+), 28 deletions(-) delete mode 100644 scripts/midtrans-sig.cjs diff --git a/scripts/midtrans-sig.cjs b/scripts/midtrans-sig.cjs deleted file mode 100644 index 2bd7821..0000000 --- a/scripts/midtrans-sig.cjs +++ /dev/null @@ -1,15 +0,0 @@ -// Utility to compute Midtrans webhook signature: sha512(order_id + status_code + gross_amount + server_key) -const crypto = require('crypto') - -function main() { - const [orderId, statusCode, grossAmount, serverKey] = process.argv.slice(2) - if (!orderId || !statusCode || !grossAmount || !serverKey) { - console.error('Usage: node scripts/midtrans-sig.js ') - process.exit(1) - } - const raw = String(orderId) + String(statusCode) + String(grossAmount) + String(serverKey) - const sig = crypto.createHash('sha512').update(raw).digest('hex') - process.stdout.write(sig) -} - -main() \ No newline at end of file diff --git a/server/index.cjs b/server/index.cjs index a794823..96241af 100644 --- a/server/index.cjs +++ b/server/index.cjs @@ -37,12 +37,16 @@ function parseEnable(v) { } const ERP_NOTIFICATION_URL = process.env.ERP_NOTIFICATION_URL || '' const ERP_ENABLE_NOTIF = parseEnable(process.env.ERP_ENABLE_NOTIF) -const ERP_CLIENT_ID = process.env.ERP_CLIENT_ID || '' +// Gunakan secret untuk signature; fallback ke CLIENT_ID bila SECRET belum ada +const ERP_CLIENT_SECRET = process.env.ERP_CLIENT_SECRET || process.env.ERP_CLIENT_ID || '' const notifiedOrders = new Set() // --- Logger utilities const LOG_LEVEL = (process.env.LOG_LEVEL || 'info').toLowerCase() const levelOrder = { debug: 0, info: 1, warn: 2, error: 3 } +const LOG_EXPOSE_API = parseEnable(process.env.LOG_EXPOSE_API) +const LOG_BUFFER_SIZE = parseInt(process.env.LOG_BUFFER_SIZE || '1000', 10) +const recentLogs = [] function shouldLog(level) { return (levelOrder[level] ?? 1) >= (levelOrder[LOG_LEVEL] ?? 1) } function ts() { return new Date().toISOString() } function sanitize(obj) { @@ -65,6 +69,11 @@ function maskPayload(obj) { function log(level, msg, meta) { if (!shouldLog(level)) return const line = `[${ts()}] [${level}] ${msg}` + try { + const entry = { ts: ts(), level, msg, meta: sanitize(meta) } + recentLogs.push(entry) + if (recentLogs.length > LOG_BUFFER_SIZE) recentLogs.shift() + } catch {} if (meta) { const data = typeof meta === 'string' ? meta : JSON.stringify(meta) console.log(line, data) @@ -163,6 +172,26 @@ app.get('/api/config', (_req, res) => { res.json(payload) }) +// Logs endpoint (dev/debug): GET /api/logs?limit=100&level=debug|info|warn|error&q=keyword +app.get('/api/logs', (req, res) => { + if (!LOG_EXPOSE_API) { + return res.status(403).json({ error: 'FORBIDDEN', message: 'Log API disabled. Set LOG_EXPOSE_API=true to enable.' }) + } + const limit = Math.max(1, Math.min(1000, parseInt(String(req.query.limit || '100'), 10))) + const level = String(req.query.level || '').toLowerCase() + const q = String(req.query.q || '').toLowerCase() + let items = recentLogs + if (level) items = items.filter((e) => e.level === level) + if (q) items = items.filter((e) => { + try { + const m = e.meta ? JSON.stringify(e.meta).toLowerCase() : '' + return e.msg.toLowerCase().includes(q) || m.includes(q) + } catch { return false } + }) + const sliced = items.slice(Math.max(0, items.length - limit)) + res.json({ count: sliced.length, items: sliced }) +}) + // Dev-only: allow updating toggles at runtime without restart app.post('/api/config', (req, res) => { const isDev = process.env.NODE_ENV !== 'production' @@ -411,33 +440,64 @@ function isSuccessfulMidtransStatus(body) { return false } -function postJson(url, data) { +class HttpError extends Error { + constructor(statusCode, body) { + super(`HTTP ${statusCode}`) + this.statusCode = statusCode + this.body = body + } +} + +function postJson(url, data, extraHeaders = {}) { return new Promise((resolve, reject) => { try { const u = new URL(url) const body = JSON.stringify(data) + const headers = { + 'Content-Type': 'application/json', + 'Content-Length': Buffer.byteLength(body), + ...extraHeaders, + } + logDebug('http.post', { + url, + headers: sanitize(headers), + body_length: Buffer.byteLength(body), + body_preview: body.slice(0, 256), + }) const opts = { method: 'POST', hostname: u.hostname, path: u.pathname + (u.search || ''), port: u.port || (u.protocol === 'https:' ? 443 : 80), - headers: { - 'Content-Type': 'application/json', - 'Content-Length': Buffer.byteLength(body), - }, + headers, } const req = (u.protocol === 'https:' ? https : require('http')).request(opts, (res) => { let chunks = '' res.on('data', (d) => { chunks += d.toString() }) res.on('end', () => { - if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) resolve({ status: res.statusCode, body: chunks }) - else reject(new Error(`HTTP ${res.statusCode}: ${chunks}`)) + const info = { + url, + statusCode: res.statusCode, + body_length: chunks.length, + body_preview: chunks.slice(0, 256), + } + if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) { + logDebug('http.response', info) + resolve({ status: res.statusCode, body: chunks }) + } else { + logWarn('http.response.error', info) + reject(new HttpError(res.statusCode, chunks)) + } }) }) - req.on('error', reject) + req.on('error', (e) => { + logError('http.request.error', { url, message: e?.message }) + reject(e) + }) req.write(body) req.end() } catch (e) { + logError('http.build.error', { url, message: e?.message }) reject(e) } }) @@ -474,8 +534,8 @@ async function notifyERP({ orderId, nominal, mercantId }) { return false } // Untuk notifikasi dinamis, hanya URL dan client secret yang wajib - if (!ERP_NOTIFICATION_URL || !ERP_CLIENT_ID) { - logWarn('erp.notify.missing_config', { hasUrl: !!ERP_NOTIFICATION_URL, hasClientId: !!ERP_CLIENT_ID }) + if (!ERP_NOTIFICATION_URL || !ERP_CLIENT_SECRET) { + logWarn('erp.notify.missing_config', { hasUrl: !!ERP_NOTIFICATION_URL, hasClientSecret: !!ERP_CLIENT_SECRET }) return false } const statusCode = '200' @@ -484,7 +544,7 @@ async function notifyERP({ orderId, nominal, mercantId }) { logWarn('erp.notify.skip', { orderId, reason: 'missing_mercant_id' }) return false } - const signature = await computeErpSignature(mId, statusCode, nominal, ERP_CLIENT_ID) + const signature = await computeErpSignature(mId, statusCode, nominal, ERP_CLIENT_SECRET) const payload = { mercant_id: mId, @@ -492,13 +552,26 @@ async function notifyERP({ orderId, nominal, mercantId }) { nominal: nominal, signature: signature, } + // Tambahan debug: pastikan signature benar-benar ikut terkirim + logDebug('erp.notify.payload.outgoing', { + mercant_id: mId, + status_code: statusCode, + nominal: nominal, + signature_present: typeof signature !== 'undefined', + signature_length: (typeof signature === 'string') ? signature.length : -1, + }) logInfo('erp.notify.start', { orderId, url: ERP_NOTIFICATION_URL }) try { const res = await postJson(ERP_NOTIFICATION_URL, payload) logInfo('erp.notify.success', { orderId, status: res.status }) return true } catch (e) { - logError('erp.notify.error', { orderId, message: e?.message }) + logError('erp.notify.error', { + orderId, + status: e?.statusCode, + message: e?.message, + body: typeof e?.body === 'string' ? e.body.slice(0, 256) : undefined, + }) return false } } -- 2.40.1