Compare commits

..

2 Commits

2 changed files with 86 additions and 28 deletions

View File

@ -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 <order_id> <status_code> <gross_amount> <server_key>')
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()

View File

@ -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
}
}