fix(webhook): auto secret token + URL dari public base/host
All checks were successful
CI / test (push) Successful in 2m5s
All checks were successful
CI / test (push) Successful in 2m5s
- ensureWebhookSecret saat boot dan buka Pengaturan - URL webhook memakai public_base_url fallback host - tolak default billing-rtrw-secret-key - versi 0.6.3
This commit is contained in:
@@ -2,6 +2,13 @@
|
|||||||
|
|
||||||
Format mengikuti [Keep a Changelog](https://keepachangelog.com/) dan [Semantic Versioning](https://semver.org/).
|
Format mengikuti [Keep a Changelog](https://keepachangelog.com/) dan [Semantic Versioning](https://semver.org/).
|
||||||
|
|
||||||
|
## [0.6.3] — 2026-11-06 — "webhook-secret-auto"
|
||||||
|
|
||||||
|
### Diperbaiki
|
||||||
|
- **Secret Token webhook pembayaran** dibuat otomatis (64 hex) bila kosong/default, ditampilkan di Pengaturan dan langsung dipakai pada URL webhook.
|
||||||
|
- URL webhook memakai **Public Base URL**; bila kosong memakai host saat ini (tidak lagi menghasilkan URL relatif).
|
||||||
|
- Default lama `billing-rtrw-secret-key` tidak lagi diterima — forwarder harus memakai secret dari Pengaturan.
|
||||||
|
|
||||||
## [0.6.2] — 2026-11-06 — "webhook-diag"
|
## [0.6.2] — 2026-11-06 — "webhook-diag"
|
||||||
|
|
||||||
### Diperbaiki
|
### Diperbaiki
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="https://img.shields.io/badge/lisensi-ISC-ADFF2F" alt="Lisensi: ISC">
|
<img src="https://img.shields.io/badge/lisensi-ISC-ADFF2F" alt="Lisensi: ISC">
|
||||||
<img src="https://img.shields.io/badge/versi-0.6.2_webhook--diag-ADFF2F" alt="Versi 0.6.2 (webhook-diag)">
|
<img src="https://img.shields.io/badge/versi-0.6.3_webhook--secret-ADFF2F" alt="Versi 0.6.3 (webhook-secret-auto)">
|
||||||
<img src="https://img.shields.io/badge/Node.js-%E2%89%A520-339933" alt="Node.js >= 20">
|
<img src="https://img.shields.io/badge/Node.js-%E2%89%A520-339933" alt="Node.js >= 20">
|
||||||
<img src="https://img.shields.io/badge/docker-ready-2496ED?logo=docker&logoColor=white" alt="Docker">
|
<img src="https://img.shields.io/badge/docker-ready-2496ED?logo=docker&logoColor=white" alt="Docker">
|
||||||
<a href="https://demo-openisp.oxpoe.xyz"><img src="https://img.shields.io/badge/demo-live-ADFF2F?logo=googlechrome&logoColor=black" alt="Demo"></a>
|
<a href="https://demo-openisp.oxpoe.xyz"><img src="https://img.shields.io/badge/demo-live-ADFF2F?logo=googlechrome&logoColor=black" alt="Demo"></a>
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ const { SUPPORTED_LANGS, FALLBACK_LANG, normalizeLang, t } = require('./config/i
|
|||||||
if (typeof ensureDefaultSettings === 'function') {
|
if (typeof ensureDefaultSettings === 'function') {
|
||||||
ensureDefaultSettings();
|
ensureDefaultSettings();
|
||||||
try { require('./config/settingsManager').ensureGowaWebhookSecret(); } catch (e) {}
|
try { require('./config/settingsManager').ensureGowaWebhookSecret(); } catch (e) {}
|
||||||
|
try { require('./config/settingsManager').ensureWebhookSecret(); } catch (e) {}
|
||||||
// Proses baru = kode terbaru sudah dimuat; hapus penanda restart.
|
// Proses baru = kode terbaru sudah dimuat; hapus penanda restart.
|
||||||
try { fs.rmSync(path.join(__dirname, '.restart-required'), { force: true }); } catch (e) {}
|
try { fs.rmSync(path.join(__dirname, '.restart-required'), { force: true }); } catch (e) {}
|
||||||
}
|
}
|
||||||
@@ -901,7 +902,7 @@ app.post('/api/webhook/v1/payment-notif', multer().any(), async (req, res) => {
|
|||||||
gotTrim = gotTrim.replace(/^[\"'\s]+|[\"'\s]+$/g, '');
|
gotTrim = gotTrim.replace(/^[\"'\s]+|[\"'\s]+$/g, '');
|
||||||
if (/^bearer\s+/i.test(gotTrim)) gotTrim = gotTrim.replace(/^bearer\s+/i, '').trim();
|
if (/^bearer\s+/i.test(gotTrim)) gotTrim = gotTrim.replace(/^bearer\s+/i, '').trim();
|
||||||
|
|
||||||
const isSecretValid = (gotTrim === expectedTrim) || (gotTrim === 'billing-rtrw-secret-key');
|
const isSecretValid = gotTrim === expectedTrim;
|
||||||
|
|
||||||
if (!isSecretValid) {
|
if (!isSecretValid) {
|
||||||
logger.warn(`[WEBHOOK][payment-notif] Forbidden: secret_key mismatch. service=${String(service || '-')} (gotLen=${gotTrim.length}${gotTrim ? ', ***' + gotTrim.slice(-3) : ', empty'}, expectedLen=${expectedTrim.length}, ***${expectedTrim.slice(-3)})`);
|
logger.warn(`[WEBHOOK][payment-notif] Forbidden: secret_key mismatch. service=${String(service || '-')} (gotLen=${gotTrim.length}${gotTrim ? ', ***' + gotTrim.slice(-3) : ', empty'}, expectedLen=${expectedTrim.length}, ***${expectedTrim.slice(-3)})`);
|
||||||
|
|||||||
@@ -282,6 +282,14 @@ function formatTimeLocal(date) {
|
|||||||
* Pastikan gowa_webhook_secret tidak memakai default 'secret'.
|
* Pastikan gowa_webhook_secret tidak memakai default 'secret'.
|
||||||
* Dibuat otomatis (32 byte hex) bila kosong/default/terlalu pendek.
|
* Dibuat otomatis (32 byte hex) bila kosong/default/terlalu pendek.
|
||||||
*/
|
*/
|
||||||
|
function ensureWebhookSecret(force) {
|
||||||
|
const current = String(getSetting('webhook_secret', '') || '').trim();
|
||||||
|
if (!force && current && current !== 'billing-rtrw-secret-key' && current.length >= 24) return current;
|
||||||
|
const secret = crypto.randomBytes(32).toString('hex');
|
||||||
|
saveSettings({ webhook_secret: secret });
|
||||||
|
return secret;
|
||||||
|
}
|
||||||
|
|
||||||
function ensureGowaWebhookSecret(force) {
|
function ensureGowaWebhookSecret(force) {
|
||||||
const current = String(getSetting('gowa_webhook_secret', '') || '').trim();
|
const current = String(getSetting('gowa_webhook_secret', '') || '').trim();
|
||||||
if (!force && current && current !== 'secret' && current.length >= 24) return current;
|
if (!force && current && current !== 'secret' && current.length >= 24) return current;
|
||||||
@@ -374,5 +382,6 @@ module.exports = {
|
|||||||
parseDateInTimezone,
|
parseDateInTimezone,
|
||||||
startSettingsWatcher,
|
startSettingsWatcher,
|
||||||
ensureDefaultSettings,
|
ensureDefaultSettings,
|
||||||
ensureGowaWebhookSecret
|
ensureGowaWebhookSecret,
|
||||||
|
ensureWebhookSecret
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "open-isp",
|
"name": "open-isp",
|
||||||
"version": "0.6.2",
|
"version": "0.6.3",
|
||||||
"description": "OPEN-ISP \u2014 sistem billing & manajemen ISP open-source (PWA pelanggan, multi-OLT, RADIUS, WhatsApp, QRIS)",
|
"description": "OPEN-ISP \u2014 sistem billing & manajemen ISP open-source (PWA pelanggan, multi-OLT, RADIUS, WhatsApp, QRIS)",
|
||||||
"main": "app-customer.js",
|
"main": "app-customer.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -4490,15 +4490,17 @@ router.post('/automation/send-now', requireAdminSession, express.urlencoded({ ex
|
|||||||
});
|
});
|
||||||
|
|
||||||
router.get('/settings', requireAdminSession, requireSidebarMenuAccess('settings'), (req, res) => {
|
router.get('/settings', requireAdminSession, requireSidebarMenuAccess('settings'), (req, res) => {
|
||||||
const settings = getSettings();
|
let settings = getSettings();
|
||||||
|
try { require('../config/settingsManager').ensureWebhookSecret(); settings = getSettings(); } catch (e) {}
|
||||||
const protocol = req.headers['x-forwarded-proto'] || req.protocol;
|
const protocol = req.headers['x-forwarded-proto'] || req.protocol;
|
||||||
const host = req.get('host');
|
const host = req.get('host');
|
||||||
const baseUrl = (settings && settings.app_url ? String(settings.app_url) : `${protocol}://${host}`).replace(/\/+$/, '');
|
const baseUrl = (settings.public_base_url || (settings && settings.app_url) || `${protocol}://${host}`).toString().replace(/\/+$/, '');
|
||||||
const paymentWebhookUrl = `${baseUrl}/customer/payment/callback`;
|
const paymentWebhookUrl = `${baseUrl}/customer/payment/callback`;
|
||||||
res.render('admin/settings', {
|
res.render('admin/settings', {
|
||||||
title: 'Pengaturan Sistem', company: company(), activePage: 'settings',
|
title: 'Pengaturan Sistem', company: company(), activePage: 'settings',
|
||||||
settings, msg: flashMsg(req),
|
settings, msg: flashMsg(req),
|
||||||
paymentWebhookUrl
|
paymentWebhookUrl,
|
||||||
|
webhookBaseUrl: baseUrl
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
0.6.2
|
0.6.3
|
||||||
@@ -815,7 +815,7 @@
|
|||||||
<div style="margin-top: 15px; border-top: 1px dashed rgba(0,0,0,0.1); padding-top: 15px;">
|
<div style="margin-top: 15px; border-top: 1px dashed rgba(0,0,0,0.1); padding-top: 15px;">
|
||||||
<h6 style="font-size:11px; margin-bottom: 10px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.5px;"><i class="bi bi-phone"></i> Hubungkan Webhook Notifikasi E-Wallet (Mode 2)</h6>
|
<h6 style="font-size:11px; margin-bottom: 10px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.5px;"><i class="bi bi-phone"></i> Hubungkan Webhook Notifikasi E-Wallet (Mode 2)</h6>
|
||||||
<%
|
<%
|
||||||
const _base = (settings.public_base_url || '').replace(/\/+$/, '');
|
const _base = String(typeof webhookBaseUrl !== 'undefined' && webhookBaseUrl ? webhookBaseUrl : (settings.public_base_url || '')).replace(/\/+$/, '');
|
||||||
const _secret = String(settings.webhook_secret || '');
|
const _secret = String(settings.webhook_secret || '');
|
||||||
const macroWebhookUrlFull = _base + '/api/webhook/v1/payment-notif?secret_key=' + encodeURIComponent(_secret);
|
const macroWebhookUrlFull = _base + '/api/webhook/v1/payment-notif?secret_key=' + encodeURIComponent(_secret);
|
||||||
%>
|
%>
|
||||||
@@ -841,6 +841,7 @@
|
|||||||
<option value="false" <%= (settings.webhook_unmatched_notify_enabled === undefined ? true : settings.webhook_unmatched_notify_enabled) ? '' : 'selected' %>><%= t('common.inactive','Nonaktif') %></option>
|
<option value="false" <%= (settings.webhook_unmatched_notify_enabled === undefined ? true : settings.webhook_unmatched_notify_enabled) ? '' : 'selected' %>><%= t('common.inactive','Nonaktif') %></option>
|
||||||
</select>
|
</select>
|
||||||
<div class="hint"><%= t('admin.set.wh_wa_hint','Kirim WA ke admin tiap ada pembayaran terdeteksi tapi belum cocok (untuk dicocokkan manual via jam notif vs bukti transfer).') %></div>
|
<div class="hint"><%= t('admin.set.wh_wa_hint','Kirim WA ke admin tiap ada pembayaran terdeteksi tapi belum cocok (untuk dicocokkan manual via jam notif vs bukti transfer).') %></div>
|
||||||
|
<div class="hint" style="margin-top:6px"><i class="bi bi-info-circle"></i> Secret dibuat otomatis jika kosong. URL memakai <b>Public Base URL</b>; bila kosong memakai host saat ini. Salin URL + token ke aplikasi notification forwarder.</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="fg" style="margin-top:10px">
|
<div class="fg" style="margin-top:10px">
|
||||||
<label class="fl"><i class="bi bi-clock-history"></i> <%= t('admin.set.wh_retention','Retensi Notifikasi Webhook (Otomasi)') %></label>
|
<label class="fl"><i class="bi bi-clock-history"></i> <%= t('admin.set.wh_retention','Retensi Notifikasi Webhook (Otomasi)') %></label>
|
||||||
|
|||||||
Reference in New Issue
Block a user