fix(gowa): QR sebagai data URI + rewrite qr_link ke base URL publik
All checks were successful
CI / test (push) Successful in 2m41s

- gambar tidak lagi bergantung request /qr.png kedua
- qr_link host internal diganti origin base URL
- log saat gambar gagal diambil; onerror di UI
- versi 0.5.9
This commit is contained in:
oxpoe
2026-11-05 15:55:29 +07:00
parent 7a5b488882
commit b98ad7568d
7 changed files with 47 additions and 8 deletions

View File

@@ -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.5.9] — 2026-11-05 — "gowa-qr-image"
### Diperbaiki
- QR GOWA dikirim sebagai **data URI** sehingga gambar tetap tampil walau app berjalan multi-proses atau request gambar terpisah gagal.
- `qr_link` dari GOWA yang menunjuk host internal (`127.0.0.1`/container) ditulis ulang memakai base URL publik yang dikonfigurasi.
- Log peringatan `[GOWA] Gagal ambil gambar QR` bila pengambilan gambar gagal.
## [0.5.8] — 2026-11-05 — "gowa-qr-error" ## [0.5.8] — 2026-11-05 — "gowa-qr-error"
### Diperbaiki ### Diperbaiki

View File

@@ -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.5.8_gowa--qr-error-ADFF2F" alt="Versi 0.5.8 (gowa-qr-error)"> <img src="https://img.shields.io/badge/versi-0.5.9_gowa--qr-image-ADFF2F" alt="Versi 0.5.9 (gowa-qr-image)">
<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>

View File

@@ -1,6 +1,6 @@
{ {
"name": "open-isp", "name": "open-isp",
"version": "0.5.8", "version": "0.5.9",
"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": {

View File

@@ -6654,7 +6654,16 @@ router.get('/whatsapp/gowa/qr', requireAdminSession, async (req, res) => {
const detail = r.message || r.code || 'pastikan GOWA berjalan & username/password Basic Auth benar'; const detail = r.message || r.code || 'pastikan GOWA berjalan & username/password Basic Auth benar';
return res.json({ ok: false, status: r.status || 0, message: 'Gagal membuat QR: ' + (r.status ? 'HTTP ' + r.status + ' — ' : '') + detail + '.' }); return res.json({ ok: false, status: r.status || 0, message: 'Gagal membuat QR: ' + (r.status ? 'HTTP ' + r.status + ' — ' : '') + detail + '.' });
} }
return res.json({ ok: true, image: '/admin/whatsapp/gowa/qr.png?v=' + Date.now(), device_id: r.device_id, duration: r.duration }); // Kirim gambar langsung sebagai data URI: tidak bergantung pada request
// kedua /qr.png (aman bila app berjalan multi-proses) dan tidak kena cache.
let image = '/admin/whatsapp/gowa/qr.png?v=' + Date.now();
try {
const img = await gowa.getLastQrImage();
if (img && img.buffer && img.buffer.length) {
image = 'data:' + (img.contentType || 'image/png') + ';base64,' + img.buffer.toString('base64');
}
} catch (e) {}
return res.json({ ok: true, image, device_id: r.device_id, duration: r.duration });
} catch (e) { return res.json({ ok: false, message: e.message }); } } catch (e) { return res.json({ ok: false, message: e.message }); }
}); });

View File

@@ -192,14 +192,37 @@ async function getLoginQr(deviceId) {
return { ok: false, status: 0, code: null, message: e.message }; return { ok: false, status: 0, code: null, message: e.message };
} }
} }
// GOWA bisa mengembalikan qr_link absolut ke host internal (127.0.0.1 / nama
// container). Ambil path-nya saja lalu tempel ke base URL yang dikonfigurasi,
// supaya gambar tetap bisa diambil saat GOWA diakses lewat domain/reverse proxy.
function resolveQrLink(link) {
const l = String(link || '').trim();
if (!l) return '';
if (/^data:/i.test(l)) return l;
try {
const u = new URL(l, baseUrl() + '/');
const base = new URL(baseUrl());
return base.origin + u.pathname + u.search;
} catch (e) {
return baseUrl() + (l.charAt(0) === '/' ? l : '/' + l);
}
}
async function getLastQrImage() { async function getLastQrImage() {
if (!_lastQr.link) return null; if (!_lastQr.link) return null;
const url = resolveQrLink(_lastQr.link);
try { try {
const res = await fetch(_lastQr.link, { headers: commonHeaders() }); const res = await fetch(url, { headers: commonHeaders() });
if (!res.ok) return null; if (!res.ok) {
logger.warn(`[GOWA] Gagal ambil gambar QR: HTTP ${res.status} (${url})`);
return null;
}
const buf = Buffer.from(await res.arrayBuffer()); const buf = Buffer.from(await res.arrayBuffer());
return { buffer: buf, contentType: res.headers.get('content-type') || 'image/png' }; return { buffer: buf, contentType: res.headers.get('content-type') || 'image/png' };
} catch (e) { return null; } } catch (e) {
logger.warn(`[GOWA] Gagal ambil gambar QR: ${e.message} (${url})`);
return null;
}
} }
module.exports = { sendGowaMessage, sendGowaImage, sendGowaDocument, testGowaConnection, gowaStatus, listDevices, ensureDevice, getLoginQr, getLastQrImage, normalizePhoneJid, baseUrl, resolveDeviceId, invalidateDeviceCache }; module.exports = { sendGowaMessage, sendGowaImage, sendGowaDocument, testGowaConnection, gowaStatus, listDevices, ensureDevice, getLoginQr, getLastQrImage, normalizePhoneJid, baseUrl, resolveDeviceId, invalidateDeviceCache };

View File

@@ -1 +1 @@
0.5.8 0.5.9

View File

@@ -181,7 +181,7 @@
<button type="button" class="btn btn-p btn-sm" onclick="showGowaQr()"><i class="bi bi-qr-code-scan"></i> Tampilkan QR</button> <button type="button" class="btn btn-p btn-sm" onclick="showGowaQr()"><i class="bi bi-qr-code-scan"></i> Tampilkan QR</button>
<a href="<%= gowaUiUrl %>" target="_blank" rel="noopener" class="btn btn-g btn-sm" style="margin-left:6px"><i class="bi bi-box-arrow-up-right"></i> Buka UI GOWA</a> <a href="<%= gowaUiUrl %>" target="_blank" rel="noopener" class="btn btn-g btn-sm" style="margin-left:6px"><i class="bi bi-box-arrow-up-right"></i> Buka UI GOWA</a>
<div id="gowa_qr_wrap" style="display:none;margin-top:10px;text-align:center"> <div id="gowa_qr_wrap" style="display:none;margin-top:10px;text-align:center">
<img id="gowa_qr_img" alt="QR GOWA" style="max-width:240px;background:#fff;padding:10px;border-radius:12px"> <img id="gowa_qr_img" alt="QR GOWA" style="max-width:240px;background:#fff;padding:10px;border-radius:12px" onerror="var s=document.getElementById('gowa_qr_status');if(s)s.textContent='Gambar QR gagal dimuat — cek log server (base URL/kredensial GOWA).'">
<div id="gowa_qr_status" style="font-size:12px;color:var(--muted);margin-top:6px"></div> <div id="gowa_qr_status" style="font-size:12px;color:var(--muted);margin-top:6px"></div>
</div> </div>
</div> </div>