Compare commits

...
26 Commits
Author SHA1 Message Date
oxpoe 91b65fc769 docs: banner mirror (repo asli di Gitea) 2026-10-14 12:30:36 +07:00
oxpoe ed557cf899 chore(tidy): version.txt=0.5.1 (cek update) + buang blok UI khusus Baileys 2026-10-13 04:07:15 +07:00
oxpoe d02c9baf7b feat(update): mode container -> menu Update tampil dgn pesan rebuild image (bukan disembunyikan) 2026-10-13 04:00:25 +07:00
oxpoe fed81ed5f9 chore: selaraskan default server_port=3001 (Docker/healthcheck/wiki) 2026-10-13 03:49:47 +07:00
oxpoe a68121544d chore: rapikan istilah lama (RTRW/Baileys) + path relatif isolir-redirect 2026-10-13 03:49:25 +07:00
oxpoe cdb5046147 docs: bersihkan dokumen basi (artefak fork/dev) + buang referensi digiflazz 2026-10-13 03:48:58 +07:00
oxpoe cb60ceec10 chore: default wa_gateway_type=gowa (baileys dilepas) + README 2026-10-13 03:47:32 +07:00
oxpoe 0f612df70f fix(monitoring): diagnosa WhatsApp sadar-gateway (GoWA) + tambah cek Telegram 2026-10-13 03:36:53 +07:00
oxpoe 6f97430e41 feat: dukung SETTINGS_PATH (settings.json di direktori mount/persisten) 2026-10-13 03:29:36 +07:00
oxpoe f27c02309e feat: badge kelengkapan data pelanggan; fix IP search GenieACS; mount speedtest backend; isolir-redirect embedded; sembunyikan menu Update tanpa .git 2026-10-13 03:01:16 +07:00
oxpoe b715f48f8d chore: gitignore penanda restore .restore-pending 2026-10-13 00:15:52 +07:00
oxpoe 6abfeb4577 chore(release): siapkan v0.5.1 (versi, CHANGELOG, release notes) 2026-10-13 00:11:22 +07:00
oxpoe c6b39ae2bc chore(deps): upgrade node-telegram-bot-api 0.66 -> 2.1.0 (via adapter v1->v2), audit 0 vuln 2026-10-12 23:37:00 +07:00
oxpoe 3e913d8ceb chore(log): arahkan console.* (server) ke logger agar log seragam & tersimpan di logs/ 2026-10-12 23:31:45 +07:00
oxpoe ba0845da47 fix(backup): terapkan restore DB saat startup (tunda swap) - menimpa DB di bawah koneksi hidup merusak (WAL) 2026-10-12 23:24:24 +07:00
oxpoe 2aadb66f77 fix(backup): perbaiki scope restartNeeded (di luar try) agar restart setelah restore DB benar-benar jalan 2026-10-12 23:20:13 +07:00
oxpoe 994e3f8d61 fix(backup): buang -wal/-shm lama saat restore DB (backup self-contained) 2026-10-12 23:18:04 +07:00
oxpoe a77975ca44 fix(backup): restart otomatis setelah restore database (koneksi lama masih pegang file DB lama) 2026-10-12 23:13:35 +07:00
oxpoe 3eb4edfe5b fix(backup): VACUUM INTO agar backup konsisten saat DB mode WAL (sebelumnya salin file mentah bisa kehilangan -wal) 2026-10-12 23:12:35 +07:00
oxpoe ebbfffc116 fix(deps): xlsx -> build resmi SheetJS 0.20.3 (tutup high CVE) 2026-10-12 23:06:14 +07:00
oxpoe 4185af3971 fix(deps): hapus baileys (tak dipakai), update axios/multer, audit fix; fix trust proxy (XFF) 2026-10-12 22:45:29 +07:00
oxpoe a1e1570221 fix(log): putar log otomatis (10MB x 5) agar tak menumpuk 2026-10-12 22:37:46 +07:00
oxpoe dc5c1299d3 feat(telegram): perintah /cekterhubung <tag> (status perangkat) 2026-10-12 22:29:59 +07:00
oxpoe 008aa871f6 docs: CHANGELOG — catat perubahan pasca rilis v0.5.0 2026-10-12 22:27:56 +07:00
oxpoe 63994df1e4 feat: toggle fitur voucher (setting voucher_enabled) + gate route & sembunyikan link 2026-10-12 22:20:48 +07:00
oxpoe 8caae14cfd fix: footer versi & portal ikut company_header (brand-aware); hapus hack SKYFIBER 2026-10-09 11:32:20 +07:00
57 changed files with 468 additions and 11880 deletions

No files matched your search

+3
View File
@@ -82,3 +82,6 @@ public/downloads/*.aab
public/downloads/logo-* public/downloads/logo-*
.npmrc .npmrc
.gitconfig .gitconfig
# penanda restore tertunda (runtime)
database/.restore-pending
+40
View File
@@ -2,6 +2,46 @@
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/).
## [Unreleased] — setelah v0.5.1
### Diperbaiki
- Kolom **IP Address** di **GenieACS Pro** kini tampil saat **mode pencarian** (sebelumnya kosong).
- Menu **Update GitHub** otomatis disembunyikan pada instalasi **tanpa `.git`** (container/Docker), tetap tampil pada instalasi git.
### Ditambahkan
- **Badge kelengkapan data** di daftar pelanggan: centang hijau = **nomor HP + koordinat + ACS** lengkap; silang merah = ada yang kurang (tooltip menyebut yang belum).
- **Backend speedtest bawaan** (`/speedtest/*`) kini aktif (LibreSpeed self-hosted, same-origin).
- `isolir-redirect-server` dapat dijalankan **embedded** dalam proses app (opt-out via `ISOLIR_REDIRECT_EMBEDDED=0`).
## [0.5.1] — 2026-10-13 — "pickle-lime"
### Diubah
- **WhatsApp** kini **notifikasi keluar saja** (interaksi/bot perintah dimatikan); interaksi dipindah ke **Telegram**.
- **Telegram** jadi pusat monitoring admin: menu + `/sistem`, `/pendapatan`, `/tunggakan`, `/tiket`, `/offline`, `/aktif`, `/olt`, `/trafik`.
- Alert admin/monitoring (staff/GenieACS/cron) dikirim via **Telegram** (WA fallback).
- Footer & portal *brand-aware* (mengikuti `company_header`).
### Ditambahkan
- Toggle **`voucher_enabled`** (Pengaturan → Fitur Opsional) untuk menyalakan/mematikan fitur Beli Voucher.
- Notifikasi **tiket** menyertakan **Chat Pelanggan (wa.me)** + **Share Lokasi (Maps)**.
### Diperbaiki
- Spintax `{A|B|C}` diproses terpusat (teks/gambar/dokumen).
- `getTechById` (notifikasi tiket oleh admin yang sebelumnya gagal).
- Tabel `discount_logs` dibuat saat fresh-install (500 di `/admin/discounts`).
- `.devcontainer` + `.nvmrc` untuk Codespaces (Node 22).
- **Backup DB** memakai `VACUUM INTO` (konsisten walau mode WAL) dan **restore diterapkan saat startup** (sebelum koneksi DB dibuka); app restart otomatis untuk menerapkan.
- **Logger** memutar berkas otomatis (10MB × 5) dan `console.*` sisi server diarahkan ke logger (log seragam di `logs/`).
- `settings.json` permission di-set `600`.
### Keamanan
- **`npm audit`: 16 → 0** kerentanan.
- Hapus `@whiskeysockets/baileys` (tidak dipakai).
- Perbarui `axios`, `multer`, `brace-expansion`, `proxy-addr`.
- `xlsx` → build resmi **SheetJS 0.20.3**.
- `node-telegram-bot-api` 0.66 → **2.1.0** (via adapter v1→v2 internal).
- Perbaiki bug **`trust proxy`** (`X-Forwarded-For` terbalik → IP bisa dipalsukan).
## [0.5.0] — 2026-10-09 — "pickle-lime" ## [0.5.0] — 2026-10-09 — "pickle-lime"
Rilis open-source perdana. Rilis open-source perdana.
+4 -2
View File
@@ -1,3 +1,5 @@
> 🪞 **Mirror.** Repo asli & pengembangan ada di **Gitea**: https://gitea.oxpoe.qzz.io/oxpoe/open-isp
<p align="center"> <p align="center">
<img src="docs/assets/banner.png" alt="OPEN-ISP" width="100%"> <img src="docs/assets/banner.png" alt="OPEN-ISP" width="100%">
</p> </p>
@@ -9,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_pickle--lime-ADFF2F" alt="Versi 0.5 (pickle-lime)"> <img src="https://img.shields.io/badge/versi-0.5.1_pickle--lime-ADFF2F" alt="Versi 0.5.1 (pickle-lime)">
<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">
<img src="https://img.shields.io/github/stars/oxpoe/open-isp?style=flat" alt="Stars"> <img src="https://img.shields.io/github/stars/oxpoe/open-isp?style=flat" alt="Stars">
@@ -108,7 +110,7 @@ Dukungan sukarela tetap diterima lewat menu **Dukung OPEN-ISP** di aplikasi.
## Teknologi ## Teknologi
Node.js · Express · EJS · better-sqlite3 (SQLite) · Bootstrap 5 + Bootstrap Icons (vendor lokal) · Leaflet · PWA/Service Worker · sharp · net-snmp · MikroTik RouterOS API · Baileys / GoWA · Telegram Bot API. Node.js · Express · EJS · better-sqlite3 (SQLite) · Bootstrap 5 + Bootstrap Icons (vendor lokal) · Leaflet · PWA/Service Worker · sharp · net-snmp · MikroTik RouterOS API · GoWA (WhatsApp gateway) · Telegram Bot API.
## Lisensi &amp; Kredit ## Lisensi &amp; Kredit
+23 -5
View File
@@ -11,6 +11,18 @@ const _jimpMod = require('jimp');
const Jimp = _jimpMod.Jimp || _jimpMod; const Jimp = _jimpMod.Jimp || _jimpMod;
const qrisUtil = require('./utils/qrisUtil'); const qrisUtil = require('./utils/qrisUtil');
const { logger } = require('./config/logger'); const { logger } = require('./config/logger');
// Semua console.* (server) diarahkan ke logger agar log seragam & tersimpan di logs/.
// Dipasang SETELAH logger dibuat: winston sudah mengikat console asli -> tanpa rekursi.
const _logFmt = (x) => {
if (x instanceof Error) return x.stack || x.message;
if (x && typeof x === 'object') { try { return JSON.stringify(x); } catch (e) { return String(x); } }
return typeof x === 'undefined' ? 'undefined' : String(x);
};
for (const [m, lvl] of [['log', 'info'], ['info', 'info'], ['warn', 'warn'], ['error', 'error'], ['debug', 'debug']]) {
console[m] = (...args) => logger[lvl](args.map(_logFmt).join(' '));
}
const db = require('./config/database'); const db = require('./config/database');
const customerSvc = require('./services/customerService'); const customerSvc = require('./services/customerService');
const noticeSvc = require('./services/serviceNoticeService'); const noticeSvc = require('./services/serviceNoticeService');
@@ -55,11 +67,12 @@ const app = express();
// Kompresi gzip (performa: respons HTML/CSS/JS lebih kecil) // Kompresi gzip (performa: respons HTML/CSS/JS lebih kecil)
app.use(compression()); app.use(compression());
app.use('/speedtest', require('./routes/speedtest'));
const isProduction = process.env.NODE_ENV === 'production'; const isProduction = process.env.NODE_ENV === 'production';
const cookieSecure = getSetting('cookie_secure', isProduction); const cookieSecure = getSetting('cookie_secure', isProduction);
const trustProxySetting = getSetting('trust_proxy', true); const trustProxySetting = getSetting('trust_proxy', true);
app.set('trust proxy', trustProxySetting ? 1 : true); app.set('trust proxy', trustProxySetting ? 1 : false);
// Middleware dasar // Middleware dasar
app.use(express.json({ app.use(express.json({
@@ -165,7 +178,7 @@ app.use((req, res, next) => {
res.locals.uptimeUrl = getSetting('uptime_url', ''); res.locals.uptimeUrl = getSetting('uptime_url', '');
res.locals.heroVer = getSetting('landing_hero_version', ''); res.locals.heroVer = getSetting('landing_hero_version', '');
res.locals.projectUrl = String(getSetting('project_url', 'https://github.com/oxpoe/open-isp') || '').replace(/\/+$/, ''); res.locals.projectUrl = String(getSetting('project_url', 'https://github.com/oxpoe/open-isp') || '').replace(/\/+$/, '');
res.locals.appVersion = String(getSetting('app_version', '0.5') || '0.5'); res.locals.appVersion = String(getSetting('app_version', '0.5.1') || '0.5.1');
res.locals.appVersionName = String(getSetting('app_version_name', 'pickle-lime') || 'pickle-lime'); res.locals.appVersionName = String(getSetting('app_version_name', 'pickle-lime') || 'pickle-lime');
res.locals.pwaVersion = String(getSetting('pwa_version', '2.0') || '2.0'); res.locals.pwaVersion = String(getSetting('pwa_version', '2.0') || '2.0');
next(); next();
@@ -1094,7 +1107,7 @@ app.post('/api/webhook/v1/payment-notif', multer().any(), async (req, res) => {
// Variabel global untuk modul lain yang masih membaca konfigurasi (mis. skrip utilitas) // Variabel global untuk modul lain yang masih membaca konfigurasi (mis. skrip utilitas)
global.appSettings = { global.appSettings = {
port: getSetting('server_port', 4555), port: getSetting('server_port', 3001),
host: getSetting('server_host', 'localhost'), host: getSetting('server_host', 'localhost'),
genieacsUrl: getSetting('genieacs_url', 'http://localhost:7557'), genieacsUrl: getSetting('genieacs_url', 'http://localhost:7557'),
genieacsUsername: getSetting('genieacs_username', ''), genieacsUsername: getSetting('genieacs_username', ''),
@@ -1649,7 +1662,7 @@ function buildPwaManifest(scope, startUrl, desc, orientation) {
app.get('/version.txt', (req, res) => { app.get('/version.txt', (req, res) => {
res.type('text/plain; charset=utf-8'); res.type('text/plain; charset=utf-8');
res.set('Cache-Control', 'no-store, no-cache, must-revalidate'); res.set('Cache-Control', 'no-store, no-cache, must-revalidate');
const billing = String(getSetting('app_version', '0.5') || '0.5'); const billing = String(getSetting('app_version', '0.5.1') || '0.5.1');
const nameV = String(getSetting('app_version_name', 'pickle-lime') || 'pickle-lime'); const nameV = String(getSetting('app_version_name', 'pickle-lime') || 'pickle-lime');
const pwaV = String(getSetting('pwa_version', '2.0') || '2.0'); const pwaV = String(getSetting('pwa_version', '2.0') || '2.0');
const stUrl = String(getSetting('speedtest_url', '') || ''); const stUrl = String(getSetting('speedtest_url', '') || '');
@@ -2198,7 +2211,7 @@ logger.info(`Attempting to start server on configured port: ${port}`);
startServer(port); startServer(port);
if (getSetting('whatsapp_enabled', false)) { if (getSetting('whatsapp_enabled', false)) {
const gwType = getSetting('wa_gateway_type', 'baileys'); const gwType = getSetting('wa_gateway_type', 'gowa');
if (gwType === 'baileys') { if (gwType === 'baileys') {
import('./services/whatsappBot.mjs') import('./services/whatsappBot.mjs')
.then((mod) => mod.startWhatsAppBot()) .then((mod) => mod.startWhatsAppBot())
@@ -2230,3 +2243,8 @@ app.use(errorHandler);
// Export app untuk testing // Export app untuk testing
module.exports = app; module.exports = app;
// isolir redirect server embedded (port 8099) — untuk container/reverse-proxy; opt-out: ISOLIR_REDIRECT_EMBEDDED=0
if (String(process.env.ISOLIR_REDIRECT_EMBEDDED || '1') !== '0') {
try { require('./isolir-redirect-server.js'); } catch (e) { console.error('[isolir-redirect]', e.message); }
}
+21 -1
View File
@@ -10,6 +10,26 @@ if (!fs.existsSync(dbDir)) fs.mkdirSync(dbDir, { recursive: true });
const dbPath = path.join(dbDir, 'billing.db'); const dbPath = path.join(dbDir, 'billing.db');
// Terapkan restore DB yang ditunda SEBELUM koneksi dibuka.
// (menimpa billing.db di bawah koneksi hidup bisa merusak DB karena WAL bercampur)
const _pendingRestore = path.join(dbDir, '.restore-pending');
if (fs.existsSync(_pendingRestore)) {
try {
const _bkName = fs.readFileSync(_pendingRestore, 'utf8').trim();
const _bkPath = path.join(__dirname, '..', 'backups', _bkName);
if (_bkName && fs.existsSync(_bkPath)) {
fs.copyFileSync(_bkPath, dbPath);
for (const _ext of ['-wal', '-shm']) { try { fs.unlinkSync(dbPath + _ext); } catch (e) {} }
console.log('[DB] Restore tertunda diterapkan dari ' + _bkName);
} else {
console.error('[DB] Restore tertunda dilewati, backup tidak ada: ' + _bkName);
}
} catch (e) {
console.error('[DB] Gagal menerapkan restore tertunda:', e.message);
}
try { fs.unlinkSync(_pendingRestore); } catch (e) {}
}
let db; let db;
try { try {
db = new Database(dbPath); db = new Database(dbPath);
@@ -444,7 +464,7 @@ db.exec(`
CREATE TABLE IF NOT EXISTS wa_chat_messages ( CREATE TABLE IF NOT EXISTS wa_chat_messages (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
direction TEXT NOT NULL DEFAULT 'outbound', direction TEXT NOT NULL DEFAULT 'outbound',
gateway TEXT NOT NULL DEFAULT 'baileys', gateway TEXT NOT NULL DEFAULT 'gowa',
sender_phone TEXT, sender_phone TEXT,
recipient_phone TEXT, recipient_phone TEXT,
customer_id INTEGER REFERENCES customers(id) ON DELETE SET NULL, customer_id INTEGER REFERENCES customers(id) ON DELETE SET NULL,
+4 -4
View File
@@ -27,11 +27,11 @@ const logger = winston.createLogger({
transports: [ transports: [
// Log ke file // Log ke file
new winston.transports.File({ new winston.transports.File({
filename: path.join(logsDir, 'error.log'), filename: path.join(logsDir, 'error.log'), maxsize: 10485760, maxFiles: 5,
level: 'error' level: 'error'
}), }),
new winston.transports.File({ new winston.transports.File({
filename: path.join(logsDir, 'combined.log') filename: path.join(logsDir, 'combined.log'), maxsize: 10485760, maxFiles: 5
}), }),
// Log ke console // Log ke console
new winston.transports.Console({ new winston.transports.Console({
@@ -46,7 +46,7 @@ const logger = winston.createLogger({
], ],
exceptionHandlers: [ exceptionHandlers: [
new winston.transports.File({ new winston.transports.File({
filename: path.join(logsDir, 'exceptions.log') filename: path.join(logsDir, 'exceptions.log'), maxsize: 10485760, maxFiles: 5
}), }),
new winston.transports.Console({ new winston.transports.Console({
format: winston.format.combine( format: winston.format.combine(
@@ -60,7 +60,7 @@ const logger = winston.createLogger({
], ],
rejectionHandlers: [ rejectionHandlers: [
new winston.transports.File({ new winston.transports.File({
filename: path.join(logsDir, 'rejections.log') filename: path.join(logsDir, 'rejections.log'), maxsize: 10485760, maxFiles: 5
}) })
] ]
}); });
+7
View File
@@ -0,0 +1,7 @@
// Lokasi file settings.json — dapat diarahkan via env SETTINGS_PATH
// (dipakai agar settings bisa disimpan di direktori yang di-mount / persisten).
const path = require('path');
const settingsPath = process.env.SETTINGS_PATH
? path.resolve(process.env.SETTINGS_PATH)
: path.join(__dirname, '..', 'settings.json');
module.exports = { settingsPath };
+2 -2
View File
@@ -17,7 +17,7 @@ let settingsCacheTime = 0;
const CACHE_DURATION = 2000; // 2 detik const CACHE_DURATION = 2000; // 2 detik
// File system watcher untuk auto-reload settings // File system watcher untuk auto-reload settings
const settingsPath = path.join(__dirname, '../settings.json'); const { settingsPath } = require('./paths');
let watcher = null; let watcher = null;
// Helper untuk baca settings.json secara dinamis // Helper untuk baca settings.json secara dinamis
@@ -99,7 +99,7 @@ function startSettingsWatcher() {
try { try {
const s = getSettingsWithCache(); const s = getSettingsWithCache();
const port = s.server_port ?? 4555; const port = s.server_port ?? 3001;
const host = s.server_host || 'localhost'; const host = s.server_host || 'localhost';
const gurl = s.genieacs_url || '(tidak diatur)'; const gurl = s.genieacs_url || '(tidak diatur)';
const company = s.company_header || '(default)'; const company = s.company_header || '(default)';
-274
View File
@@ -1,274 +0,0 @@
# Analisis Komprehensif Sistem, Logika Backend, dan UI/UX — Billing RT/RW Net
> **Target Project**: `/opt/billing-rtrw` (VPS `deneva` — `116.212.74.115`)
> **Layanan**: `billing-rtrw.service` (Node.js v20+, port `3001`)
> **Repository Asal**: `https://github.com/alijayanet/billing-rtrw`
> **Repository Fork/Deploy**: `https://github.com/alijayanet/billing-rtrw`
---
## 1. Ringkasan Eksekutif & Gambaran Sistem
Billing RT/RW Net ini adalah sistem manajemen operasional ISP (Internet Service Provider) berbasis komunitas yang memadukan:
1. **Billing & Invoicing**: Siklus tagihan bulanan, isolir otomatis, dan pembayaran QRIS dinamis/manual.
2. **Network Automation**: Integrasi RouterOS MikroTik (PPPoE, Hotspot Voucher, Simple Queue, Address List).
3. **Hardware Management (FTTH)**: Embedded ACS TR-069 Server (CWMP) untuk manajemen ONT klien dan SNMP OLT (EPON/GPON).
4. **Customer Self-Service (PWA)**: Portal pelanggan mobile-first dengan auto-login 90 hari, ganti WiFi SSID/password mandiri, grafik pemakaian kuota/bandwidth, dan reboot modem.
5. **Multi-Channel Bot**: Bot WhatsApp (Baileys socket) dan Bot Telegram untuk notifikasi, penagihan, dan tiket operasional.
```mermaid
flowchart TD
subgraph Klien_Pelanggan["Portal Klien & Interaksi"]
CustPWA["Customer PWA (Tailwind + Lucide)"]
PublicVoucher["Portal Voucher & QRIS"]
WABot["WhatsApp Bot (Baileys)"]
TeleBot["Telegram Bot Notifikasi"]
end
subgraph Core_Backend["Express 4 Engine (Port 3001)"]
App["app-customer.js (Main Entry)"]
ACSServer["Embedded TR-069 Server (/acs)"]
AuthSvc["Auth & Token Service (90 Hari)"]
BillSvc["Billing & Invoice Engine"]
MikroSvc["MikroTik RouterOS API"]
OLTSvc["OLT Service (SNMP / Web)"]
UsageSvc["Bandwidth Usage Tracker"]
end
subgraph Data_Network["Infrastruktur Jaringan & Database"]
DB[("SQLite WAL (better-sqlite3)")]
MikroTik[("Router MikroTik (PPPoE / Hotspot)")]
OLT[("OLT Fiber (Hioso/HSGQ/VSOL)")]
CPE[("ONT / Modem Pelanggan (TR-069)")]
end
CustPWA -->|HTTP/REST| App
PublicVoucher -->|HTTP/REST| App
WABot <--> App
TeleBot <--> App
App --> AuthSvc
App --> BillSvc
App --> MikroSvc
App --> OLTSvc
App --> UsageSvc
CPE <-->|CWMP SOAP| ACSServer
ACSServer <--> DB
BillSvc <--> DB
MikroSvc <--> MikroTik
OLTSvc <--> OLT
UsageSvc <--> MikroTik
```
---
## 2. Analisis Backend & Logika Alur Fungsi
### 2.1 Arsitektur Server & Runtime
* **Runtime**: Node.js `>=20.0.0` (Native ES Module + CommonJS hybrid).
* **Database Engine**: SQLite 3 melalui `better-sqlite3` dengan mode **WAL (Write-Ahead Logging)**, foreign keys aktif, sinkronisasi file cepat dan hemat resource.
* **Proses Lifecycle**: Dikelola oleh Systemd (`billing-rtrw.service`) dengan auto-restart `on-failure` (delay 5 detik), batas memori RAM `--max-old-space-size=512`.
* **Proteksi Server**:
* Penanganan `unhandledRejection` dan `uncaughtException` terpusat agar error koneksi (misal timeout MikroTik) tidak mematikan server Express.
* Fallback port otomatis (`startServer` mencoba port +1000 jika port 3001 bentrok).
* Proteksi CSRF berbasis validasi header `Origin` dan `Referer` (mengecualikan webhook dan endpoint SOAP ACS).
---
### 2.2 Modul-Modul Inti & Alur Logika
#### A. Layanan Billing, Invoice & Isolir (`services/billingService.js` & `services/cronService.js`)
* **Logika Pembuatan Tagihan**:
1. Cron berjalan setiap hari (`cronService.js`) memeriksa seluruh pelanggan aktif.
2. Tagihan dibuat otomatis setiap siklus tanggal tagihan (`billing_date`) untuk periode bulan berjalan.
3. Kode unik nominal QRIS (`qris_amount_unique`) diinjeksi 3 digit acak (1–999) agar mutasi bank/QRIS dapat dicocokkan otomatis tanpa payment gateway berbayar.
* **Logika Isolir Otomatis**:
1. Bila tanggal saat ini mencapai atau melewati `isolir_day` (default tanggal 20 tiap bulan) dan tagihan pelanggan masih `unpaid`:
2. Status pelanggan di database diubah menjadi `isolated`.
3. `mikrotikService` dipanggil untuk memindahkan secret PPPoE ke profile isolir atau memasukkan IP klien ke Address List `ISOLIR` di MikroTik Firewall.
4. Sesi aktif diputus (`disconnectPPPoE`) sehingga pelanggan ter-redirect ke halaman Walled Garden (`/isolated`).
#### B. Layanan TR-069 ACS Server (`services/acsServerService.js` & `/acs`)
* **Protokol**: CWMP (CPE WAN Management Protocol) via SOAP XML over HTTP POST.
* **Alur Inform & Handshake**:
1. Modem (ONT) mengirim HTTP POST `Inform` ke endpoint `/acs`.
2. Server memeriksa header `Authorization`. Jika belum diautentikasi, server membalas dengan **HTTP 401 Unauthorized** (Digest Challenge).
3. Modem mengirim kembali request dengan Digest Hash yang dihitung berdasarkan username (`skyfiber`) dan password acak perangkat.
4. Server membaca parameter XML (`Device.DeviceInfo.*`, `Device.WANDevice.*`, `Device.Ethernet.*`, `Device.LANDevice.*`).
5. Data kekuatan redaman optik (`RXPower`, `TXPower`), serial number, model, dan versi software disimpan ke tabel `acsdevices`.
6. Server memeriksa antrean tugas (task queue) per perangkat: jika ada perintah ganti password WiFi atau reboot, server merespons dengan `SetParameterValues` atau `Reboot`.
#### C. Pengelolaan WiFi Multi-Vendor (`services/customerDeviceService.js`)
* **Masalah Klasik**: Tiap vendor modem (Huawei, ZTE, FiberHome, Nokia) memiliki skema OID/TR-098 yang berbeda-beda. Mengirim parameter yang salah menyebabkan SOAP Fault 9007.
* **Logika Adaptif Per-Vendor**:
* **Huawei / FiberHome / Nokia**: Memprioritaskan `PreSharedKey.1.KeyPassphrase` dan menghindari pengiriman hex 64-digit bersamaan dengan plain string.
* **ZTE / ZICG / CIOT**: Memprioritaskan `KeyPassphrase` top-level.
* **Dual-Band SSID**: Mengatur `WLANConfiguration.1.SSID` (2.4 GHz) dan `WLANConfiguration.5.SSID` (5 GHz) secara independen tanpa merusak konfigurasi guest SSID.
#### D. Integrasi Jaringan MikroTik (`services/mikrotikService.js`)
* **Koneksi**: RouterOS API (port default `25722`/`8728`) menggunakan library `routeros-client`.
* **Fitur Utama**:
* Sinkronisasi PPPoE: Read active connections, update secret passwords, kick active sessions.
* Monitoring Traffic: Mengambil data byte RX/TX interface pelanggan untuk kalkulasi kuota.
* Voucher Hotspot: Direct injection user hotspot, profile rate-limit, dan validitas voucher.
#### E. Autentikasi Pelanggan & Persistent Token (`services/customerAuthTokenService.js`)
* **Alur Sesi PWA**:
1. Pelanggan memasukkan nomor HP / ID Pelanggan.
2. Saat login berhasil, backend men-generate token kriptografi acak `crypto.randomBytes(32).toString('hex')`.
3. Token di-hash dengan SHA-256 dan disimpan di tabel SQLite `customer_auth_tokens` dengan masa berlaku 90 hari.
4. Plain token dikirimkan ke browser sebagai cookie `customer_remember` (`httpOnly`, `secure`, `sameSite: lax`).
5. Ketika pengguna membuka PWA (bahkan setelah browser/HP di-restart), middleware auto-login memverifikasi cookie ini dan merekonstruksi session secara transparan.
#### F. Pelacakan Kuota & Pemakaian Bandwidth (`services/usageService.js`)
* **Logika Agregasi**:
1. Mengambil byte delta dari MikroTik queue / interface.
2. Menyimpan sampel agregat harian dan bulanan ke database.
3. Menyediakan API untuk konsumsi frontend (Chart.js di dashboard pelanggan).
---
## 3. Analisis UI / UX, Styling, Typografi & Layout
Pemeriksaan mendalam terhadap template EJS dan file CSS/JS di `/opt/billing-rtrw/views/` dan `/opt/billing-rtrw/public/` mengungkap sejumlah temuan penting mengenai styling, engine, dan konsistensi desain:
### 3.1 Peta Engine & Library UI per Portal
| Portal / Halaman | Engine CSS Utama | Library Icon | Font Family Utama | Style Pendekatan |
|---|---|---|---|---|
| **Customer Dashboard** (`views/dashboard.ejs`) | **Tailwind CSS 3.4.17** (via CDN) | **Lucide Icons** (v1.48.0) + FontAwesome 6.4.0 | **Plus Jakarta Sans** (Google Fonts) | Modern App Shell / Glassmorphism Mobile-first |
| **Customer Login** (`views/login.ejs`) | **Bootstrap 5.3.2** (CDN) | **Bootstrap Icons 1.11.3** | **System Font Stack** (`-apple-system`, Segoe UI, Roboto) | Centered Glass Card / Radial Background |
| **Admin Portal** (`views/admin/*`) | **Custom CSS** (`/css/admin.css`) | **Bootstrap Icons 1.11.3** + FontAwesome | **Inter** (Google Fonts) | Desktop Enterprise Sidebar & Table Layout |
| **Teknisi Portal** (`views/tech/*`) | **Bootstrap 5** + `/css/theme.css` | **Bootstrap Icons** | System Font / Inter | Mobile Bottom Navigation |
| **Halaman Peta Jaringan** (`views/admin/map.ejs`) | **Leaflet 1.9.4** + `/css/admin.css` | FontAwesome + SVG Animasi | Inter | Fullscreen Interactive Map & Vector Layer |
---
### 3.2 Analisis Typografi & Hierarki Font
```
[Customer Dashboard] Plus Jakarta Sans (300, 400, 500, 600, 700, 800)
├── Title / Header : text-base sm:text-lg font-bold (Tracking tight)
├── Section Labels : text-xs font-semibold text-slate-400 uppercase (Tracking wider)
├── Body / Konten : text-sm font-normal text-slate-200 (Leading relaxed)
└── Badges & Metadata : text-[10px] sm:text-xs font-medium
[Customer Login] System Font Stack (BlinkMacSystemFont, Segoe UI, Roboto)
└── Ketidaksesuaian : Font berbeda dengan Dashboard; saat user login terjadi visual jump dari font sistem ke Plus Jakarta Sans.
[Admin Portal] Inter (300, 400, 500, 600, 700, 800)
├── Sidebar Items : font-size 13px, font-weight 500
├── Data Table Header : font-size 11px, text-transform uppercase, letter-spacing 0.5px
└── Table Data Cells : font-size 13px, color var(--text: #e6edf3)
```
---
### 3.3 Analisis Styling, Palette Warna & Tema Gelap/Terang
#### Palette Warna Dominan
1. **Mode Gelap (Customer Dashboard)**:
* Background Utama: `#0b0f19` (Deep Blue-Black)
* Card & Panel: `#1e293b` (Slate 800) dengan border `#334155` (Slate 700)
* Aksen Primer (Brand): Gradasi Teal `#14b8a6` ke Cyan `#06b6d4`
* Aksen Sekunder: Indigo `#6366f1`
* Status Indikator: Hijau Sukses `#10b981`, Merah Warning/Error `#f43f5e`, Amber `#f59e0b`
2. **Mode Terang (Customer Dashboard & Global Theme)**:
* Background Utama: `#f1f5f9` (Slate 100)
* Card & Panel: `#ffffff` murni dengan border `rgba(15, 23, 42, 0.08)`
* Teks Utama: `#0f172a` (Slate 900), Teks Muted: `#64748b` (Slate 500)
3. **Mekanisme Tema (Theming Engine)**:
* Terdapat dua sistem tema yang berjalan berdampingan:
* `views/dashboard.ejs` menggunakan script lokal inline (`localStorage: skyfiber-customer-theme`) mengendalikan class `dark` pada `<html>` untuk Tailwind.
* Portal lainnya menggunakan `/js/theme.js` dan `/css/theme.css` (`localStorage: app-theme`) mengendalikan atribut `data-theme="light|dark"`.
* Pada rilis `v1.9.4`, kedua key ini sudah disinkronkan, namun styling mode terang di portal admin masih membutuhkan penyesuaian kontras pada beberapa tabel dan modal.
---
### 3.4 Analisis Layout, Spacing, Alignment & UX
#### 1. Customer Dashboard (Mobile App Shell)
* **Alignment & Container**:
* Lebar maksimum dibatasi ke `max-w-md` atau `max-w-lg` di tengah layar (`mx-auto`), memberikan rasa native mobile application saat dibuka di smartphone maupun desktop browser.
* Padding konten konsisten (`px-4 py-3`), dengan padding bawah ekstra (`pb-28`) untuk memberi ruang bagi floating navigation dock.
* **Floating Glass Dock (5 Tab Navigasi)**:
* Terpasang fixed di bagian bawah layar: `fixed bottom-4 left-1/2 -translate-x-1/2`.
* Menggunakan efek glassmorphism (`backdrop-blur-md`, background semi-transparan, border tipis halus).
* Indikator aktif berupa **sliding pill** yang bergerak halus mengikuti tab yang diklik (`updatePillPosition`).
* Tab navigasi:
1. `tab-beranda` (Home, status paket, kuota, shortcut aksi)
2. `tab-router` (Status ONT, redaman fiber optic, ganti WiFi, reboot)
3. `tab-ppob` (Layanan isi pulsa & token listrik - status Coming Soon)
4. `tab-tagihan` (Riwayat invoice, tombol bayar QRIS)
5. `tab-profile` (Data akun, riwayat tiket kendala, tombol CS WhatsApp, logout)
* **Kerapian & Touch Target**:
* Seluruh tombol interaktif memiliki ukuran touch-target minimal `40x40px` (ramah sentuhan jari di layar HP).
* Safe-area-insets untuk iPhone (`env(safe-area-inset-bottom)`) sudah ditambahkan pada `.dock-glass`.
#### 2. Admin & Staff Portal
* **Layout**:
* Sidebar navigasi di sisi kiri (`width: 240px`, fixed).
* Konten utama di sebelah kanan dengan header fixed dan breadcrumb.
* Responsive breakpoint: Pada layar kecil (mobile), sidebar disembunyikan dan diakses lewat toggle menu hamburger.
---
### 3.5 Temuan Evaluasi & Celah UI/UX (Bottlenecks)
1. **Fragmentasi Framework CSS**:
* Halaman login menggunakan **Bootstrap 5**, sedangkan dashboard pelanggan menggunakan **Tailwind CSS**. Ini membebani browser pengguna karena harus mengunduh dua framework CSS besar saat pertama kali membuka aplikasi.
2. **Ketergantungan CDN Runtime (Client-Side Tailwind)**:
* Dashboard pelanggan menggunakan `<script src="https://cdn.tailwindcss.com/3.4.17"></script>`. CDN ini mem-parse class Tailwind saat runtime di perangkat klien (JIT compiler di browser), yang dapat memperlambat rendering awal (First Contentful Paint) pada ponsel berspesifikasi rendah.
3. **Dual Icon Libraries**:
* Halaman dashboard memuat **Lucide Icons** dan **FontAwesome 6.4.0** secara bersamaan. FontAwesome (file CSS + font ~400KB) hanya digunakan untuk segelintir ikon kecil, sementara sebagian besar UI sudah menggunakan SVG Lucide.
4. **Log Error Berulang di Backend**:
* Terdapat log `Telegram Polling Error` berkala pada journal server deneva karena token bot belum dikonfigurasi / timeout koneksi ke server Telegram API.
---
## 4. Rencana Kerja Terperinci (Execution Plan)
Rencana perubahan ini dibagi menjadi 4 fase berurutan dengan prinsip **zero-downtime** dan keamanan data:
```mermaid
timeline
title Timeline Rencana Peningkatan Billing RTRW
Fase 1 : Konsolidasi Backend & Error Fix : Fix Polling Telegram : Optimalisasi Error Handling
Fase 2 : Unifikasi Design System : Standarisasi Font (Plus Jakarta Sans) : Harmonisasi Halaman Login & Dashboard
Fase 3 : Optimasi Asset & Performa : Eliminasi Redundansi Icon : Cache Profiling & PWA Hardening
Fase 4 : Testing & Validasi : Uji Responsivitas Semua Layar : Uji Fungsi WiFi & Pembayaran Live
```
### Fase 1: Perbaikan Stabilitas & Logika Backend
1. **Fix Polling Telegram (`services/telegramBot.js`)**:
* Tambahkan pengecekan kondisi: jika token Telegram kosong, nonaktifkan `polling: true` agar tidak membanjiri log server dengan polling error setiap 5 detik.
2. **Peningkatan Ketahanan Koneksi MikroTik (`services/mikrotikService.js`)**:
* Implementasikan retry backoff otomatis saat query queue/PPPoE timeout, menghindari uncaught exception yang tidak perlu.
3. **Audit Query Database (`config/database.js`)**:
* Periksa indeks pada tabel `pppoe_traffic_samples`, `invoices`, dan `customer_auth_tokens` untuk mempercepat query bulanan.
### Fase 2: Harmonisasi UI/UX & Standarisasi Tipografi
1. **Standarisasi Tipografi**:
* Terapkan font **Plus Jakarta Sans** ke seluruh halaman pelanggan, termasuk halaman Login (`views/login.ejs`), Register, dan Isolir (`views/isolated.ejs`), menggantikan system font default agar pengalaman visual konsisten sejak awal.
2. **Harmonisasi Desain Halaman Login (`views/login.ejs`)**:
* Rombak halaman login agar memiliki nuansa tema, sudut rounded, dan aksen warna Teal/Cyan yang identik dengan Dashboard Pelanggan.
* Ganti elemen Bootstrap pada halaman login menjadi layout yang selaras dengan tema dashboard.
3. **Penyempurnaan Form Ganti WiFi di Tab Router**:
* Perjelas instruksi frekuensi 2.4 GHz vs 5 GHz.
* Tambahkan toggle visibility (mata) untuk password WiFi dan validasi panjang password (minimal 8 karakter) di sisi klien sebelum submit.
### Fase 3: Optimasi Frontend & Efisiensi Aset
1. **Rasionalisasi Icon**:
* Migrasikan sisa ikon FontAwesome di Dashboard pelanggan ke **Lucide Icons** bawaan.
* Hapus pemanggilan CSS FontAwesome di dashboard pelanggan untuk menghemat ukuran transfer data hingga ~400 KB.
2. **Optimasi Mode Terang (Light Mode)**:
* Periksa dan rapikan kontras teks di seluruh tab dashboard agar tidak ada teks putih yang tidak terbaca saat berganti ke background terang.
* Pastikan border card dan form input terlihat tegas pada mode terang.
### Fase 4: Pengujian, Validasi & Deployment
1. **Uji Coba Lintas Perangkat**:
* Uji tampilan di mobile (Android Chrome, iOS Safari), tablet, dan desktop.
* Uji fungsi PWA Add to Home Screen dan verifikasi auto-login setelah kill aplikasi.
2. **Uji Fungsional End-to-End**:
* Uji ganti SSID/password pada modem online.
* Uji alur pembuatan invoice dan QRIS.
* Restart service `billing-rtrw.service` dan verifikasi log bersih tanpa error.
-317
View File
@@ -1,317 +0,0 @@
# AUDIT FINDINGS SUMMARY - QUICK REFERENCE
**Status:** Complete Audit
**Date:** 14 August 2026
**Format:** Executive Summary for Management
---
## 🎯 AUDIT OVERVIEW
| Metric | Status | Details |
|--------|--------|---------|
| **Security Score** | 5.5/10 | ⚠️ Critical issues found |
| **Code Quality** | 7/10 | Good structure, needs hardening |
| **Database Integrity** | 8/10 | Well-designed schema |
| **Production Ready** | ❌ NO | Must fix critical issues first |
| **Audit Completed** | ✅ YES | Full report available |
---
## 🔴 CRITICAL FINDINGS (MUST FIX IMMEDIATELY)
### 1. PASSWORD STORAGE (CRITICAL)
- **Issue:** Passwords stored in plaintext in database
- **Risk:** Full account compromise if database breached
- **Files:** `services/techService.js`, `settings.json`
- **Fix Time:** 4-6 hours
- **Severity:** CRITICAL
- **Status:** Not Fixed ❌
### 2. DEFAULT CREDENTIALS (CRITICAL)
- **Issue:** Default admin password `admin123` in settings.json
- **Risk:** Anyone can login to admin panel
- **Fix Time:** 1 hour
- **Severity:** CRITICAL
- **Status:** Not Fixed ❌
### 3. UNENCRYPTED CREDENTIALS (CRITICAL)
- **Issue:** API keys and passwords in plaintext in settings.json
- **Risk:** Exposure in version control, server breach
- **Files:** `settings.json`
- **Fix Time:** 2-3 hours
- **Severity:** CRITICAL
- **Status:** Not Fixed ❌
### 4. WEAK PASSWORD HASHING (HIGH)
- **Issue:** SHA256 and MD5 used instead of bcrypt/argon2
- **Risk:** Password cracking in hours/minutes
- **Files:** Multiple service files
- **Fix Time:** 6-8 hours
- **Severity:** HIGH
- **Status:** Not Fixed ❌
---
## 🟠 HIGH PRIORITY FINDINGS
### 5. CSRF PROTECTION (LIMITED)
- **Issue:** Only Referer/Origin validation, no explicit CSRF tokens
- **Risk:** Cross-site request forgery attacks possible
- **Fix Time:** 4-6 hours
- **Severity:** HIGH
### 6. SESSION MANAGEMENT
- **Issue:** In-memory session store, no regeneration on login
- **Risk:** Session hijacking possible
- **Fix Time:** 3-4 hours
- **Severity:** HIGH
### 7. INPUT VALIDATION
- **Issue:** Incomplete input validation and sanitization
- **Risk:** XSS and injection attacks
- **Fix Time:** 8-10 hours
- **Severity:** HIGH
---
## 🟡 MEDIUM PRIORITY FINDINGS
### 8. FILE UPLOAD SECURITY
- **Issue:** Only MIME type validation, no deep file scanning
- **Risk:** Malicious file upload
- **Fix Time:** 2-4 hours
- **Severity:** MEDIUM
### 9. BACKUP & DISASTER RECOVERY
- **Issue:** Auto-backup disabled in settings
- **Risk:** Data loss in disaster scenario
- **Fix Time:** 2-3 hours
- **Severity:** MEDIUM
### 10. ERROR LOGGING
- **Issue:** Some errors may expose sensitive info
- **Risk:** Information disclosure
- **Fix Time:** 2-3 hours
- **Severity:** MEDIUM
---
## ✅ POSITIVE FINDINGS
### What's Working Well:
- ✅ Database design is solid with proper constraints
- ✅ Error handling middleware implemented
- ✅ Winston logger properly configured
- ✅ SQL queries use parameterized statements (SQL injection safe)
- ✅ File upload has MIME type validation
- ✅ Session cookies have httpOnly and sameSite flags
- ✅ CORS/Origin checking implemented
- ✅ Comprehensive feature set well-structured
---
## 📊 TIMELINE TO PRODUCTION READINESS
```
Week 1 (CRITICAL):
├─ Fix password hashing → 8 hours
├─ Setup .env & remove hardcoded secrets → 4 hours
├─ Implement CSRF tokens → 4 hours
├─ Setup session store → 3 hours
└─ Testing → 4 hours
TOTAL: ~23 hours (3 days)
Week 2 (HIGH):
├─ Input validation framework → 8 hours
├─ Rate limiting → 4 hours
├─ Backup automation → 3 hours
├─ Security headers (helmet) → 2 hours
└─ Testing & QA → 8 hours
TOTAL: ~25 hours (3 days)
Week 3 (MEDIUM):
├─ File upload scanning → 6 hours
├─ Log review & cleanup → 4 hours
├─ Documentation → 4 hours
└─ Staging deployment → 4 hours
TOTAL: ~18 hours (2 days)
GRAND TOTAL: ~66 hours (~2 weeks full-time)
or ~3-4 weeks part-time (20 hrs/week)
```
---
## 💼 BUSINESS IMPACT
### Current State (If Deployed Now):
- 🔴 **NOT RECOMMENDED** for production
- Risk of account takeover is HIGH
- Risk of data breach is CRITICAL
- Compliance risk with regulations (OJK, PCI-DSS if payment processing)
### After Critical Fixes (1-2 weeks):
- 🟢 **PRODUCTION READY** for initial deployment
- Password security improved significantly
- Credentials properly managed
- Basic CSRF protection in place
### After All Fixes (3-4 weeks):
- 🟢 **PRODUCTION HARDENED**
- Enterprise-grade security
- Compliance-ready
- Audit-ready for external review
---
## 👥 RESOURCE REQUIREMENTS
### Team Needed:
- **Backend Developer(s):** 1-2 (main focus)
- **Security Reviewer:** 1 (part-time, 4-6 hours/week)
- **QA/Tester:** 1 (part-time, 2-4 hours/week)
- **DevOps/Infrastructure:** 0.5 (env setup, deployment)
### Skills Required:
- Node.js/JavaScript (Backend)
- Database migration experience
- Security best practices knowledge
- Testing & QA methodologies
---
## 🎯 RECOMMENDED ACTION PLAN
### Phase 1: IMMEDIATE (Days 1-2)
1. ✅ Review this audit with team
2. ✅ Create .env file with sensitive data
3. ✅ Install required packages (bcrypt, helmet, etc)
4. ✅ Backup current database
### Phase 2: CRITICAL FIXES (Days 3-5)
1. ✅ Implement password hashing with bcrypt
2. ✅ Migrate existing passwords
3. ✅ Setup environment variables
4. ✅ Remove hardcoded credentials
### Phase 3: TESTING & VALIDATION (Days 6-7)
1. ✅ Test all authentication flows
2. ✅ Verify password migration
3. ✅ Test environment variable loading
4. ✅ Deploy to staging
### Phase 4: HIGH PRIORITY FIXES (Week 2)
1. ✅ Implement CSRF protection
2. ✅ Setup session store
3. ✅ Add rate limiting
4. ✅ Input validation
### Phase 5: FINAL HARDENING (Week 3)
1. ✅ Security headers (Helmet)
2. ✅ File upload scanning
3. ✅ Backup automation
4. ✅ Final testing
### Phase 6: PRODUCTION DEPLOYMENT (End of Week 3)
1. ✅ Security review checklist
2. ✅ Final audit verification
3. ✅ Deploy to production
4. ✅ Monitoring setup
---
## 📋 DEPLOYMENT CHECKLIST
Before production deployment, verify:
- [ ] **Password Security**
- [ ] Bcrypt implemented and tested
- [ ] All passwords migrated to hashes
- [ ] No plaintext passwords in code/config
- [ ] Login flows working with hashed passwords
- [ ] **Credential Management**
- [ ] .env file created and populated
- [ ] Settings.json reviewed for sensitive data
- [ ] .env added to .gitignore
- [ ] Environment variables working
- [ ] **Access Control**
- [ ] CSRF protection implemented
- [ ] Rate limiting configured
- [ ] Session management hardened
- [ ] Role-based access control verified
- [ ] **Security Headers**
- [ ] Helmet.js installed and configured
- [ ] HTTPS enforced (if applicable)
- [ ] Security headers present in responses
- [ ] **Data Protection**
- [ ] Input validation implemented
- [ ] Output sanitization verified
- [ ] File uploads scanning configured
- [ ] Database encryption (at rest if needed)
- [ ] **Logging & Monitoring**
- [ ] Audit trail logged for sensitive operations
- [ ] Error logging configured
- [ ] Monitoring alerts setup
- [ ] Log aggregation configured
- [ ] **Backup & Recovery**
- [ ] Automated backup enabled
- [ ] Backup encryption enabled
- [ ] Recovery procedure tested
- [ ] Backup retention policy set
- [ ] **Testing**
- [ ] Unit tests passing
- [ ] Integration tests passing
- [ ] Security tests completed
- [ ] Load testing completed
- [ ] Penetration testing (optional but recommended)
- [ ] **Documentation**
- [ ] Security practices documented
- [ ] Incident response plan ready
- [ ] Setup procedures documented
- [ ] Troubleshooting guide prepared
---
## 🚨 KNOWN VULNERABILITIES
| Vulnerability | CVSS Score | Status | Action |
|---|---|---|---|
| Plaintext password storage | 9.8 | Open | CRITICAL - Fix immediately |
| Default credentials | 8.7 | Open | CRITICAL - Fix immediately |
| Unencrypted API keys | 8.9 | Open | CRITICAL - Fix immediately |
| Weak password hashing | 7.5 | Open | HIGH - Fix this week |
| Limited CSRF protection | 6.5 | Open | HIGH - Fix this week |
| In-memory session store | 6.1 | Open | HIGH - Fix this week |
| Incomplete input validation | 7.1 | Open | HIGH - Fix this week |
---
## 📞 CONTACT & SUPPORT
For questions or clarifications:
1. Review detailed findings in `AUDIT_SECURITY_2026.md`
2. Check implementation guide in `IMPLEMENTATION_GUIDE_SECURITY.md`
3. Consult code samples in this document
---
## 📝 SIGN-OFF
**Audit Completed By:** Security & Code Quality Review Team
**Date Completed:** 14 August 2026
**Next Review:** 30 days after fixes implementation
**Status:** READY FOR ACTION ✅
---
**RECOMMENDATION: DO NOT DEPLOY TO PRODUCTION UNTIL CRITICAL FIXES ARE COMPLETED**
For production deployment, all findings must be reviewed, and critical issues must be resolved.
File diff suppressed because it is too large. Load diff
-833
View File
@@ -1,833 +0,0 @@
# AUDIT KEAMANAN & KUALITAS APLIKASI BILLING RTRWNET
**Status:** ✅ AUDIT LENGKAP
**Tanggal Audit:** 14 Agustus 2026
**Auditor:** Security & Code Quality Review
**Aplikasi Status:** 🟠 PRODUCTION WITH CRITICAL ISSUES
---
## 📋 RINGKASAN EKSEKUTIF
Aplikasi RTRWNET Billing System adalah sistem yang **comprehensive dan feature-rich** untuk manajemen ISP. Namun, audit menemukan **beberapa kerentanan keamanan KRITIS** yang harus segera diperbaiki sebelum production deployment atau jika sudah di production harus segera ditindaklanjuti.
### Skor Keamanan: 5.5/10 🔴
- ✅ Aspek Positif: Struktur kode baik, error handling solid, logging terimplementasi
- ⚠️ Aspek Kritis: Keamanan password, configuration management, input validation
- ❌ Aspek Negatif: Plaintext credentials, default passwords, weak hashing
---
## 🔐 KEAMANAN KRITIS - SEGERA PERBAIKI
### 1. PASSWORD DISIMPAN DALAM PLAINTEXT ❌ CRITICAL
**Lokasi:**
- [services/techService.js](services/techService.js#L4) - Password technician tersimpan plaintext di database
- [settings.json](settings.json#L20-L21) - Admin password `admin123` dalam plaintext
**Masalah:**
```javascript
// ❌ TIDAK AMAN - Line 4 di techService.js
const tech = db.prepare('SELECT * FROM technicians WHERE username = ? AND password = ? AND is_active = 1').get(username, password);
// Password dibandingkan langsung tanpa hashing
// ❌ TIDAK AMAN - settings.json
"admin_username": "admin",
"admin_password": "admin123", // Password plaintext!
```
**Risiko:**
- Jika database bocor, semua password terekspos
- Jika settings.json terlihat, credentials admin langsung ketahuan
- Tidak compliance dengan standar keamanan (OWASP, ISO27001)
- Database breach = total kompromis sistem
**Rekomendasi - HARUS DILAKUKAN:**
1. **Implementasi bcrypt untuk hashing password:**
```javascript
// Install bcrypt terlebih dahulu
// npm install bcrypt
// Saat register/create user:
const bcrypt = require('bcrypt');
const hashedPassword = await bcrypt.hash(plainPassword, 10);
// Simpan hashedPassword ke database
// Saat authenticate:
const isValid = await bcrypt.compare(plainPassword, hashedPassword);
if (isValid) {
// Login berhasil
}
```
2. **Update database schema:**
```sql
-- Tambahkan kolom password_hash
ALTER TABLE technicians ADD COLUMN password_hash TEXT;
ALTER TABLE administrators ADD COLUMN password_hash TEXT;
-- Update existing passwords dengan hash
-- (Lakukan migration script terpisah)
```
3. **Jangan simpan password di settings.json:**
```json
// ❌ SEBELUM
{
"admin_password": "admin123"
}
// ✅ SESUDAH - Gunakan database atau environment variable
{
"admin_user_id": 1 // Reference ke database saja
}
// Atau gunakan .env file:
ADMIN_PASSWORD_HASH="$2b$10$..."
```
**Timeline:** IMMEDIATE - Sebelum production
---
### 2. DEFAULT CREDENTIALS (admin/admin123) ❌ CRITICAL
**Lokasi:** [settings.json](settings.json#L19-L21)
**Masalah:**
- Default username/password yang sangat lemah
- Siapapun yang tahu dokumentasi aplikasi bisa login
- OWASP Top 10: A01:2021 - Broken Access Control
**Rekomendasi:**
```bash
# Script untuk force password change saat first-time setup
node scripts/setup-admin-password.js
# Atau implementasikan setup wizard:
- Saat pertama kali aplikasi dijalankan
- Wajib setup admin username & password baru
- Simpan di database dengan hash bcrypt
- Tandai setup selesai di flag di database
```
**Timeline:** IMMEDIATE
---
### 3. API KEY & CREDENTIALS DALAM PLAINTEXT ❌ CRITICAL
**Lokasi:** [settings.json](settings.json)
```json
"genieacs_password": "admin",
"midtrans_server_key": "",
"xendit_api_key": "",
"tripay_private_key": "",
"digiflazz_api_key": "",
"radius_secret": "secret123"
```
**Masalah:**
- Semua credentials eksternal dalam plaintext di file JSON
- File bisa terlihat di source code management
- Risiko exposure jika server dikompromis
- Tidak praktis untuk berbagai environment (dev/staging/production)
**Rekomendasi - HARUS DILAKUKAN:**
1. **Gunakan environment variables (.env):**
```bash
# File: .env (JANGAN commit ke git!)
GENIEACS_URL=http://192.168.8.189:7557
GENIEACS_USERNAME=admin
GENIEACS_PASSWORD=SecurePassword123!
MIDTRANS_SERVER_KEY=Mid-xxxxx-xxx
XENDIT_API_KEY=xnd_development_xxxxx
TRIPAY_API_KEY=xxxxx
DIGIFLAZZ_API_KEY=xxxxx
RADIUS_SECRET=RadiusSecret123!
SESSION_SECRET=RandomSecure123!SecretHere
ADMIN_API_KEY=AdminApiKey123!Secure
```
2. **Implementasi di code:**
```javascript
// ✅ AMAN
const settings = {
genieacs_password: process.env.GENIEACS_PASSWORD || getSetting('genieacs_password'),
midtrans_server_key: process.env.MIDTRANS_SERVER_KEY,
xendit_api_key: process.env.XENDIT_API_KEY,
// ... dll
};
```
3. **Update .gitignore:**
```bash
# Jangan commit file ini!
.env
.env.local
.env.*.local
settings.json # Alternatif: commit template saja
```
4. **Implementasi encryption untuk settings.json (jika diperlukan):**
```javascript
// Gunakan settingsEncryption.js yang sudah ada
const { encryptValue, decryptValue } = require('./config/settingsEncryption');
// Encrypt credentials:
const encrypted = encryptValue(credentialValue);
// Decrypt saat digunakan:
const plainValue = decryptValue(encryptedValue);
```
**Timeline:** IMMEDIATE
---
### 4. WEAK PASSWORD HASHING ⚠️ HIGH
**Lokasi Lainnya:**
- [config/database.js](config/database.js#L627) - SHA256 untuk password
- [services/agentService.js](services/agentService.js#L27) - MD5 hash
- [routes/adminPortal.js](routes/adminPortal.js#L98) - MD5 hash
**Masalah:**
```javascript
// ❌ TIDAK AMAN - SHA256 bukan untuk password hashing
const hash = crypto.createHash('sha256').update(input).digest('hex');
// ❌ TIDAK AMAN - MD5 sudah deprecated
const sign = crypto.createHash('md5').update(username + apiKey).digest('hex');
```
- SHA256/MD5 = algoritma hashing cepat (bisa di-brute force)
- Tidak ada salt random
- Tidak ada rate limiting pada hashing
- Bisa di-crack dengan GPU dalam hitungan jam
**Rekomendasi:**
```javascript
// ✅ AMAN - Gunakan bcrypt atau Argon2
const bcrypt = require('bcrypt');
const hash = await bcrypt.hash(password, 10); // 10 = cost factor
const isValid = await bcrypt.compare(plainPassword, hash);
// Atau Argon2 (lebih kuat):
const argon2 = require('argon2');
const hash = await argon2.hash(password);
const isValid = await argon2.verify(hash, password);
```
**Timeline:** HIGH - Dalam 1 minggu
---
## 🔒 KEAMANAN SEDANG - TINGKATKAN
### 5. SESSION MANAGEMENT ⚠️ MEDIUM
**Lokasi:** [app-customer.js](app-customer.js#L79-L90)
**Status Saat Ini:** ✅ Baik
```javascript
app.use(session({
secret: getSetting('session_secret', 'default-secret'),
resave: false,
saveUninitialized: false,
cookie: {
secure: Boolean(cookieSecure), // ✅ HTTPS only (jika production)
httpOnly: true, // ✅ JavaScript tidak bisa akses
sameSite: 'lax', // ✅ CSRF protection
maxAge: 24 * 60 * 60 * 1000 // ✅ Timeout 24 jam
},
name: 'customer.sid'
}));
```
**Rekomendasi Improvement:**
1. **Gunakan session store eksternal:**
```javascript
// Jangan gunakan default memory store di production
const SessionStore = require('better-sqlite3-session-store')(session);
const store = new SessionStore({
client: db,
expired: {
clear: true,
intervalMs: 15 * 60 * 1000 // Clear expired sessions tiap 15 menit
}
});
app.use(session({
store: store,
// ... config lainnya
}));
```
2. **Regenerate session ID saat login:**
```javascript
// Saat login berhasil
req.session.regenerate((err) => {
if (err) return res.status(500).json({ error: 'Session error' });
req.session.userId = user.id;
req.session.userRole = user.role;
// ... simpan data user lainnya
res.redirect('/dashboard');
});
```
3. **Validasi session pada setiap request:**
```javascript
app.use((req, res, next) => {
// Cek timeout session
const lastActivity = req.session.lastActivity || Date.now();
const inactivityTimeout = 30 * 60 * 1000; // 30 menit
if (Date.now() - lastActivity > inactivityTimeout) {
req.session.destroy();
return res.status(401).json({ error: 'Session expired' });
}
req.session.lastActivity = Date.now();
next();
});
```
**Timeline:** MEDIUM - 2 minggu
---
### 6. CSRF PROTECTION ⚠️ MEDIUM
**Status Saat Ini:** ⚠️ Terbatas
```javascript
// Hanya check Referer/Origin, tidak ada CSRF token explicit
if (origin) {
const originHost = new URL(origin).host;
if (originHost !== host) {
return res.status(403).json({ error: 'Forbidden - Invalid Origin' });
}
}
```
**Masalah:**
- Referer bisa di-spoof dalam beberapa kasus
- Browser lama atau proxy bisa hilangkan Referer header
- Tidak ada explicit CSRF token di form
**Rekomendasi:**
1. **Implementasi CSRF token library:**
```javascript
const csrf = require('csurf');
const csrfProtection = csrf({ cookie: false }); // Gunakan session, bukan cookie
app.post('/admin/action', csrfProtection, (req, res) => {
// CSRF token sudah tervalidasi otomatis
// Proses request
});
```
2. **Di EJS templates:**
```ejs
<form method="POST" action="/admin/billing/generate">
<input type="hidden" name="_csrf" value="<%= csrfToken %>">
<button type="submit">Generate Invoice</button>
</form>
```
**Timeline:** MEDIUM - 2 minggu
---
### 7. INPUT VALIDATION & SANITIZATION ⚠️ MEDIUM
**Issue:**
Beberapa endpoint tidak melakukan validasi input yang ketat sebelum:
- Memasukkan ke database
- Menampilkan ke user (XSS risk)
- Digunakan dalam query
**Contoh lokasi yang perlu improvement:**
- [routes/adminPortal.js](routes/adminPortal.js) - Bulk operations
- [routes/customerPortal.js](routes/customerPortal.js) - Customer data update
- File upload handling
**Rekomendasi:**
1. **Gunakan input validation library:**
```bash
npm install joi express-validator
```
2. **Implementasi validation:**
```javascript
const { body, validationResult } = require('express-validator');
router.post('/admin/customers/add', [
body('name').trim().notEmpty().isLength({ max: 100 }),
body('phone').trim().isMobilePhone('id-ID'),
body('email').isEmail().normalizeEmail(),
body('address').trim().isLength({ max: 255 }),
body('package_id').isInt({ gt: 0 }),
], (req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
// Lanjutkan proses
});
```
3. **Sanitize output di EJS:**
```ejs
<!-- ❌ TIDAK AMAN - Bisa XSS -->
<%= customer.name %>
<!-- ✅ AMAN - Sanitized -->
<%- cleanHtml(customer.name) %>
<!-- atau gunakan escape by default di EJS config -->
```
**Timeline:** MEDIUM - 2 minggu
---
## ⚠️ KEAMANAN RENDAH - PERHATIAN
### 8. FILE UPLOAD SECURITY ⚠️ LOW
**Status Saat Ini:** ⚠️ Cukup
```javascript
// ✅ Ada validasi MIME type
const allowedTypes = /jpeg|jpg|png|gif|webp/;
const mimetype = allowedTypes.test(file.mimetype);
// ✅ Ada file size limit (5MB)
limits: { fileSize: 5 * 1024 * 1024 }
```
**Rekomendasi Improvement:**
1. **Validasi file lebih ketat:**
```javascript
const FileType = require('file-type');
// Validasi magic bytes (bukan hanya extension)
const fileType = await FileType.fromBuffer(file.buffer);
if (!['image/jpeg', 'image/png', 'image/webp'].includes(fileType?.mime)) {
throw new Error('Invalid file type');
}
```
2. **Scan file dengan antivirus:**
```javascript
const ClamScan = require('clamscan');
const clamscan = await new ClamScan().init({
clamdscan: { host: 'localhost', port: 3310 }
});
const { isInfected } = await clamscan.scanFile(filePath);
if (isInfected) {
fs.unlinkSync(filePath);
throw new Error('Malicious file detected');
}
```
**Timeline:** LOW - Dalam 1 bulan
---
### 9. LOGGING & MONITORING ⚠️ LOW
**Status Saat Ini:** ✅ Baik
- Winston logger terimplementasi dengan baik
- Terpisah antara error.log, combined.log, exceptions.log
- Console output di development
**Rekomendasi:**
1. **Audit trail untuk sensitive operations:**
```javascript
const auditTrailSvc = require('./services/auditTrailService');
// Log setiap action sensitive
auditTrailSvc.logAction({
user_id: req.session.adminId,
action: 'invoice_generated',
resource_type: 'invoice',
resource_id: invoiceId,
changes: { status: 'generated' },
ip_address: req.ip,
timestamp: new Date()
});
```
2. **Centralized log aggregation:**
```javascript
// Contoh: Kirim logs ke ELK Stack atau Datadog
const winston = require('winston');
const WinstonCloudWatch = require('winston-cloudwatch');
logger.add(new WinstonCloudWatch({
logGroupName: 'billing-rtrwnet',
logStreamName: 'production',
awsRegion: 'ap-southeast-1'
}));
```
**Timeline:** LOW - Dalam 1 bulan
---
## 🎯 KUALITAS KODE
### 10. ERROR HANDLING ✅ BAIK
**Status:** ✅ Baik
- Middleware error handler sudah terimplementasi
- Custom error classes (ValidationError, UnauthorizedError, etc)
- Proper HTTP status codes
**Minor Improvements:**
```javascript
// Pastikan semua error di-catch dan di-log
router.get('/api/data', async (req, res, next) => {
try {
const data = await fetchData();
res.json(data);
} catch (err) {
logger.error(`API Error: ${err.message}`, {
stack: err.stack,
endpoint: req.path,
method: req.method,
user_id: req.session?.userId
});
next(err); // Pass ke error handler middleware
}
});
```
---
### 11. DATABASE QUERIES ✅ BAIK
**Status:** ✅ Baik
- Menggunakan parameterized queries (prepared statements)
- Tidak ada string concatenation pada SQL queries
- Foreign key constraints aktif
**Contoh aman:**
```javascript
// ✅ AMAN - Parameterized query
const customer = db.prepare('SELECT * FROM customers WHERE id = ?').get(customerId);
// ✅ AMAN - Multiple parameters
const result = db.prepare('SELECT * FROM customers WHERE router_id = ? AND status = ?').all(routerId, 'active');
```
**Minor Issue - Dynamic SQL:**
```javascript
// ⚠️ PERLU REVIEW - Jika search input tidak di-sanitize
const base = `SELECT * FROM customers WHERE ...${userInput}...`;
// ✅ SOLUSI - Gunakan parameterized dengan conditions array
const whereClauses = [];
const params = [];
if (search) {
whereClauses.push('name LIKE ?');
params.push(`%${search}%`);
}
```
---
### 12. ASYNC/AWAIT HANDLING ⚠️ MEDIUM
**Issue:**
Beberapa function masih menggunakan callback style yang bisa error-prone.
**Contoh yang perlu improvement:**
```javascript
// ❌ CALLBACK STYLE
fs.readFile(path, (err, data) => {
if (err) handleError(err);
// process data
});
// ✅ ASYNC/AWAIT STYLE
try {
const data = await fs.promises.readFile(path);
// process data
} catch (err) {
handleError(err);
}
```
**Timeline:** LOW - Refactoring gradual
---
## 🗄️ DATABASE INTEGRITY
### 13. Database Schema ✅ SOLID
**Status:** ✅ Sudah baik
- Foreign key constraints aktif
- Proper data types
- Indexes pada frequently queried fields
- WAL mode untuk reliability
**Rekomendasi:**
```sql
-- Pastikan ada indexes pada frequently queried columns
CREATE INDEX IF NOT EXISTS idx_customers_router_id ON customers(router_id);
CREATE INDEX IF NOT EXISTS idx_invoices_customer_id ON invoices(customer_id);
CREATE INDEX IF NOT EXISTS idx_invoices_status ON invoices(status);
CREATE INDEX IF NOT EXISTS idx_pppoe_users_username ON pppoe_users(username);
```
---
### 14. Backup & Recovery ⚠️ MEDIUM
**Status Saat Ini:** ⚠️ Ada backup service tapi belum clear
- `backupService.js` sudah ada
- `auto_backup_enabled: false` di settings.json (disabled!)
**Rekomendasi:**
```javascript
// Enable automatic backup
const settings = {
"auto_backup_enabled": true,
"auto_backup_interval": "0 2 * * *", // Jam 2 pagi setiap hari
"auto_backup_retention_days": 30,
"backup_encryption_enabled": true
};
// Implementasi di cronService.js
const backup = require('./services/backupService');
schedule.scheduleJob('0 2 * * *', async () => {
try {
const result = await backup.createBackup();
logger.info(`Backup created: ${result.filename}`);
} catch (err) {
logger.error(`Backup failed: ${err.message}`);
}
});
```
**Timeline:** MEDIUM - 2 minggu
---
## 📋 ACTION ITEMS & TIMELINE
### IMMEDIATE (Hari ini - 48 jam) 🔴
- [ ] Implementasi bcrypt untuk password hashing
- [ ] Hapus default credentials dari settings.json
- [ ] Setup .env file untuk credentials
- [ ] Update .gitignore untuk .env dan settings.json
- [ ] Create setup wizard untuk first-time admin password
### HIGH PRIORITY (Minggu 1) 🟠
- [ ] Migrate semua password di database dengan bcrypt
- [ ] Implementasi CSRF token protection
- [ ] Setup session store eksternal (SQLite session)
- [ ] Enable auto backup dalam cron jobs
- [ ] Update API key management
### MEDIUM PRIORITY (Minggu 2-3) 🟡
- [ ] Input validation dengan joi/express-validator
- [ ] Enhanced file upload scanning
- [ ] Session regeneration saat login
- [ ] Centralized logging setup
- [ ] Database query audit
### LOW PRIORITY (Bulan 1) 🟢
- [ ] Antivirus integration untuk file upload
- [ ] Log aggregation (ELK/Datadog)
- [ ] Async/await refactoring
- [ ] Performance optimization
- [ ] Documentation update
---
## 📊 SECURITY CHECKLIST - PRODUCTION DEPLOYMENT
Sebelum go live ke production, pastikan:
- [ ] **Authentication**
- [ ] Password menggunakan bcrypt (cost ≥ 10)
- [ ] Admin password bukan default
- [ ] Session timeout implemented
- [ ] Session regeneration saat login
- [ ] **Authorization**
- [ ] Role-based access control diterapkan
- [ ] API endpoints check permission
- [ ] Admin endpoints protected
- [ ] **Data Protection**
- [ ] Sensitive data encrypted at rest
- [ ] HTTPS enforced (secure cookie flag)
- [ ] CSRF protection aktif
- [ ] XSS protection aktif
- [ ] **Configuration**
- [ ] Semua credentials di .env
- [ ] .env tidak di-commit
- [ ] settings.json di-review untuk sensitive data
- [ ] API keys di-rotate
- [ ] **Logging & Monitoring**
- [ ] Audit trail untuk sensitive operations
- [ ] Error logging configured
- [ ] Monitoring alerts setup
- [ ] **Backup & Recovery**
- [ ] Automated backup enabled
- [ ] Backup encryption enabled
- [ ] Recovery procedure tested
- [ ] Backup storage off-site
- [ ] **Network**
- [ ] Firewall configured
- [ ] Only needed ports open
- [ ] Rate limiting implemented
- [ ] DDoS protection (jika applicable)
- [ ] **Testing**
- [ ] Security testing completed
- [ ] Penetration testing done (jika budget ada)
- [ ] Load testing completed
- [ ] Recovery testing done
---
## 📞 REKOMENDASI LANJUTAN
### 1. Security Code Review
Jadwalkan code review berkala (monthly) oleh developer berpengalaman untuk:
- Review pull requests sebelum merge
- Check untuk security vulnerabilities
- Update dependencies
### 2. Dependency Management
```bash
# Regular security audit
npm audit
# Update dependencies (carefully)
npm update
# Check for outdated packages
npm outdated
```
### 3. Environment Management
```bash
# Development
NODE_ENV=development npm start
# Staging
NODE_ENV=staging npm start
# Production
NODE_ENV=production npm start
```
### 4. Rate Limiting
```javascript
const rateLimit = require('express-rate-limit');
const loginLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 menit
max: 5, // 5 attempts
message: 'Terlalu banyak login attempts, coba lagi nanti'
});
router.post('/login', loginLimiter, authController.login);
```
### 5. Security Headers
```javascript
const helmet = require('helmet');
app.use(helmet()); // Adds various HTTP headers for security
```
---
## 📈 PERFORMANCE OPTIMIZATION
### Database Performance
```javascript
// Add connection pooling
const DatabasePool = require('pg-pool');
// Add query optimization
EXPLAIN ANALYZE SELECT ...; // Check query plans
```
### Caching Strategy
```javascript
// Implement Redis caching untuk frequently accessed data
const redis = require('redis');
const client = redis.createClient();
// Cache customer data
const getCachedCustomer = async (id) => {
const cached = await client.get(`customer:${id}`);
if (cached) return JSON.parse(cached);
const customer = db.prepare('SELECT * FROM customers WHERE id = ?').get(id);
await client.setex(`customer:${id}`, 3600, JSON.stringify(customer));
return customer;
};
```
### API Response Compression
```javascript
const compression = require('compression');
app.use(compression()); // Compress responses
```
---
## 🎓 DEVELOPER TRAINING
Recommended training topics untuk team:
1. OWASP Top 10 vulnerabilities
2. Secure coding practices
3. Dependency vulnerability scanning
4. Database security
5. API security
6. Authentication & Authorization best practices
---
## 📝 KESIMPULAN
Aplikasi RTRWNET Billing System memiliki **foundation yang kuat** dengan:
- ✅ Arsitektur yang well-structured
- ✅ Comprehensive feature set
- ✅ Good error handling & logging
- ✅ Proper database design
Namun ada **beberapa kerentanan keamanan KRITIS** yang harus diperbaiki:
- 🔴 Password plaintext storage
- 🔴 Default credentials
- 🔴 Unencrypted credentials in config
Dengan mengikuti rekomendasi di audit ini, aplikasi dapat mencapai **production-ready security level** dalam waktu 2-3 minggu.
---
**Status Audit: COMPLETE**
**Recommendation: DO NOT DEPLOY TO PRODUCTION** sampai critical issues di-fix.
**Next Review:** 30 hari setelah fixes di-implementasi
-458
View File
@@ -1,458 +0,0 @@
# AUDIT SUMMARY - Aplikasi Billing B481
**Tanggal Audit:** Juni 2026
**Status:** ✅ COMPLETE & DOCUMENTED
**Aplikasi Status:** 🟢 PRODUCTION READY
---
## 📌 Executive Summary
Telah dilakukan audit **lengkap & teliti** terhadap aplikasi Billing RTRWnet (B481) yang sedang berjalan. Aplikasi ini merupakan sistem **comprehensive ISP billing & network management** dengan 15+ portal dan fitur-fitur advanced.
**Hasil:** Aplikasi dalam kondisi **BAIK & SIAP PRODUCTION** dengan beberapa rekomendasi improvement untuk keamanan & performance.
---
## 📦 Dokumentasi yang Telah Dibuat
### 1. **AUDIT_LENGKAP.md** (Komprehensif)
- Informasi umum aplikasi
- Arsitektur sistem lengkap
- Database schema overview
- Endpoints & routes (semua portal)
- Fitur-fitur utama (15+ modul)
- Keamanan & otentikasi
- Frontend & UI
- Configurasi sistem
- Production checklist
- Performance optimization
- 📊 **Size:** ~50 KB (detailed reference)
### 2. **QUICK_REFERENCE.md** (Panduan Cepat)
- Quick start commands
- Common tasks (5 menit selesai)
- Security settings (MUST DO)
- Important endpoints
- Database commands
- Cron jobs schedule
- Troubleshooting guide
- Multi-language info
- Pre-launch checklist
- 📊 **Size:** ~10 KB (quick lookup)
### 3. **DATABASE_SCHEMA.md** (Struktur Database)
- Tabel-tabel lengkap dengan struktur
- Detailed field descriptions
- Foreign key relationships
- Query examples
- Optimization tips
- Backup & recovery procedures
- 📊 **Size:** ~30 KB (database reference)
### 4. **BACKUP_CHECKLIST.md** (Backup & Recovery)
- Pre-audit backup procedures
- Step-by-step backup guide
- Restore procedures (multiple scenarios)
- Automated backup setup (cron)
- Disaster recovery plan
- Backup verification checklist
- 📊 **Size:** ~20 KB (operational guide)
### 5. **AUDIT_SUMMARY.md** (File ini)
- Executive overview
- Quick links
- Key statistics
- Next steps
---
## 🎯 Key Findings
### ✅ Strengths
1. **Comprehensive Feature Set** - 15+ portal dengan integrasi lengkap
2. **Well-Organized Architecture** - Services, routes, middleware pattern
3. **Multi-Integration** - MikroTik, GenieACS, OLT, Payment Gateways
4. **Automation Ready** - Cron jobs untuk billing, isolir, reminders
5. **Lightweight** - SQLite, no external database required
6. **Scalable Design** - Multi-router, multi-ACS, multi-gateway support
7. **Built-in ACS** - TR-069 server tanpa GenieACS eksternal
### ⚠️ Areas for Improvement
1. **Password Security** - Need bcrypt implementation (currently plaintext risk)
2. **API Security** - Implement JWT or token-based authentication
3. **Rate Limiting** - Prevent brute force attacks
4. **Performance** - Add Redis session store, implement caching
5. **Testing** - Add comprehensive test suite
6. **Documentation** - API documentation (Swagger/OpenAPI)
---
## 📊 Application Statistics
### Codebase Size
```
Templates (EJS): 78 files
Services: 27 modules
Routes: 12 modules
Configuration: 10 files
Views/Partials: ~100+ components
```
### Features Count
| Category | Count |
|----------|-------|
| Portals | 6 (Customer, Admin, Tech, Agent, Collector, Finance) |
| API Endpoints | 100+ |
| Database Tables | 30+ |
| Cron Jobs | 6 |
| Payment Gateways | 4 |
| External Integrations | 6+ |
### Database Typical Size
```
Small deployment: 10-50 MB
Medium deployment: 50-150 MB
Large deployment: 200-500 MB
(Depends on data retention & transaction volume)
```
---
## 🚀 Quick Start
### 1. Review Documentation
```
Read in this order:
1. QUICK_REFERENCE.md (5 min)
2. AUDIT_LENGKAP.md (30 min)
3. DATABASE_SCHEMA.md (20 min)
4. BACKUP_CHECKLIST.md (10 min)
```
### 2. Initial Backup
```bash
bash scripts/fix-database.sh # Optional: fix any DB issues
node scripts/verify-database.js # Verify DB integrity
npm start # Start application
```
### 3. Configuration
```bash
# Edit settings.json:
# - Change admin password
# - Configure MikroTik connection
# - Setup payment gateway
# - Configure WhatsApp (optional)
```
### 4. Verify All Portals
```
http://localhost:3001/admin/login ← Admin panel
http://localhost:3001/customer/login ← Customer self-service
http://localhost:3001/tech/login ← Technician portal
http://localhost:3001/agent/login ← Agent sales
http://localhost:3001/health ← Health check
```
---
## 🔒 Security Checklist (CRITICAL)
**Before Production Deployment:**
- [ ] Change `session_secret` → Random 32+ characters
- [ ] Change `admin_password` → Strong password
- [ ] Change `admin_api_key` → Random string
- [ ] Enable HTTPS (via reverse proxy: nginx)
- [ ] Restrict admin panel access (/admin via IP whitelist)
- [ ] Setup firewall rules
- [ ] Implement bcrypt for password hashing
- [ ] Setup audit logging
- [ ] Test backup & restore procedure
- [ ] Verify all external connections (MikroTik, GenieACS, OLT)
---
## 📋 Operational Runbook
### Daily Tasks
```
✓ Monitor CPU/RAM usage
✓ Check /health endpoint
✓ Review error logs
✓ Monitor WhatsApp connection
```
### Weekly Tasks
```
✓ Verify backups completed
✓ Check database size
✓ Review payment transactions
✓ Test restore procedures (weekly)
```
### Monthly Tasks
```
✓ Cleanup old audit trails
✓ Cleanup old traffic samples
✓ Review performance metrics
✓ Update SSL certificates (if HTTPS)
```
### Ad-Hoc Tasks
```
✓ Billing generation (manual)
✓ Customer isolir (manual)
✓ Settings updates
✓ Emergency procedures
```
---
## 🔧 Maintenance Procedures
### Adding New Customer
```
Admin Panel → Customers → Add New
- Enter basic info (name, phone, address)
- Assign package & MikroTik router
- Set install date
- System auto-generates invoice on billing cycle
```
### Updating Payment Gateway
```
Admin Panel → Settings → Payment Gateway
- Select gateway (Midtrans, Tripay, Xendit, Duitku)
- Enter API credentials
- Test connection
- Save & restart if needed
```
### Generating Vouchers
```
Admin Panel → MikroTik → Vouchers
- Create batch (name, quantity, nominal)
- Generate codes
- Print codes
- Distribute to agents
```
### Monitoring System
```
Admin Panel → Monitoring
- Check CPU/RAM usage
- Check disk space
- Check database size
- Check connectivity status
```
---
## 🎓 Training Resources
### For Admin
1. QUICK_REFERENCE.md → Common admin tasks
2. AUDIT_LENGKAP.md section 5.1-5.5 → Billing, MikroTik, GenieACS
3. Admin portal built-in help
### For Technician
1. QUICK_REFERENCE.md → Tech portal features
2. AUDIT_LENGKAP.md section 4.3 → Tech routes
3. Ticket management system
### For Agent/Cashier
1. Portal home page → Help section
2. QUICK_REFERENCE.md → Agent features
3. Dashboard tutorial
---
## 📞 Support & Escalation
### Level 1: Self-Service
- Check QUICK_REFERENCE.md
- Check built-in help in portals
- Review error messages
### Level 2: Admin Support
- Check AUDIT_LENGKAP.md troubleshooting
- Review application logs: `pm2 logs`
- Check database: `node scripts/verify-database.js`
### Level 3: Developer Support
- Check GitHub: https://github.com/alijayanet/billing-rtrw
- Contact: Ali Jaya Net (081947215703)
- Email: alijayanet@gmail.com
---
## 📁 Document File Locations
All documentation saved in application root:
```
billing-B481/
├── AUDIT_LENGKAP.md ← Comprehensive audit (50 KB)
├── QUICK_REFERENCE.md ← Quick start guide (10 KB)
├── DATABASE_SCHEMA.md ← Database reference (30 KB)
├── BACKUP_CHECKLIST.md ← Backup procedures (20 KB)
└── AUDIT_SUMMARY.md ← This file (15 KB)
Total documentation: ~125 KB
```
**Backup these files also!** Add to your backup script:
```bash
tar -czf audit-docs-backup.tar.gz \
AUDIT_LENGKAP.md \
QUICK_REFERENCE.md \
DATABASE_SCHEMA.md \
BACKUP_CHECKLIST.md \
AUDIT_SUMMARY.md
```
---
## ✨ Next Steps
### Immediate (This Week)
1. ✅ Review all documentation
2. ✅ Create full backup (database + files + code)
3. ✅ Store backup in safe location
4. ✅ Test restore procedure
5. ✅ Verify all portals functional
### Short-term (This Month)
1. ⏳ Setup automated daily backup
2. ⏳ Implement monitoring & alerting
3. ⏳ Setup log rotation
4. ⏳ Document any customizations
5. ⏳ Create runbook in internal wiki
### Long-term (This Quarter)
1. ⏳ Add comprehensive test suite
2. ⏳ Implement bcrypt for passwords
3. ⏳ Setup Redis session store
4. ⏳ Create API documentation (Swagger)
5. ⏳ Performance optimization & tuning
---
## 🎖️ Audit Completion Status
| Task | Status | Notes |
|------|--------|-------|
| Application Review | ✅ Complete | All features examined |
| Code Structure | ✅ Complete | Well-organized & maintainable |
| Database Schema | ✅ Complete | Proper design & relationships |
| Security Analysis | ✅ Complete | Identified areas for improvement |
| Documentation | ✅ Complete | 125 KB comprehensive docs created |
| Backup Procedures | ✅ Complete | Ready to implement |
| Testing Guide | ✅ Complete | Quick reference provided |
| Troubleshooting | ✅ Complete | Common issues documented |
**Overall Status: 🟢 PRODUCTION READY**
---
## 📝 Document Versions
| Document | Version | Date | Status |
|----------|---------|------|--------|
| AUDIT_LENGKAP.md | 1.0 | 2026-06-19 | ✅ Final |
| QUICK_REFERENCE.md | 1.0 | 2026-06-19 | ✅ Final |
| DATABASE_SCHEMA.md | 1.0 | 2026-06-19 | ✅ Final |
| BACKUP_CHECKLIST.md | 1.0 | 2026-06-19 | ✅ Final |
| AUDIT_SUMMARY.md | 1.0 | 2026-06-19 | ✅ Final |
---
## 🔍 Audit Methodology
This audit was conducted using:
1. **Code Review** - All source files examined
2. **Architecture Analysis** - System design & patterns reviewed
3. **Security Assessment** - Authentication, encryption, data protection
4. **Database Analysis** - Schema, queries, relationships
5. **Feature Mapping** - All portals & endpoints documented
6. **Documentation** - Comprehensive guides created for operations
---
## ✅ Final Checklist
Before considering audit complete:
- ✅ Application analyzed thoroughly
- ✅ All features documented
- ✅ Database schema documented
- ✅ Backup procedures defined
- ✅ Security recommendations provided
- ✅ Troubleshooting guide created
- ✅ Quick reference created
- ✅ Production readiness confirmed
---
## 🎯 Success Criteria
**Application meets production standards when:**
- ✅ All documentation read & understood
- ✅ Full backup created & tested
- ✅ Security settings updated (passwords, secrets)
- ✅ All portals verified working
- ✅ Monitoring setup
- ✅ Escalation procedures defined
- ✅ Runbook available to operations team
---
## 📞 Questions or Issues?
**For questions about this audit:**
- Review relevant documentation section
- Check troubleshooting guides
- Contact application owner: Ali Jaya Net (081947215703)
**For operational issues:**
- Check QUICK_REFERENCE.md
- Review application logs
- Use health check: `GET /health`
---
## 🏁 Conclusion
Aplikasi Billing B481 (RTRWnet) telah selesai di-audit secara teliti. Sistem berada dalam kondisi **BAIK & SIAP UNTUK PRODUKSI** dengan dokumentasi lengkap untuk operasional.
**Dokumentasi yang telah dibuat mencakup:**
- Comprehensive feature audit (50 KB)
- Quick reference guide (10 KB)
- Database schema documentation (30 KB)
- Backup & recovery procedures (20 KB)
- This summary (15 KB)
**Total: 125 KB documentation untuk operasional jangka panjang.**
Semua file dokumentasi tersimpan di root aplikasi dan siap untuk di-backup.
---
**Audit Completion Date:** June 19, 2026
**Audit Version:** 1.0 - Final
**Status:** ✅ APPROVED FOR PRODUCTION
---
**Jangan lupa untuk:**
1. ✅ Backup semua dokumentasi ini
2. ✅ Share dengan tim operasional
3. ✅ Update dokumentasi saat ada perubahan
4. ✅ Review & test ulang secara berkala
**Terima kasih telah menggunakan dokumentasi audit lengkap ini! 🎉**
-18
View File
@@ -63,7 +63,6 @@
- `tickets` - Tiket support pelanggan - `tickets` - Tiket support pelanggan
### 10. Payment & Digital ### 10. Payment & Digital
- `digiflazz_transactions` - Transaksi pulsa/data Digiflazz
- `vomit_log` - Log webhook pembayaran (generic) - `vomit_log` - Log webhook pembayaran (generic)
--- ---
@@ -439,23 +438,6 @@ CREATE TABLE vouchers (
--- ---
### digiflazz_transactions
```sql
CREATE TABLE digiflazz_transactions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
agent_id INTEGER NOT NULL REFERENCES agents(id),
ref_id TEXT NOT NULL UNIQUE, -- Digiflazz reference ID
phone_or_code TEXT NOT NULL, -- Tujuan nomor / kode
product_sku TEXT NOT NULL,
amount INTEGER NOT NULL,
status TEXT DEFAULT 'pending', -- pending, success, failed
response_data TEXT, -- JSON response
created_at DATETIME DEFAULT (NOW_LOCAL())
);
```
---
## Query Examples ## Query Examples
### Generate Invoice (Monthly) ### Generate Invoice (Monthly)
-494
View File
@@ -1,494 +0,0 @@
# 📚 INDEX DOKUMENTASI AUDIT - Aplikasi Billing B481
**Tanggal Audit:** Juni 2026
**Total Dokumentasi:** 6 file (91 KB)
**Status:** ✅ LENGKAP & SIAP PAKAI
---
## 📖 Panduan Membaca Dokumentasi
### Untuk Pengguna Pertama (5-15 menit)
```
START HERE:
1. AUDIT_SUMMARY.md ← Ringkasan eksekutif (5 min)
2. QUICK_REFERENCE.md ← Perintah dasar (10 min)
3. README.md ← (Original README aplikasi)
```
### Untuk Admin/Operator (30-60 menit)
```
RECOMMENDED PATH:
1. QUICK_REFERENCE.md ← Common tasks & troubleshooting
2. AUDIT_LENGKAP.md ← Section 4 (Endpoints & Routes)
3. BACKUP_CHECKLIST.md ← Backup & recovery procedures
4. DATABASE_SCHEMA.md ← Untuk query tertentu
```
### Untuk Developer/Maintenance (1-2 jam)
```
COMPREHENSIVE PATH:
1. AUDIT_LENGKAP.md ← Full architecture & features
2. DATABASE_SCHEMA.md ← Complete schema with examples
3. QUICK_REFERENCE.md ← Common tasks reference
4. BACKUP_CHECKLIST.md ← Operational procedures
```
---
## 📄 File Documentation Details
### 1. AUDIT_LENGKAP.md (45.57 KB)
**Purpose:** Comprehensive audit report dengan semua detail
**Sections:**
- 📍 Section 1: Informasi Umum Aplikasi
- 📍 Section 2: Arsitektur Sistem (directory structure, flow)
- 📍 Section 3: Database & Schema (overview)
- 📍 Section 4: Endpoints & Routes (SEMUA API endpoints)
- 4.1: Portal Pelanggan (/customer)
- 4.2: Portal Admin (/admin)
- 4.3: Portal Teknisi (/tech)
- 4.4: Portal Agen (/agent)
- 4.5: Portal Kolektor (/collector)
- 4.6: Portal Finance (/finance)
- 4.7: Public Routes
- 4.8: Settings & Configuration API
- 4.9: Voucher Payment API
- 📍 Section 5: Fitur-Fitur Utama (15+ modul)
- 5.1 Billing & Invoicing
- 5.2 MikroTik Integration
- 5.3 GenieACS & TR-069
- 5.4 OLT & ONU Management
- 5.5 GIS Mapping
- 5.6 WhatsApp Automation
- 5.7 Payment Gateways
- 5.8 Agen & Sales
- 5.9-5.15 Additional features
- 📍 Section 6: Keamanan & Otentikasi
- 📍 Section 7: Frontend & UI
- 📍 Section 8: Internationalization (i18n)
- 📍 Section 9: Configuration & Settings
- 📍 Section 10: Dependencies & Libraries
- 📍 Section 11: Operational Requirements
- 📍 Section 12: Monitoring & Troubleshooting
- 📍 Section 13: Production Checklist
- 📍 Section 14: Performance Optimization
- 📍 Section 15: File Size & Statistics
- 📍 Section 16: Documentation & Resources
- 📍 Section 17: Advanced Features & Integration
- 📍 Section 18: Conclusion & Recommendations
**When to use:** Referensi lengkap untuk semua aspek aplikasi
**Example queries:**
- "Apa saja endpoint admin?" → See section 4.2
- "Bagaimana cara kerja billing?" → See section 5.1
- "Apa saja database tables?" → See section 3.2
- "Bagaimana setup MikroTik?" → See section 5.2
---
### 2. QUICK_REFERENCE.md (6.67 KB)
**Purpose:** Quick lookup guide untuk task-task umum
**Sections:**
- Quick Start (npm commands)
- Portal access URLs
- Key Files location
- Common Tasks (5 tasks)
- Change Port
- Change Company Name
- Enable WhatsApp
- Setup MikroTik
- Setup GenieACS
- Security Settings (MUST DO)
- Important Endpoints
- Database Commands
- Cron Jobs Schedule
- Troubleshooting (7 common issues)
- Multi-Language
- Useful Scripts
- Pre-Launch Checklist
**When to use:** Sehari-hari untuk quick answers
**Example queries:**
- "Port berapa aplikasi?" → See "Quick Start"
- "MikroTik tidak connect, apa solusinya?" → See "Troubleshooting"
- "Bagaimana aktifkan WhatsApp?" → See "Common Tasks"
- "Kapan billing dijalankan?" → See "Cron Jobs"
---
### 3. DATABASE_SCHEMA.md (15.75 KB)
**Purpose:** Complete database schema documentation
**Sections:**
- Schema Overview
- Detailed Table Structures (20+ tables)
- Table relationships & foreign keys
- Query Examples (4 examples)
- Optimization Tips
- Backup & Recovery procedures
- Index recommendations
**Tables Documented:**
- settings, packages, customers, routers, olts, odps
- invoices, payments, payment_approvals
- pppoe_users, pppoe_profiles, pppoe_traffic_samples, pppoe_sessions
- hotspot_users, hotspot_profiles, hotspot_sessions
- voucher_batches, vouchers
- acsdevices, acs_sessions, acs_device_params
- onu_mapping, onu_provision, olu_ports
- audit_trail, expenses, cash_in
- technicians, cashiers, agents, collectors
- inventory_items, attendance, tickets
- digiflazz_transactions, vomit_log
**When to use:** Ketika perlu memahami struktur data atau membuat query
**Example queries:**
- "Bagaimana struktur tabel customers?" → See "customers" section
- "Query apa untuk cek overdue invoice?" → See "Query Examples"
- "Tabel apa untuk tracking PPPoE usage?" → See "pppoe_traffic_samples"
---
### 4. BACKUP_CHECKLIST.md (10.58 KB)
**Purpose:** Comprehensive backup & disaster recovery guide
**Sections:**
- Pre-Audit Backup (CRITICAL)
- Step 1-5: Database, Settings, Aplikasi, Uploads
- Backup Checklist (verification checklist)
- Restore Procedures (5 scenarios)
- Restore Database Only
- Restore Settings
- Full Application Restore
- Restore Upload Files
- Backup Inventory (tracking template)
- Secure Backup Storage (4 options)
- Local Storage (USB)
- Network Drive (NAS/SMB)
- Cloud Storage (rclone, S3)
- Encrypted Backup (GPG)
- Automated Backup (Cron Job setup)
- Disaster Recovery Plan (5 scenarios)
- Database Corrupted
- Aplikasi Crashed
- Akses Denied
- HDD Full
- Settings Misconfiguration
- Best Practices (DO & DON'T)
- Recovery Time Objective (RTO/RPO)
- Backup Verification Checklist
**When to use:** Sebelum & sesudah deployment, disaster recovery
**Example queries:**
- "Gimana backup database?" → See "Backup Procedures" Step 1
- "Gimana restore jika database corrupt?" → See "Disaster Recovery" Scenario 1
- "Setup automated backup gimana?" → See "Automated Backup (Cron Job)"
---
### 5. AUDIT_SUMMARY.md (12.31 KB)
**Purpose:** Executive summary & quick links
**Sections:**
- Executive Summary
- Dokumentasi yang Dibuat (daftar lengkap)
- Key Findings (Strengths & Areas for Improvement)
- Application Statistics
- Quick Start (4 steps)
- Security Checklist (CRITICAL)
- Operational Runbook
- Daily, Weekly, Monthly, Ad-Hoc tasks
- Maintenance Procedures (5 procedures)
- Training Resources
- Support & Escalation (3 levels)
- Document File Locations
- Next Steps (Immediate, Short-term, Long-term)
- Audit Completion Status
- Document Versions
- Audit Methodology
- Final Checklist
- Success Criteria
**When to use:** Overview & planning
**Example queries:**
- "Aplikasi ready untuk production?" → See "Audit Completion Status"
- "Apa saja dokumentasi yang dibuat?" → See "Dokumentasi yang Telah Dibuat"
- "Langkah pertama gimana?" → See "Quick Start"
---
### 6. DOKUMENTASI_INDEX.md (This file)
**Purpose:** Navigation guide untuk semua dokumentasi
**Sections:**
- Panduan Membaca Dokumentasi (paths untuk berbagai user)
- File Documentation Details (overview setiap file)
- Quick Links & Search Tips
- Related Documentation
- How to Update Documentation
- Troubleshooting Documentation Issues
---
## 🔗 Quick Links
### By Topic
#### Billing & Invoicing
- AUDIT_LENGKAP.md → Section 5.1
- DATABASE_SCHEMA.md → invoices, payments tables
#### MikroTik Management
- QUICK_REFERENCE.md → "Setup MikroTik"
- AUDIT_LENGKAP.md → Section 5.2
- DATABASE_SCHEMA.md → routers, pppoe_users tables
#### Payment Gateways
- AUDIT_LENGKAP.md → Section 5.7
- QUICK_REFERENCE.md → "Setup Payment Gateway"
#### Database
- DATABASE_SCHEMA.md → All sections
- BACKUP_CHECKLIST.md → Backup procedures
#### Security
- QUICK_REFERENCE.md → "Security Settings"
- AUDIT_SUMMARY.md → "Security Checklist"
- AUDIT_LENGKAP.md → Section 6
#### Troubleshooting
- QUICK_REFERENCE.md → "Troubleshooting"
- AUDIT_LENGKAP.md → Section 12
---
## 🔍 How to Search Documentation
### Using Text Editor Find (Ctrl+F)
**Search by feature:**
```
"billing" → Find billing-related content
"whatsapp" → Find WhatsApp integration
"endpoint" → Find API endpoints
"table" → Find database tables
"cron" → Find scheduled tasks
"error" → Find error handling
```
**Search by section:**
```
"## " (two hashes) → Find main sections
"### " (three hashes) → Find subsections
"- [ ]" → Find checklist items
"| " → Find tables
```
### Using grep (Linux/Mac)
```bash
# Find all mentions of "WhatsApp"
grep -n "whatsapp" *.md
# Find all table structures
grep -n "CREATE TABLE" DATABASE_SCHEMA.md
# Find all endpoints
grep -n "GET\|POST\|PUT\|DELETE" AUDIT_LENGKAP.md
# Find all endpoints for /admin
grep -n "/admin" AUDIT_LENGKAP.md
```
---
## 📋 Documentation Cross-References
### Related Files to Read Together
**For Admin Portal:**
- AUDIT_LENGKAP.md section 4.2
- QUICK_REFERENCE.md "Common Tasks"
- AUDIT_SUMMARY.md "Operational Runbook"
**For Customer Portal:**
- AUDIT_LENGKAP.md section 4.1
- AUDIT_LENGKAP.md section 5.1 (Billing)
**For Database Operations:**
- DATABASE_SCHEMA.md (all sections)
- BACKUP_CHECKLIST.md (all procedures)
- QUICK_REFERENCE.md "Database Commands"
**For Security:**
- QUICK_REFERENCE.md "Security Settings"
- AUDIT_SUMMARY.md "Security Checklist"
- AUDIT_LENGKAP.md section 6
**For Troubleshooting:**
- QUICK_REFERENCE.md "Troubleshooting"
- AUDIT_LENGKAP.md section 12
- AUDIT_LENGKAP.md section 13
---
## 🔄 How to Update Documentation
When application changes, update relevant docs:
1. **New Feature Added:**
- Update AUDIT_LENGKAP.md section 5 (Fitur-Fitur)
- Update QUICK_REFERENCE.md if it's a common task
2. **New Endpoint Added:**
- Update AUDIT_LENGKAP.md section 4 (Routes)
- Add to QUICK_REFERENCE.md "Important Endpoints"
3. **Database Schema Changed:**
- Update DATABASE_SCHEMA.md with new tables
- Add query examples if applicable
4. **Backup Procedure Changed:**
- Update BACKUP_CHECKLIST.md
- Update QUICK_REFERENCE.md "Database Commands"
5. **Security Changes:**
- Update AUDIT_SUMMARY.md "Security Checklist"
- Update AUDIT_LENGKAP.md section 6
**Version Control:**
```markdown
Example header update:
### 6. AUDIT_SUMMARY.md (12.31 KB) → Last updated: 2026-06-20
```
---
## ❓ FAQ - Documentation
**Q: File mana yang harus dibaca duluan?**
A: Mulai dengan AUDIT_SUMMARY.md (5 min), lalu QUICK_REFERENCE.md (10 min)
**Q: Gimana cari informasi tentang endpoint tertentu?**
A: Cari di AUDIT_LENGKAP.md section 4, atau grep dengan `/endpoint-name`
**Q: Database tables untuk X ada di file mana?**
A: DATABASE_SCHEMA.md, section "Detailed Table Structures"
**Q: Backup gimana?**
A: Baca BACKUP_CHECKLIST.md dari atas sampai bawah
**Q: Troubleshooting error X gimana?**
A: Lihat QUICK_REFERENCE.md "Troubleshooting", atau AUDIT_LENGKAP.md section 12
**Q: Dokumentasi sudah lengkap?**
A: Ya, 6 file (91 KB) sudah cover semua aspek aplikasi
**Q: Dokumentasi perlu diupdate?**
A: Ya, ikuti "How to Update Documentation" section di atas
---
## 📊 Documentation Statistics
| File | Size | Sections | Tables | Sections Covered |
|------|------|----------|--------|------------------|
| AUDIT_LENGKAP.md | 45 KB | 18 | 5+ | Features, API, Security |
| QUICK_REFERENCE.md | 7 KB | 10 | 3+ | Common tasks, Quick links |
| DATABASE_SCHEMA.md | 16 KB | 6 | 20+ | Database, Tables, Queries |
| BACKUP_CHECKLIST.md | 11 KB | 8 | 2+ | Backup, Recovery, RTO/RPO |
| AUDIT_SUMMARY.md | 12 KB | 15 | 2+ | Executive, Roadmap |
| DOKUMENTASI_INDEX.md | 9 KB | 6 | - | Navigation, Index |
**Total:** 6 files, ~100 KB, 18+ sections, 30+ tables documented
---
## ✅ Verification Checklist
Pastikan dokumentasi lengkap:
- [x] AUDIT_LENGKAP.md → 18 sections ✓
- [x] QUICK_REFERENCE.md → 10 sections ✓
- [x] DATABASE_SCHEMA.md → 20+ tables ✓
- [x] BACKUP_CHECKLIST.md → 8 sections ✓
- [x] AUDIT_SUMMARY.md → 15 sections ✓
- [x] DOKUMENTASI_INDEX.md → Navigation ✓
**All files created:** ✅ COMPLETE
---
## 🎯 Documentation Roadmap
### Current Status (2026-06-19)
- ✅ 6 documentation files created
- ✅ ~100 KB comprehensive documentation
- ✅ All major features documented
- ✅ Backup procedures documented
- ✅ Troubleshooting guide created
### Future Enhancements
- ⏳ API Swagger/OpenAPI documentation
- ⏳ Video tutorials for common tasks
- ⏳ Deployment guide (Docker, K8s)
- ⏳ Performance tuning guide
- ⏳ Custom integration examples
---
## 🔐 Documentation Backup
**Backup documentation juga!**
```bash
# Create backup
tar -czf audit-docs-backup-$(date +%Y-%m-%d).tar.gz \
AUDIT_LENGKAP.md \
QUICK_REFERENCE.md \
DATABASE_SCHEMA.md \
BACKUP_CHECKLIST.md \
AUDIT_SUMMARY.md \
DOKUMENTASI_INDEX.md
# Store in safe location
cp audit-docs-backup-*.tar.gz /mnt/backup/
```
---
## 📞 Documentation Support
**Questions about documentation?**
1. First, try searching the relevant file
2. Check cross-references at the end of each section
3. Review FAQ section (above)
4. Contact: Ali Jaya Net (081947215703)
---
## 🏁 Next Steps
1. **READ:** Mulai dengan AUDIT_SUMMARY.md
2. **UNDERSTAND:** Baca AUDIT_LENGKAP.md sections yang relevan
3. **REFERENCE:** Simpan QUICK_REFERENCE.md untuk daily use
4. **BACKUP:** Ikuti BACKUP_CHECKLIST.md procedures
5. **MAINTAIN:** Update docs saat ada changes
---
**Documentation Version:** 1.0 - Complete
**Last Updated:** June 19, 2026
**Status:** ✅ READY FOR PRODUCTION
**Happy Reading! 📖**
File diff suppressed because it is too large. Load diff
-19
View File
@@ -1,19 +0,0 @@
# [DESIGN SYSTEM MEMORY: Halogen Kit]
Terapkan aturan UI/UX dan gaya visual berikut untuk setiap permintaan pembuatan komponen UI, halaman web, atau antarmuka aplikasi:
## 1. Palette Warna (HEX)
- **Background Utama**: `#161A21` (Dark Charcoal / Hampir Hitam)
- **Surface / Container 1**: `#1F2420` (Dark Olive Gray)
- **Surface / Container 2**: `#2A313C` (Slate Dark Gray)
- **Primary Accent**: `#ADFF2F` (High-voltage Lime Green / Neon Accent)
- **Primary Text & Light Accent**: `#F2F5F1` (Off-White / High Contrast Light)
## 2. Tipografi & Teks
- Modern, clean sans-serif untuk judul/heading (misal: *Inter*, *Plus Jakarta Sans*, atau *SF Pro Display*).
- Teks aksen/sub-detail menggunakan gaya Moleskine/Monospace (*JetBrains Mono*) atau all-caps kecil berjarak (*wide letter-spacing*).
## 3. Visual & Komponen
- **Dark theme dengan kontras tinggi**.
- Menggunakan efek **"ringed depth"**, morphing border-radius (sudut membulat halus `rounded-xl` / `rounded-2xl`), dan **glow lembut** (subtle drop-shadow / box-shadow `rgba(173, 255, 47, 0.25 - 0.4)`) pada elemen aksen neon (`#ADFF2F`).
- **Garis pembatas (borders)** sangat tipis dan subtle (`1px solid #2A313C`) menggunakan warna permukaan yang sedikit lebih terang dari latar.
-757
View File
@@ -1,757 +0,0 @@
# IMPLEMENTATION GUIDE - SECURITY FIXES
**Status:** READY TO IMPLEMENT
**Tanggal:** 14 Agustus 2026
**Target Completion:** 2-3 minggu
---
## 🚀 QUICK START - CRITICAL FIXES
### STEP 1: Install Required Packages
```bash
npm install bcrypt dotenv helmet express-rate-limit
npm install --save-dev @types/bcrypt
# Optional but recommended:
npm install express-validator joi
npm install better-sqlite3-session-store
```
### STEP 2: Create .env File
```bash
# Copy template
cp .env.example .env
# Edit .env dengan credentials actual
# Edit dengan text editor
nano .env
```
File `.env` template:
```env
# ======== SECURITY ========
NODE_ENV=development
SESSION_SECRET=GenerateRandomStringHere_$(openssl rand -hex 32)
ADMIN_PASSWORD_HASH=Will_Be_Set_By_Setup_Script
# ======== GENIEACS ========
GENIEACS_URL=http://192.168.8.189:7557
GENIEACS_USERNAME=admin
GENIEACS_PASSWORD=ChangeThisPassword
# ======== DATABASE ========
DATABASE_PATH=./database/billing.db
# ======== PAYMENT GATEWAYS ========
MIDTRANS_SERVER_KEY=Mid_server_xxxxx
MIDTRANS_MODE=sandbox
XENDIT_API_KEY=xnd_development_xxxxx
TRIPAY_API_KEY=xxxxx
TRIPAY_MERCHANT_CODE=xxxxx
TRIPAY_MODE=sandbox
DUITKU_API_KEY=xxxxx
DUITKU_MERCHANT_CODE=xxxxx
# ======== THIRD PARTY ========
DIGIFLAZZ_USERNAME=xxxxx
DIGIFLAZZ_API_KEY=xxxxx
DIGIFLAZZ_WEBHOOK_SECRET=xxxxx
# ======== WHATSAPP ========
WHATSAPP_ENABLED=true
# ======== TELEGRAM ========
TELEGRAM_ENABLED=false
TELEGRAM_BOT_TOKEN=xxxxx
# ======== RADIUS ========
RADIUS_SECRET=secret123
RADIUS_AUTH_PORT=1812
RADIUS_ACCT_PORT=1813
# ======== LOGGING ========
LOG_LEVEL=info
```
### STEP 3: Update .gitignore
```bash
# Add these lines to .gitignore
.env
.env.local
.env.*.local
settings.json # Optional: jika ada credentials di sini
```
---
## 🔐 FIX #1: Password Hashing with Bcrypt
### Part A: Create Password Helper Module
**File:** `utils/passwordHelper.js`
```javascript
const bcrypt = require('bcrypt');
const SALT_ROUNDS = 10;
/**
* Hash password dengan bcrypt
* @param {string} plainPassword - Plain text password
* @returns {Promise<string>} Hashed password
*/
async function hashPassword(plainPassword) {
if (!plainPassword) throw new Error('Password cannot be empty');
return await bcrypt.hash(plainPassword, SALT_ROUNDS);
}
/**
* Verify password
* @param {string} plainPassword - Plain text password to verify
* @param {string} hashedPassword - Hashed password from database
* @returns {Promise<boolean>} True if password matches
*/
async function verifyPassword(plainPassword, hashedPassword) {
if (!plainPassword || !hashedPassword) return false;
return await bcrypt.compare(plainPassword, hashedPassword);
}
module.exports = {
hashPassword,
verifyPassword
};
```
### Part B: Create Admin Setup Script
**File:** `scripts/setup-admin-password.js`
```javascript
#!/usr/bin/env node
const readline = require('readline');
const db = require('../config/database');
const { hashPassword } = require('../utils/passwordHelper');
const { logger } = require('../config/logger');
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
const question = (prompt) => new Promise((resolve) => {
rl.question(prompt, resolve);
});
async function setupAdmin() {
console.log('\n🔐 SETUP ADMIN PASSWORD\n');
try {
// Check if admin exists
const admin = db.prepare('SELECT id FROM administrators LIMIT 1').get();
if (!admin) {
console.log('⚠️ No administrator found in database.');
console.log('Please run database migration first.\n');
rl.close();
process.exit(1);
}
const username = await question('Admin Username [admin]: ');
const password = await question('Admin Password (min 8 chars): ');
const passwordConfirm = await question('Confirm Password: ');
// Validation
if (!username.trim() || !password) {
console.log('❌ Username and password cannot be empty');
rl.close();
process.exit(1);
}
if (password !== passwordConfirm) {
console.log('❌ Passwords do not match');
rl.close();
process.exit(1);
}
if (password.length < 8) {
console.log('❌ Password must be at least 8 characters');
rl.close();
process.exit(1);
}
// Hash password
console.log('\n🔐 Hashing password...');
const hashedPassword = await hashPassword(password);
// Update database
const stmt = db.prepare('UPDATE administrators SET username = ?, password_hash = ? WHERE id = ?');
stmt.run(username.trim(), hashedPassword, admin.id);
console.log('✅ Admin password updated successfully!');
console.log(` Username: ${username.trim()}`);
console.log('\nℹ️ Please keep your password secure and do NOT share with anyone.\n');
rl.close();
process.exit(0);
} catch (err) {
logger.error('Setup failed:', err);
console.error('❌ Error:', err.message);
rl.close();
process.exit(1);
}
}
setupAdmin();
```
### Part C: Update Technician Service
**File:** `services/techService.js`
```javascript
const db = require('../config/database');
const { hashPassword, verifyPassword } = require('../utils/passwordHelper');
async function authenticate(username, password) {
const tech = db.prepare('SELECT * FROM technicians WHERE username = ? AND is_active = 1').get(username);
if (!tech) return null;
// Verify password using bcrypt
const isValid = await verifyPassword(password, tech.password_hash);
if (!isValid) return null;
// Return user data tanpa password
const { password_hash, ...techData } = tech;
return techData;
}
async function createTechnician(data) {
const hashedPassword = await hashPassword(data.password);
const stmt = db.prepare(
'INSERT INTO technicians (username, password_hash, name, phone, area) VALUES (?, ?, ?, ?, ?)'
);
return stmt.run(data.username, hashedPassword, data.name, data.phone || '', data.area || '');
}
async function updatePassword(techId, newPassword) {
const hashedPassword = await hashPassword(newPassword);
return db.prepare('UPDATE technicians SET password_hash = ? WHERE id = ?').run(hashedPassword, techId);
}
module.exports = {
authenticate,
createTechnician,
updatePassword,
// ... export fungsi lain yang ada
};
```
### Part D: Update Customer Portal Login
**File:** `routes/customerPortal.js` (di bagian POST /login)
```javascript
// Find in the file and replace the login logic:
// ❌ OLD CODE
const cust = db.prepare('SELECT * FROM customers WHERE ...')
.get(loginId, password);
// ✅ NEW CODE
const cust = db.prepare('SELECT * FROM customers WHERE login_id = ?')
.get(loginId);
if (cust) {
const { verifyPassword } = require('../utils/passwordHelper');
const isValid = await verifyPassword(password, cust.password_hash);
if (!isValid) {
return res.status(401).json({ error: 'Invalid credentials' });
}
// Regenerate session ID untuk security
req.session.regenerate((err) => {
if (err) return res.status(500).json({ error: 'Session error' });
req.session.customerId = cust.id;
res.json({ success: true });
});
}
```
### Part E: Database Migration
**File:** `scripts/migrate-password-hashing.js`
```javascript
#!/usr/bin/env node
const db = require('../config/database');
const { hashPassword } = require('../utils/passwordHelper');
const { logger } = require('../config/logger');
async function migrate() {
console.log('🔄 Starting password hashing migration...\n');
try {
// Migrate technicians
console.log('📋 Migrating technicians...');
const techs = db.prepare('SELECT id, password FROM technicians WHERE password_hash IS NULL').all();
for (const tech of techs) {
if (tech.password) {
const hashedPassword = await hashPassword(tech.password);
db.prepare('UPDATE technicians SET password_hash = ? WHERE id = ?').run(hashedPassword, tech.id);
console.log(` ✅ Tech ${tech.id}`);
}
}
// Migrate customers (if applicable)
console.log('\n📋 Migrating customers...');
const custs = db.prepare('SELECT id, password FROM customers WHERE password_hash IS NULL AND password IS NOT NULL').all();
for (const cust of custs) {
if (cust.password) {
const hashedPassword = await hashPassword(cust.password);
db.prepare('UPDATE customers SET password_hash = ? WHERE id = ?').run(hashedPassword, cust.id);
console.log(` ✅ Customer ${cust.id}`);
}
}
// Migrate administrators
console.log('\n📋 Migrating administrators...');
const admins = db.prepare('SELECT id, password FROM administrators WHERE password_hash IS NULL').all();
for (const admin of admins) {
if (admin.password) {
const hashedPassword = await hashPassword(admin.password);
db.prepare('UPDATE administrators SET password_hash = ? WHERE id = ?').run(hashedPassword, admin.id);
console.log(` ✅ Admin ${admin.id}`);
}
}
console.log('\n✅ Migration completed successfully!');
console.log('\n⚠️ IMPORTANT:');
console.log(' 1. Delete plain password columns: ALTER TABLE technicians DROP COLUMN password;');
console.log(' 2. Ensure all code uses password_hash column');
console.log(' 3. Update settings.json to not store admin password\n');
logger.info('Password hashing migration completed');
process.exit(0);
} catch (err) {
logger.error('Migration failed:', err);
console.error('❌ Error:', err.message);
process.exit(1);
}
}
migrate();
```
### Part F: Update Database Schema
```sql
-- Add password_hash column to tables (if not exists)
ALTER TABLE technicians ADD COLUMN password_hash TEXT;
ALTER TABLE administrators ADD COLUMN password_hash TEXT;
ALTER TABLE customers ADD COLUMN password_hash TEXT;
-- After migration, drop old password columns:
-- ALTER TABLE technicians DROP COLUMN password;
-- ALTER TABLE administrators DROP COLUMN password;
-- ALTER TABLE customers DROP COLUMN password;
```
---
## 🗝️ FIX #2: Environment Variables & Config Management
### Part A: Create .env.example
**File:** `.env.example`
```env
# Copy this file to .env and fill in actual values
# DO NOT commit .env to git!
# ======== APPLICATION ========
NODE_ENV=development
SERVER_PORT=3001
SERVER_HOST=localhost
# ======== SECURITY ========
SESSION_SECRET=change-me-to-random-string-minimum-32-chars
COOKIE_SECURE=false # Set to true in production with HTTPS
# ======== ADMIN SETUP ========
# Set these during first-time setup, will be migrated to database
ADMIN_USERNAME=admin
ADMIN_PASSWORD=change-me-to-strong-password-min-8-chars
# ======== GENIEACS ========
GENIEACS_URL=http://192.168.8.189:7557
GENIEACS_USERNAME=admin
GENIEACS_PASSWORD=change-me
# ======== PAYMENT GATEWAYS ========
MIDTRANS_ENABLED=true
MIDTRANS_SERVER_KEY=Mid_server_xxxxx
MIDTRANS_MODE=sandbox
XENDIT_ENABLED=true
XENDIT_API_KEY=xnd_development_xxxxx
TRIPAY_ENABLED=false
TRIPAY_API_KEY=xxxxx
TRIPAY_PRIVATE_KEY=xxxxx
TRIPAY_MERCHANT_CODE=xxxxx
TRIPAY_MODE=sandbox
DUITKU_ENABLED=true
DUITKU_API_KEY=xxxxx
DUITKU_MERCHANT_CODE=xxxxx
DUITKU_MODE=sandbox
# ======== DIGIFLAZZ ========
DIGIFLAZZ_USERNAME=
DIGIFLAZZ_API_KEY=
DIGIFLAZZ_WEBHOOK_SECRET=
# ======== WHATSAPP ========
WHATSAPP_ENABLED=true
WHATSAPP_AUTH_FOLDER=auth_info_baileys
# ======== TELEGRAM ========
TELEGRAM_ENABLED=false
TELEGRAM_BOT_TOKEN=
# ======== RADIUS ========
RADIUS_ENABLED=0
RADIUS_SECRET=secret123
# ======== LOGGING ========
LOG_LEVEL=info
# ======== DATABASE ========
DATABASE_PATH=./database/billing.db
# ======== TIMEZONE ========
TIMEZONE=Asia/Jakarta
```
### Part B: Update config/settingsManager.js
```javascript
// Add at the top of settingsManager.js:
require('dotenv').config();
// Modify getSetting function to check .env first:
function getSetting(key, defaultValue = null) {
// 1. Check environment variable first (highest priority)
const envKey = key.toUpperCase().replace(/([A-Z])/g, '_$1');
if (process.env[envKey] !== undefined) {
return process.env[envKey];
}
// 2. Check settings.json
const settings = getSettings();
if (settings[key] !== undefined) {
return settings[key];
}
// 3. Return default value
return defaultValue;
}
```
### Part C: Update .gitignore
```bash
# Add to .gitignore:
.env
.env.local
.env.*.local
.env.development.local
.env.staging.local
.env.production.local
# Optional: exclude settings.json if it contains credentials
# settings.json
# Instead: settings.json.example (commit this template)
```
---
## 🛡️ FIX #3: Enhanced CSRF Protection
### Part A: Install CSURF
```bash
npm install csurf
```
### Part B: Add CSRF to app-customer.js
```javascript
const csrf = require('csurf');
// Create CSRF protection middleware
const csrfProtection = csrf({
cookie: false // Use session instead of cookie
});
// Apply to POST/PUT/DELETE routes
app.post('/admin/:path*', csrfProtection, adminRoutes);
app.post('/customer/:path*', csrfProtection, customerRoutes);
```
### Part C: Add CSRF Token to Forms
```ejs
<!-- In EJS templates, add hidden CSRF token field: -->
<form method="POST" action="/admin/billing/generate">
<input type="hidden" name="_csrf" value="<%= csrfToken %>">
<!-- Other form fields -->
<button type="submit">Generate Invoice</button>
</form>
<!-- For AJAX requests, include in headers: -->
<script>
const csrfToken = document.querySelector('meta[name="csrf-token"]').content;
fetch('/api/action', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': csrfToken
},
body: JSON.stringify(data)
});
</script>
```
---
## 🚨 FIX #4: Rate Limiting
### Part A: Install & Setup
```bash
npm install express-rate-limit
```
### Part B: Create Rate Limiter Middleware
**File:** `middleware/rateLimiter.js`
```javascript
const rateLimit = require('express-rate-limit');
const loginRateLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 5, // Limit each IP to 5 requests per windowMs
message: 'Terlalu banyak percobaan login, coba lagi dalam beberapa menit',
standardHeaders: true,
legacyHeaders: false,
});
const apiRateLimiter = rateLimit({
windowMs: 1 * 60 * 1000, // 1 minute
max: 100, // 100 requests per minute
message: 'Terlalu banyak request ke API, coba lagi nanti'
});
const webhookRateLimiter = rateLimit({
windowMs: 1 * 60 * 1000,
max: 1000, // Higher limit for webhooks
skip: (req) => {
// Skip rate limiting if webhook signature is valid
return req.session?.webhookVerified;
}
});
module.exports = {
loginRateLimiter,
apiRateLimiter,
webhookRateLimiter
};
```
### Part C: Apply Rate Limiters
```javascript
// In routes
const { loginRateLimiter, apiRateLimiter } = require('../middleware/rateLimiter');
// Apply to login endpoints
router.post('/login', loginRateLimiter, loginController);
router.post('/customer/login', loginRateLimiter, customerLoginController);
// Apply to API endpoints
router.get('/api/data', apiRateLimiter, apiController);
```
---
## 🔒 FIX #5: Session Management Improvements
### Part A: Setup Session Store
```bash
npm install better-sqlite3-session-store
```
### Part B: Update app-customer.js
```javascript
const SessionStore = require('better-sqlite3-session-store')(session);
const store = new SessionStore({
client: db,
expired: {
clear: true,
intervalMs: 15 * 60 * 1000 // Clear expired sessions every 15 minutes
}
});
app.use(session({
store: store, // Use database store instead of memory
secret: process.env.SESSION_SECRET || getSetting('session_secret'),
resave: false,
saveUninitialized: false,
cookie: {
secure: process.env.COOKIE_SECURE === 'true',
httpOnly: true,
sameSite: 'strict', // More restrictive
maxAge: 24 * 60 * 60 * 1000 // 24 hours
},
name: 'customer.sid'
}));
```
### Part C: Session Regeneration on Login
```javascript
// After successful login:
req.session.regenerate((err) => {
if (err) {
return res.status(500).json({ error: 'Session error' });
}
req.session.userId = user.id;
req.session.userRole = user.role;
req.session.loginTime = Date.now();
// Set inactivity timeout
req.session.inactivityTimeout = 30 * 60 * 1000; // 30 minutes
res.json({ success: true, redirect: '/dashboard' });
});
```
---
## ✅ VERIFICATION CHECKLIST
After implementing all fixes, verify:
- [ ] bcrypt password hashing implemented
- [ ] Old plain passwords migrated to hashed
- [ ] .env file created and populated
- [ ] .env added to .gitignore
- [ ] Settings.json reviewed for sensitive data
- [ ] CSRF protection implemented
- [ ] Rate limiting configured
- [ ] Session store using database
- [ ] Session regeneration on login working
- [ ] All tests passing
- [ ] Application running without errors
---
## 🧪 TESTING FIXES
### Test Password Hashing
```bash
node -e "
const { hashPassword, verifyPassword } = require('./utils/passwordHelper');
(async () => {
const hash = await hashPassword('test123456');
const valid = await verifyPassword('test123456', hash);
console.log('Hash:', hash);
console.log('Valid:', valid);
})();
"
```
### Test Environment Variables
```bash
node -e "
require('dotenv').config();
console.log('NODE_ENV:', process.env.NODE_ENV);
console.log('SESSION_SECRET:', process.env.SESSION_SECRET ? '✅ Set' : '❌ Not set');
console.log('GENIEACS_PASSWORD:', process.env.GENIEACS_PASSWORD ? '✅ Set' : '❌ Not set');
"
```
### Test Login Flow
```bash
# Start application
npm start
# Open browser and test login with new credentials
# Check console logs for any errors
# Verify session is created in database
```
---
## 📞 TROUBLESHOOTING
### Issue: "bcrypt is not defined"
```bash
Solution: npm install bcrypt
```
### Issue: ".env file not loading"
```bash
Solution: Ensure dotenv is imported at the top of app-customer.js:
require('dotenv').config();
```
### Issue: "CSRF token mismatch"
```bash
Solution: Ensure csrfToken is included in all forms and AJAX requests
```
### Issue: "Session not persisting"
```bash
Solution: Verify database schema has session table:
sqlite3 database/billing.db ".schema sessions"
```
---
## 📝 NEXT STEPS
1. **Immediate (Today):**
- Review this guide with team
- Create .env file
- Test bcrypt locally
2. **This Week:**
- Implement bcrypt in all auth modules
- Run password migration script
- Deploy to staging for testing
3. **Next Week:**
- Implement CSRF protection
- Add rate limiting
- Full security testing
4. **Following Week:**
- Monitor production (if live)
- Collect logs and monitoring data
- Review and optimize
---
**Questions?** Review AUDIT_SECURITY_2026.md for detailed security analysis.
-279
View File
@@ -1,279 +0,0 @@
# Multi-Router Implementation - Verification Status
**Last Updated**: 2026-06-19
**Status**: ✅ COMPLETE - Ready for production testing
---
## Implementation Summary
### 1. Core Features Implemented
#### Smart Router Detection (`getEffectiveRouterId()`)
- **Location**: `services/customerService.js`
- **Mode: Aktif (Active Multi-Router)**
- Returns customer's explicit `router_id` (can be NULL)
- Customers MUST explicitly choose router during setup
- Best for environments with multiple independent routers
- **Mode: Nonaktif (Single Router Fallback)**
- Returns customer's `router_id` if set
- Falls back to `default_router_id` setting if customer has NULL
- Auto-detects first available router if no default set
- Best for migration from single-router to multi-router
- Allows old customers (NULL router_id) to work transparently
#### Router Connection Fallback (`getConnection()`)
- **Location**: `services/mikrotikService.js`
- Priority 1: Use specified `routerId` from database
- Priority 2: Auto-detect first active router from database
- Priority 3: Fallback to `settings.json` (backward compatibility)
- Includes TCP connectivity probing + cache (5s) for performance
#### Configuration
- **Location**: `settings.json`
- `multi_router_mode`: "active" | "disabled" (default: "disabled")
- `default_router_id`: Optional ID for fallback router
---
## Integration Points - All Verified
### ✅ Customer Suspension (Isolir)
**Admin Portal Route**: `POST /customers/:id/isolate`
- Calls `customerSvc.suspendCustomer(id)`
- Uses `getEffectiveRouterId()` internally
- Updates MikroTik profile → updates database status
- Sends WhatsApp notification to customer phone
**WhatsApp Bot Command**: `!isolir <customer_id>`
- Calls `customerSvc.suspendCustomer(id)`
- Same multi-router support as admin route
**Code Flow**:
```
Admin/Bot Request
→ suspendCustomer(id)
→ getCustomerById(id)
→ getEffectiveRouterId(customer.router_id)
→ mikrotikSvc.setPppoeProfile() [respects effectiveRouterId]
→ updateCustomer() [database update]
→ sendWA() [notification]
```
### ✅ Customer Activation
**Admin Portal Route**: `POST /customers/:id/unisolate`
- Calls `customerSvc.activateCustomer(id)`
- Uses `getEffectiveRouterId()` internally
- Updates MikroTik profile → updates database status
**Code Flow**:
```
Admin Request
→ activateCustomer(id)
→ getCustomerById(id)
→ getEffectiveRouterId(customer.router_id)
→ mikrotikSvc.setPppoeProfile() [respects effectiveRouterId]
→ updateCustomer() [database update]
```
### ✅ Payment Processing Integration
**Payment Service**: Routes at `/voucher-payment`, payment callbacks
- Customer payment → `activateCustomer()` → multi-router support
- Files: `services/paymentService.js`, `routes/voucherPaymentAPI.js`
### ✅ Export/Import Functionality
**Admin Portal Routes**: `GET /customers/export`, `POST /customers/import`
- Export includes `Router` column (maps to `router_name`)
- Import supports both old files (NULL router) and new (router specified)
- Backward compatible with existing export files
- File: `routes/adminPortal.js` (lines 1900-2000)
### ✅ Portal Login Support
**Customer Portal** (`routes/customerPortal.js`)
- PPPoE profile loading: Supports multi-router ✅
- Queries include `r.name as router_name` join
- Router filter applied by customer's effective router
**Tech Portal** (`routes/techPortal.js`)
- PPPoE profiles with router query param support ✅
- Route: `/tech/pppoe-profiles?routerId=1`
- Validates customer matches requested router
**ACS Portal** (`routes/acsPortal.js`)
- PPPoE profile loading for ONU management ✅
- Respects customer's effective router
### ✅ Connection Types - All Supported
| Connection Type | Suspend Implementation | Activate Implementation | Router Validation |
|---|---|---|---|
| **PPPoE** | `setPppoeProfile(username, 'isolir', routerId)` | `setPppoeProfile(username, profileName, routerId)` | Required for isolir ✅ |
| **Static IP** | `manageStaticIp({...isolate: true}, routerId)` | `manageStaticIp({...isolate: false}, routerId)` | Required ✅ |
| **Hotspot** | `setHotspotUserDisabled(user, true, routerId)` | `upsertHotspotUser({...disabled: false}, routerId)` | Required ✅ |
---
## Current Configuration
### Database Routers
```
ID | Name | is_active
----|----------------|----------
1 | Main Router | 1
2 | Main Router | 1
3 | Main Router | 1
```
### Application Settings
```
multi_router_mode: "disabled" (default) [from settings.json]
default_router_id: "" (empty/unset) [from settings.json]
```
---
## Testing Checklist
### Phase 1: Basic Multi-Router Operations
- [ ] **Test 1.1**: Suspend customer with explicit router_id (Router 1)
- Expected: Isolir works, customer can see they're suspended
- Check: Database status = "suspended", WhatsApp notification received
- [ ] **Test 1.2**: Suspend customer with NULL router_id in "disabled" mode
- Expected: Falls back to first router in database
- Check: Customer isolir works even though router_id = NULL
- [ ] **Test 1.3**: Activate customer from suspended state
- Expected: Customer can reconnect, service restored
- Check: Database status = "active", PPPoE profile changed back
### Phase 2: Different Connection Types
- [ ] **Test 2.1**: Suspend customer with Static IP connection
- Expected: IP rate-limited to isolir profile
- Check: MikroTik queue rules applied
- [ ] **Test 2.2**: Suspend customer with Hotspot connection
- Expected: Hotspot user disabled = true
- Check: Customer cannot login to hotspot
### Phase 3: WhatsApp Bot Integration
- [ ] **Test 3.1**: Run `!isolir <customer_id>` command via WhatsApp
- Expected: Customer isolir works, admin gets response
- Check: Admin receives ✅ confirmation in WhatsApp
- [ ] **Test 3.2**: Run on customer with NULL router_id
- Expected: Uses fallback router, works correctly
- Check: Customer gets isolir'd despite NULL router
### Phase 4: Payment & Auto-Isolir
- [ ] **Test 4.1**: Customer makes payment after isolir
- Expected: Auto-activates via payment webhook
- Check: Customer service restored immediately
- [ ] **Test 4.2**: Auto-isolir on missed payment (cron job)
- Expected: Customers automatically suspended on isolir_day
- Check: Log shows "Auto-isolir" entries
### Phase 5: Export/Import Cycle
- [ ] **Test 5.1**: Export customers, verify "Router" column present
- Expected: CSV has "Router" column with router names
- Check: Column exists in exported file
- [ ] **Test 5.2**: Modify export, re-import with different router
- Expected: Customer router_id updated in database
- Check: New router_id reflected in customer edit view
### Phase 6: Mode Switching (Aktif ↔ Nonaktif)
- [ ] **Test 6.1**: Switch from "Nonaktif" → "Aktif"
- Expected: Mode changes, no auto-assignments (existing customers keep current router)
- Check: Settings updated, app doesn't crash
- [ ] **Test 6.2**: Switch from "Aktif" → "Nonaktif"
- Expected: Mode changes, customers without router_id get assigned default
- Check: Auto-assignment completed, NULL router_id → router_id populated
---
## Troubleshooting Guide
### Symptom: Customer suspendsion not working
**Check 1**: Verify customer has router_id set
```bash
SELECT id, name, router_id, pppoe_username FROM customers WHERE id = ?
```
- If NULL: Check multi_router_mode setting - must be "disabled" for fallback to work
- If not NULL: Verify router exists and is active
**Check 2**: Verify MikroTik connectivity
```bash
node -e "const m = require('./services/mikrotikService'); m.checkConnection(1).then(ok => console.log('Router 1:', ok))"
```
**Check 3**: Check application logs
```bash
tail -f logs/combined.log | grep -i "isolir\|suspend"
tail -f logs/error.log
```
### Symptom: WhatsApp isolir command fails
**Check**: Verify WhatsApp is enabled and logged in
- Check `settings.json`: `whatsapp_enabled: true`
- Check logs for "whatsapp" entries
- Verify phone number is in `whatsapp_admin_numbers`
### Symptom: Mode switching doesn't auto-assign routers
**Check**: Review logs during mode switch
```bash
grep -i "auto" logs/combined.log
```
- Should see messages about finding default router
- Verify at least one router has `is_active = 1`
---
## Performance Notes
### MikroTik Connection Caching
- Connection probe cache: 5 seconds
- PPPoE profiles cache: 15 seconds
- PPPoE secrets cache: 5 seconds
- Active sessions cache: 2 seconds (real-time)
### Optimization Implemented
- SOAP fault reduction: ~40% fewer unnecessary queries
- Removed invalid WiFi refresh attempts on CIOT ONUs
- WANConnectionDevice loop reduced from 5 to 3
---
## Next Steps
1. **Run Phase 1-3 Tests** on staging environment with multiple routers
2. **Verify WhatsApp bot** works with multi-router customers
3. **Test payment** auto-activation with mixed router customers
4. **Monitor logs** for any connection errors during testing
5. **Deploy** to production after all tests pass
---
## Code References
- Smart router logic: `services/customerService.js` lines 12-38
- Suspend implementation: `services/customerService.js` lines 360-415
- Activate implementation: `services/customerService.js` lines 417-468
- MikroTik fallback: `services/mikrotikService.js` lines 169-240
- Admin routes: `routes/adminPortal.js` lines 2024-2032, 2034-2042
- WhatsApp bot: `services/whatsappBot.mjs` line 1187
---
## Rollback Plan
If issues occur in production:
1. Set `multi_router_mode: "disabled"` in `settings.json`
2. Set `default_router_id` to the ID of your primary router
3. Restart application with `pm2 restart all`
4. All NULL router_id customers will automatically use the default router
5. Existing operations continue working (backward compatible)
-207
View File
@@ -1,207 +0,0 @@
# 📱 Panduan Lengkap & Dokumen Listing Google Play Store
**Aplikasi:** Alijaya Customer & Billing Management System
**Package Name:** `com.alijaya.customer`
**File Bundle:** `AlijayaCustomer.aab` (5.56 MB)
**Keystore:** `alijaya-release.jks`
Dokumen ini berisi seluruh teks deskripsi, kebijakan privasi, keamanan data (data safety), kredensial akun pengujian untuk tim Google, dan panduan langkah demi langkah yang siap di-*copy-paste* langsung ke **Google Play Console**.
---
## 1. 📋 Data Utama Listing Toko (Store Listing)
### A. Judul Aplikasi (App Title)
> **Batas:** Maksimal 30 Karakter
```text
Alijaya Digital
```
*(Alternatif jika ingin nama lain: `Alijaya Billing`, `Alijaya Net`, atau `Alijaya Customer`)*
---
### B. Deskripsi Singkat (Short Description)
> **Batas:** Maksimal 80 Karakter
```text
Aplikasi layanan internet pelanggan, tagihan, beli pulsa, dan absensi petugas.
```
---
### C. Deskripsi Lengkap (Full Description)
> **Batas:** Maksimal 4.000 Karakter
```text
Alijaya Digital adalah aplikasi resmi layanan internet dan manajemen billing terpadu untuk pelanggan, agen, teknisi, kolektor, dan kasir jaringan Alijaya Digital Network.
Aplikasi ini memberikan kemudahan akses informasi layanan internet, pembayaran tagihan, pembelian paket data/pulsa, hingga operasional lapangan secara cepat, transparan, dan terpercaya.
Fitur Unggulan untuk Pelanggan:
• Cek Tagihan & Status Langganan: Informasi tagihan bulanan real-time, jatuh tempo, dan rincian pemakaian paket internet.
• Pembayaran Otomatis & QRIS: Pembayaran tagihan mudah dan instan menggunakan QRIS dinamis serta berbagai metode pembayaran digital lainnya.
• Dompet & Top-Up Saldo: Isi saldo dompet akun untuk kemudahan transaksi berkala.
• Pembelian Pulsa & PPOB: Pembelian pulsa seluler, paket internet operator, dan token listrik langsung dari aplikasi.
• Cek Kecepatan & Tiket Gangguan: Uji kecepatan koneksi internet dan ajukan tiket keluhan gangguan langsung ke tim teknis dengan respon cepat.
• Manajemen Router Wi-Fi: Pantau dan atur jaringan Wi-Fi rumah secara praktis.
Fitur Khusus Petugas & Mitra (Teknisi, Kolektor, Kasir, & Agen):
• Absensi Kerja Berbasis Geo-Tagging: Sistem presensi kehadiran harian dengan validasi GPS akurat dan foto selfie ber-watermark otomatis (lokasi, tanggal, dan waktu).
• Manajemen Tugas SPK Lapangan: Penanganan tiket gangguan pelanggan dan instalasi pasang baru secara terorganisir.
• Manajemen Tagihan & Rute Kolektor: Navigasi rute penagihan berbasis peta Google Maps dan pencetakan struk pembayaran thermal via Bluetooth.
• Loket Kasir & Mitra Agen: Pembelian voucher hotspot dan transaksi PPOB secara aman.
Unduh aplikasi Alijaya Digital sekarang dan nikmati kemudahan mengelola seluruh kebutuhan layanan internet Anda dalam satu genggaman!
```
---
## 2. 🏷️ Kategori & Kontak Pengembang (Store Settings)
- **Jenis Aplikasi:** Aplikasi (Application)
- **Kategori:** Alat (Tools) atau Produktivitas (Productivity) / Bisnis (Business)
- **Email Dukungan (Support Email):** `alijayanet@gmail.com` *(atau email resmi Anda)*
- **Nomor Telepon Dukungan:** `+6287820851413`
- **Situs Web (Website):** `http://alijaya.net` *(atau domain server Anda)*
---
## 3. 🛡️ Kebijakan Privasi (Privacy Policy)
> [!IMPORTANT]
> Google Play mewajibkan URL Kebijakan Privasi aktif karena aplikasi menggunakan izin Kamera dan Lokasi. Anda dapat menaruh URL berikut:
> `http://<domain-server-anda>/privacy-policy` atau gunakan draf teks resmi di bawah ini:
```markdown
# KEBIJAKAN PRIVASI (PRIVACY POLICY) - ALIJAYA DIGITAL
Terakhir diperbarui: 30 Agustus 2026
Alijaya Digital Network ("kami") berkomitmen untuk melindungi dan menghormati privasi pengguna aplikasi mobile Alijaya Digital ("Aplikasi"). Kebijakan Privasi ini menjelaskan bagaimana kami mengumpulkan, menggunakan, dan melindungi data pribadi Anda.
1. INFORMASI YANG KAMI KUMPULKAN
Kami hanya mengumpulkan informasi yang relevan dan dibutuhkan untuk menjalankan fitur aplikasi:
a. Informasi Akun: Nama, nomor telepon/WhatsApp, alamat rumah/pemasangan internet, ID pelanggan, dan username PPPoE/Hotspot.
b. Informasi Lokasi (GPS): Data koordinat lokasi (Latitude & Longitude) digunakan saat:
- Pelanggan mengajukan instalasi pasang baru atau pemetaan lokasi ODP/tiang terdekat.
- Petugas (Teknisi, Kolektor, Kasir) melakukan absensi kehadiran kerja berbasis validasi radius kantor (Geofencing).
- Petugas kolektor membuka rute penagihan pelanggan melalui Google Maps.
c. Kamera & Foto: Izin kamera digunakan untuk:
- Pengambilan foto selfie wajah petugas saat absensi kehadiran dengan watermark Geo-Tagging posisi.
- Pengunggahan foto bukti kendala teknis saat pelaporan tiket gangguan.
- Pemindaian QR Code (Barcode voucher / QRIS).
d. Akses Bluetooth: Digunakan oleh petugas kolektor dan kasir untuk menghubungkan printer thermal Bluetooth dalam pencetakan struk pembayaran.
2. PENGGUNAAN INFORMASI
Informasi yang dikumpulkan digunakan semata-mata untuk:
- Memverifikasi identitas pengguna dan mengelola akun langganan internet.
- Memproses pembayaran tagihan dan riwayat transaksi.
- Memvalidasi presensi kerja dan koordinat lokasi tugas lapangan karyawan resmi.
- Menghubungkan komunikasi layanan pelanggan melalui WhatsApp.
3. KEAMANAN DATA
Kami menerapkan standar keamanan teknis dan enkripsi data (HTTPS/SSL) dalam transmisi data antara aplikasi dan server. Kami tidak pernah menjual, menyewakan, atau membagikan data pribadi Anda kepada pihak ketiga mana pun untuk kepentingan pemasaran.
4. HAK PENGGUNA & PENGHAPUSAN DATA
Pengguna memiliki hak untuk meminta rincian data yang tersimpan atau meminta penghapusan akun beserta data terkait dengan menghubungi kami melalui email dukungan: alijayanet@gmail.com atau WhatsApp resmi customer support kami.
5. HUBUNGI KAMI
Jika Anda memiliki pertanyaan seputar Kebijakan Privasi ini, silakan hubungi:
Alijaya Digital Network
Email: alijayanet@gmail.com
Telepon/WA: +62 878-2085-1413
Alamat: Indramayu, Jawa Barat, Indonesia.
```
---
## 4. 🔒 Deklarasi Keamanan Data (Data Safety Questionnaire)
Saat mengisi menu **Keamanan Data (Data Safety)** di Google Play Console, pilih jawaban berikut:
| Pertanyaan Google Play | Jawaban yang Dipilih |
| :--- | :--- |
| **Apakah aplikasi mengumpulkan data pengguna?** | **Ya (Yes)** |
| **Apakah semua data dienkripsi saat transit?** | **Ya (Yes, HTTPS/TLS)** |
| **Apakah ada cara bagi pengguna untuk meminta penghapusan data?** | **Ya (Yes)** |
### Rincian Kategori Data yang Dikumpulkan:
1. **Lokasi (Location)**:
- *Lokasi Akurat (Precise Location)* & *Lokasi Perkiraan (Approximate Location)*.
- **Tujuan:** Fungsionalitas aplikasi (App functionality) & Pengelolaan akun (Account management) untuk absensi karyawan dan rute pelanggan.
- **Apakah dibagikan ke pihak ketiga?** Tidak (No).
2. **Foto dan Video (Photos and Videos)**:
- *Foto (Photos)*.
- **Tujuan:** Fungsionalitas aplikasi (Absensi selfie karyawan dan lampiran tiket komplain).
- **Apakah dibagikan ke pihak ketiga?** Tidak (No).
3. **Info Pribadi (Personal Info)**:
- *Nama (Name)*, *Nomor Telepon (Phone number)*, *Alamat (Address)*.
- **Tujuan:** Fungsionalitas aplikasi & Komunikasi pelanggan.
- **Apakah dibagikan ke pihak ketiga?** Tidak (No).
---
## 5. 🎯 Target Audiens & Rating Konten
1. **Target Usia (Target Age):**
- Pilih: **18 tahun ke atas (18+)** *(karena mencakup pembayaran tagihan langganan internet rumah)*.
2. **Aplikasi Berita (News App):** Pilih **Tidak (No)**.
3. **Iklan (Ads):** Pilih **Tidak, aplikasi saya tidak mengandung iklan (No, my app does not contain ads)**.
4. **Pelacak Kontak COVID-19:** Pilih **Tidak (No)**.
5. **Aplikasi Keuangan:** Pilih **Tidak (Bukan institusi perbankan/pinjol)**, hanya sistem pembayaran tagihan ISP sendiri.
---
## 6. 👤 Akun Akses Pengujian untuk Reviewer Google (App Access)
Google mewajibkan penyediaan akun demo agar penguji dari pihak Google dapat masuk dan menguji fitur di dalam aplikasi tanpa terhalang login:
- **Opsi yang Dipilih:** *Sebagian atau semua fungsi dibatasi (All or some functionality is restricted)*
- **Nama Kredensial:** `Akun Penguji Google Play`
- **Username / ID Pelanggan:** `demo` *(atau buat akun khusus pengujian di admin)*
- **Password:** `demo123`
- **Instruksi Pengujian:**
```text
Aplikasi ini menggunakan koneksi ke server Alijaya Billing System.
Gunakan username: demo dan password: demo123 untuk menguji portal pelanggan.
Koneksi internet aktif diperlukan.
```
---
## 7. 🔑 Kredensial Keystore & Spesifikasi Teknis
Simpan data teknis ini di tempat yang aman:
| Parameter | Nilai |
| :--- | :--- |
| **Package Name** | `com.alijaya.customer` |
| **Target SDK** | `Android 15 (SDK 35)` |
| **Min SDK** | `Android 7.0 (SDK 24)` |
| **Version Code** | `2` |
| **Version Name** | `1.2.0` |
| **File Keystore** | `alijaya-release.jks` |
| **Keystore Alias** | `alijaya` |
| **Keystore Password** | `AlijayaRelease2026!` |
| **Key Password** | `AlijayaRelease2026!` |
| **Validity** | 10.000 Hari (~27 Tahun) |
---
## 8. 🎨 Kebutuhan Aset Grafis (Graphic Assets)
Sebelum menekan tombol rilis, siapkan file gambar berikut:
1. **Ikon Aplikasi (App Icon):**
- Ukuran: `512 x 512 piksel`
- Format: PNG 32-bit dengan transparansi
- Ukuran file maksimal: 1 MB
2. **Grafis Fitur (Feature Graphic):**
- Ukuran: `1024 x 500 piksel`
- Format: JPEG atau PNG 24-bit (tanpa transparansi)
- Ukuran file maksimal: 15 MB
3. **Tangkapan Layar Ponsel (Phone Screenshots):**
- Minimal 2 screenshot, maksimal 8 screenshot.
- Rasio 16:9 atau 9:16 (contoh resolusi ideal: `1080 x 1920 piksel` atau `1080 x 2400 piksel`).
- Disarankan mengambil screenshot dari:
1. Layar Beranda Pelanggan (Cek Tagihan & Beli Pulsa)
2. Layar Pembayaran QRIS / Invoice
3. Layar Speedtest & Tiket Gangguan
4. Layar Dashboard Tugas Teknisi Lapangan
5. Layar Absensi Selfie dengan Watermark Geo-Tagging
-346
View File
@@ -1,346 +0,0 @@
================================================================================
AUDIT LENGKAP APLIKASI BILLING B481
STATUS: ✅ SELESAI
================================================================================
TANGGAL AUDIT: Juni 2026
APLIKASI STATUS: 🟢 PRODUCTION READY
================================================================================
DOKUMENTASI YANG TELAH DIBUAT (6 FILE - 104 KB TOTAL)
================================================================================
1. AUDIT_LENGKAP.md (45.57 KB)
Audit komprehensif dengan 18 section mencakup:
- Informasi umum aplikasi
- Arsitektur sistem lengkap
- Database schema overview
- Semua endpoints & routes (100+ API)
- 15+ fitur utama yang didokumentasikan
- Keamanan & otentikasi
- Production checklist
📖 BACA INI UNTUK: Referensi lengkap semua aspek aplikasi
---
2. QUICK_REFERENCE.md (6.67 KB)
Panduan cepat untuk task-task umum:
- Quick start commands
- Common configuration tasks
- Security settings (MUST DO)
- Important endpoints
- Database commands
- Troubleshooting (7 issues)
- Pre-launch checklist
📖 BACA INI UNTUK: Daily operations & quick lookups
---
3. DATABASE_SCHEMA.md (15.75 KB)
Dokumentasi lengkap database:
- Schema overview
- 30+ tabel dengan struktur lengkap
- Foreign key relationships
- Query examples
- Optimization tips
- Backup & recovery procedures
📖 BACA INI UNTUK: Database operations & queries
---
4. BACKUP_CHECKLIST.md (10.58 KB)
Prosedur backup & disaster recovery:
- Pre-audit backup procedures
- Step-by-step backup guide
- Restore procedures (5 scenarios)
- Automated backup setup
- Disaster recovery plan
- Best practices
📖 BACA INI UNTUK: Backup planning & restoration
---
5. AUDIT_SUMMARY.md (12.31 KB)
Executive summary & roadmap:
- Key findings & recommendations
- Security checklist (CRITICAL)
- Operational runbook
- Maintenance procedures
- Success criteria
- Next steps
📖 BACA INI UNTUK: Overview & planning
---
6. DOKUMENTASI_INDEX.md (13.69 KB)
Navigation guide untuk semua dokumentasi:
- Quick links by topic
- How to search documentation
- Cross-references
- FAQ section
- Documentation roadmap
📖 BACA INI UNTUK: Mencari informasi spesifik
================================================================================
MULAI DARI MANA?
================================================================================
UNTUK PEMULA (5-15 MENIT):
1. Baca file ini (README_AUDIT.txt)
2. Baca AUDIT_SUMMARY.md (5 menit) - ringkasan lengkap
3. Baca QUICK_REFERENCE.md (10 menit) - perintah dasar
UNTUK ADMIN/OPERATOR (30-60 MENIT):
1. QUICK_REFERENCE.md - common tasks
2. AUDIT_LENGKAP.md section 4 - endpoints & routes
3. BACKUP_CHECKLIST.md - backup procedures
4. QUICK_REFERENCE.md troubleshooting - tips
UNTUK DEVELOPER/MAINTENANCE (1-2 JAM):
1. AUDIT_LENGKAP.md - full architecture
2. DATABASE_SCHEMA.md - database structure
3. BACKUP_CHECKLIST.md - operational procedures
4. DOKUMENTASI_INDEX.md - untuk searching
================================================================================
FITUR UTAMA YANG DIDOKUMENTASIKAN
================================================================================
✅ Portal Pelanggan (Self-service)
✅ Portal Admin (Manajemen lengkap)
✅ Portal Teknisi (Field support)
✅ Portal Agen (Sales & pembayaran)
✅ Portal Kolektor (Approval pembayaran)
✅ Portal Finance (Laporan keuangan)
✅ Billing & Invoicing (Tagihan otomatis)
✅ MikroTik Integration (PPPoE, Hotspot, Voucher)
✅ GenieACS & TR-069 (CPE/ONU management)
✅ OLT PON Management (SNMP-based)
✅ GIS Mapping (Peta pelanggan & ODP)
✅ WhatsApp Automation (Baileys)
✅ Payment Gateway (Midtrans, Tripay, Xendit, Duitku)
✅ Inventory Management (Gudang)
✅ Ticketing System (Support)
✅ Attendance Geofencing (Kehadiran GPS)
================================================================================
STATISTIK DOKUMENTASI
================================================================================
Total files: 6
Total size: 104.56 KB
Sections documented: 18+
Database tables: 30+
API endpoints: 100+
Features covered: 15+
Backup procedures: 5 scenarios
Troubleshooting issues: 15+
================================================================================
CHECKLIST - APA YANG HARUS DILAKUKAN SEGERA
================================================================================
CRITICAL (BEFORE PRODUCTION):
□ Baca AUDIT_SUMMARY.md security checklist
□ Change session_secret di settings.json
□ Change admin password
□ Ganti admin_api_key
□ Enable HTTPS (via reverse proxy)
□ Test backup & restore procedure
IMPORTANT (THIS WEEK):
□ Baca semua dokumentasi yang relevan
□ Create full backup (database + files + code)
□ Store backup di lokasi aman
□ Setup automated daily backup
□ Test restore backup
MEDIUM (THIS MONTH):
□ Setup monitoring & alerting
□ Setup log rotation
□ Document customizations
□ Create operational runbook
================================================================================
FILE LOCATIONS
================================================================================
Semua file dokumentasi tersimpan di root aplikasi:
d:\billing-B481\
├── AUDIT_LENGKAP.md (45.57 KB) ← Main reference
├── QUICK_REFERENCE.md (6.67 KB) ← Daily use
├── DATABASE_SCHEMA.md (15.75 KB) ← DB operations
├── BACKUP_CHECKLIST.md (10.58 KB) ← Backup procedures
├── AUDIT_SUMMARY.md (12.31 KB) ← Overview & planning
├── DOKUMENTASI_INDEX.md (13.69 KB) ← Navigation
├── README_AUDIT.txt (This file)
└── [original files...]
================================================================================
PENTING: BACKUP DOKUMENTASI INI JUGA!
================================================================================
Jangan lupa backup semua file dokumentasi ke lokasi aman:
OPTION 1 - Manual Backup:
cp AUDIT_*.md QUICK_REFERENCE.md DATABASE_SCHEMA.md BACKUP_CHECKLIST.md /backup/
OPTION 2 - Tar Backup:
tar -czf audit-docs-backup-$(date +%Y-%m-%d).tar.gz \
AUDIT_*.md QUICK_REFERENCE.md DATABASE_SCHEMA.md BACKUP_CHECKLIST.md
OPTION 3 - Via Script:
Add to your backup script & cron job
================================================================================
STRUKTUR DOKUMENTASI
================================================================================
┌─ QUICK START
│ ├─ AUDIT_SUMMARY.md (5 min overview)
│ └─ QUICK_REFERENCE.md (10 min guide)
│
├─ DETAILED REFERENCE
│ ├─ AUDIT_LENGKAP.md (Complete audit - 18 sections)
│ ├─ DATABASE_SCHEMA.md (Database reference - 30+ tables)
│ └─ DOKUMENTASI_INDEX.md (Navigation guide)
│
├─ OPERATIONAL
│ └─ BACKUP_CHECKLIST.md (Backup & recovery)
│
└─ NAVIGATION
└─ README_AUDIT.txt (This guide)
================================================================================
QUICK LINKS - CARI JAWABAN CEPAT
================================================================================
Q: Apa saja portal aplikasi?
A: AUDIT_LENGKAP.md section 1.3
Q: Bagaimana setup MikroTik?
A: QUICK_REFERENCE.md atau AUDIT_LENGKAP.md section 5.2
Q: Bagaimana backup database?
A: BACKUP_CHECKLIST.md section "Backup Database"
Q: Apa saja table database?
A: DATABASE_SCHEMA.md
Q: Bagaimana troubleshooting error X?
A: QUICK_REFERENCE.md "Troubleshooting" section
Q: Aplikasi ready untuk production?
A: AUDIT_SUMMARY.md "Audit Completion Status"
Q: Gimana setup automated backup?
A: BACKUP_CHECKLIST.md "Automated Backup (Cron Job)"
Q: Apa saja security settings yang harus diubah?
A: QUICK_REFERENCE.md "Security Settings (MUST DO)"
================================================================================
VERIFIKASI SEMUA FILE SUDAH ADA
================================================================================
✅ AUDIT_LENGKAP.md (45.57 KB)
✅ QUICK_REFERENCE.md (6.67 KB)
✅ DATABASE_SCHEMA.md (15.75 KB)
✅ BACKUP_CHECKLIST.md (10.58 KB)
✅ AUDIT_SUMMARY.md (12.31 KB)
✅ DOKUMENTASI_INDEX.md (13.69 KB)
✅ README_AUDIT.txt (This file)
TOTAL: 7 files, 104.56 KB dokumentasi lengkap
================================================================================
NEXT STEPS (URUTAN PENTING)
================================================================================
1. READ (This week)
□ AUDIT_SUMMARY.md (5 min) - Wajib dibaca semua orang!
□ QUICK_REFERENCE.md (10 min) - Reference untuk daily use
□ AUDIT_LENGKAP.md (30 min) - Jika butuh detail
□ DATABASE_SCHEMA.md (20 min) - Jika ada query khusus
□ BACKUP_CHECKLIST.md (10 min) - Sebelum backup
2. BACKUP (Immediately)
□ Create full backup (database + files + code)
□ Store backup in safe location (USB/NAS/Cloud)
□ Test restore procedure
□ Document backup location
3. SECURE (This week)
□ Follow security checklist di AUDIT_SUMMARY.md
□ Change passwords & secrets
□ Setup HTTPS
□ Setup firewall rules
4. MAINTAIN (Ongoing)
□ Setup automated daily backup
□ Setup monitoring & alerting
□ Review logs regularly
□ Test restore monthly
□ Update documentation when changes occur
================================================================================
SUPPORT & RESOURCES
================================================================================
Original README:
README.md (Original aplikasi documentation)
GitHub:
https://github.com/alijayanet/billing-rtrw
Contact:
Ali Jaya Net
Phone: 081947215703
Email: alijayanet@gmail.com
================================================================================
DOKUMEN STATUS
================================================================================
Audit Date: Juni 2026
Audit Version: 1.0 (Final)
Status: ✅ COMPLETE & APPROVED FOR PRODUCTION
Documentation: 6 files, 104 KB total
Last Updated: 2026-06-19
================================================================================
CATATAN PENTING
================================================================================
1. Dokumentasi ini adalah hasil audit teliti & lengkap
2. Semua file tersimpan di root aplikasi billing-B481
3. Documentasi sudah production-ready & siap digunakan
4. Update dokumentasi jika ada perubahan pada aplikasi
5. Backup dokumentasi ini juga (jangan lupa!)
6. Share dokumentasi dengan tim operasional
7. Review dokumentasi secara berkala
================================================================================
SELAMAT MENGGUNAKAN! 🎉
================================================================================
Jika ada pertanyaan atau butuh informasi lebih lanjut:
1. Cek dokumentasi terlebih dahulu (gunakan DOKUMENTASI_INDEX.md untuk search)
2. Baca FAQ section di file-file yang relevan
3. Hubungi tim technical support
Terima kasih telah menggunakan dokumentasi audit lengkap ini!
================================================================================
Generated: 2026-06-19
Status: ✅ AUDIT COMPLETE & DOCUMENTED
+31
View File
@@ -0,0 +1,31 @@
# OPEN-ISP v0.5.1 (pickle-lime) — Patch Keamanan & Stabilitas
Tanggal: 2026-10-13
Rilis patch yang menutup seluruh temuan kerentanan dependensi, sekaligus memperkuat **backup/restore** dan **logging**.
## Keamanan
- `npm audit`: **16 → 0** kerentanan.
- Menghapus `@whiskeysockets/baileys` (tidak dipakai).
- Memperbarui `axios`, `multer`, `brace-expansion`, `proxy-addr`.
- `xlsx` → build resmi **SheetJS 0.20.3** (API sama).
- `node-telegram-bot-api` **0.66 → 2.1.0** (memakai adapter v1→v2 internal; modul bot tidak dirombak).
- Memperbaiki bug **`trust proxy`**: `X-Forwarded-For` terbalik sehingga IP bisa dipalsukan (berdampak pada rate-limit/login).
- `cookie_secure` sebaiknya diaktifkan saat aplikasi dipublikasikan via **HTTPS**.
## Backup & Restore
- Backup database memakai **`VACUUM INTO`** → snapshot **konsisten** walau database berjalan di mode **WAL** (sebelumnya menyalin file mentah berisiko kehilangan transaksi terbaru).
- **Restore diterapkan saat startup** (sebelum koneksi DB dibuka) → menghindari kerusakan data karena menimpa file DB di bawah koneksi hidup. Aplikasi **restart otomatis** untuk menerapkan restore.
- Restore settings memakai pemuatan ulang otomatis (tanpa restart).
## Logging
- Berkas log **diputar otomatis** (10MB × 5) — tidak lagi menumpuk memenuhi disk.
- Seluruh `console.*` sisi server diarahkan ke **logger** → log **seragam** dan tersimpan di `logs/`.
## Lainnya
- `settings.json` di-set permission `600` (rahasia tidak world-readable).
- Versi dinaikkan ke **0.5.1**.
## Cara Update
- **Docker:** `docker compose pull` / `docker compose up -d --build`.
- **Manual:** `git pull` lalu `npm ci --omit=dev` dan restart aplikasi.
-202
View File
@@ -1,202 +0,0 @@
# Rencana Desain & Modernisasi UI/UX — Billing RT/RW Net
*(Mengadopsi Referensi uiverse.io, ColorlibHQ/gentelella, dan Google Fonts)*
> **Status Server Biznet**: ✅ **Berhasil Dimatikan & Dinonaktifkan** (`systemctl stop/disable`, `pm2 stop/delete`). Tidak ada lagi tabrakan bot Telegram ataupun service jaringan.
> **Server Produksi Aktif**: VPS `deneva` (`116.212.74.115` — port `3001`).
---
## 1. Konsep Desain & Pendekatan Dua Arah (Dual-Persona)
Aplikasi ini melayani dua profil pengguna yang berbeda secara mendasar:
1. **Pelanggan (Customer Portal & Landing/Login)**: Menggunakan smartphone, membutuhkan antarmuka yang modern, bersih, intuitif, cepat, dan kaya animasi mikro.
* **Referensi Utama**: **`uiverse.io`** (Glassmorphism, animasi tombol neon/glow, switch toggle interaktif, floating dock, card 3D halus).
2. **Admin & Teknisi (Admin & NOC Portal)**: Membutuhkan visualisasi data densitas tinggi (*high-density information*), pemantauan lalu lintas real-time, status ONU dalam jumlah ratusan, serta tabel data yang efisien.
* **Referensi Utama**: **`ColorlibHQ/gentelella`** (Gentelella Admin — Tile KPI counters, multi-tab `x_panel` containers, collapsible sidebar, traffic charts, dense status badges).
```mermaid
graph TD
subgraph UI_Reference_System["Sistem Referensi Desain"]
UIVERSE["uiverse.io (Modern CSS Components)"]
GENTELELLA["ColorlibHQ/gentelella (Admin Dashboard)"]
GFONTS["Google Fonts (Universal Typography)"]
end
subgraph Customer_Side["Portal Pelanggan (PWA & Login)"]
CustLogin["views/login.ejs (Hero & Glass Card)"]
CustDash["views/dashboard.ejs (5-Tab App Shell)"]
CustIsolated["views/isolated.ejs (Walled Garden)"]
end
subgraph Admin_Side["Portal Admin, NOC & Teknisi"]
AdminDash["views/admin/dashboard.ejs (Tile Stats & Charts)"]
AdminTables["views/admin/* (Customers, OLT, Billing)"]
AdminSidebar["views/admin/partials/sidebar.ejs"]
AdminMap["views/admin/map.ejs (Network Topology)"]
end
UIVERSE -->|Glass Cards, Neon Toggles, Pill Buttons| Customer_Side
GENTELELLA -->|Tile Counters, X-Panels, Dense Tables| Admin_Side
GFONTS -->|Plus Jakarta Sans, Inter, JetBrains Mono| Customer_Side
GFONTS --> Admin_Side
```
---
## 2. Tipografi Universal (Google Fonts)
Untuk memastikan kompatibilitas rendering 100% di semua browser (Chrome, Safari iOS, Firefox, Edge, Android WebView) tanpa ketergantungan pada font sistem lokal:
| Peruntukan | Font Family | Bobot (Weights) | Karakteristik & Kegunaan |
|---|---|---|---|
| **Customer Portal & Landing** | **Plus Jakarta Sans** | `300, 400, 500, 600, 700, 800` | Modern geometric sans, sangat nyaman dibaca di layar AMOLED HP, angka tagihan tegas dan modern. |
| **Admin & Operasional** | **Inter** | `300, 400, 500, 600, 700, 800` | Standar industri dashboard, metrik angka rapat (*tabular numbers*), hierarki teks rapi. |
| **Data Teknis & Jaringan** | **JetBrains Mono** | `400, 500, 700` | Alamat IP, MAC Address, OID SNMP, Serial Number ONU, nilai redaman optik (dBm). |
### Standar Preconnect & Loading:
```html
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500;700&family=Plus+Jakarta+Sans:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
```
---
## 3. Peta Komponen & Transformasi Desain
### 3.1 Portal Admin (Adopsi `ColorlibHQ/gentelella`)
#### A. Gentelella Top Metric Stats ("Tile Count")
Pada dashboard admin (`views/admin/dashboard.ejs`), susunan metrik diganti dengan pola **Gentelella Tile Count**:
* **Format**: Baris kartu metrik ringkas horizontal dengan ikon latar samar, angka besar tebal, label deskriptif, dan indikator persentase tren (hijau naik / merah turun).
* **Komponen yang Diterapkan**:
1. `Total Pelanggan`: Angka aktif vs total, delta pertumbuhan bulanan.
2. `PPPoE Online`: Real-time active sessions dengan status dot hijau berkedip.
3. `Total ONU & Signal`: Jumlah online/offline + ONU dengan redaman kritis (< -27 dBm).
4. `Tagihan & Kas Masuk`: Pendapatan bulan berjalan vs tagihan tertunda (*unpaid*).
```
┌─────────────────┬─────────────────┬─────────────────┬─────────────────┐
│ TOTAL PELANGGAN │ PPPoE ONLINE │ STATUS FTTH │ PENDAPATAN BULAN │
│ 1,248 │ 1,180 │ 102 / 105 │ Rp 42.500.000 │
│ ▲ +12 bln ini │ ● 94.5% online │ ⚠️ 3 Redaman Low│ ▲ 88% Tertagih │
└─────────────────┴─────────────────┴─────────────────┴─────────────────┘
```
#### B. Kontainer Widget Bergaya `x_panel`
* Seluruh card data admin menggunakan struktur `x_panel`:
* `x_title`: Judul section dengan border bawah tipis, sub-teks muted, dan *toolbox* kanan (tombol refresh data via AJAX, filter tanggal, expand/collapse).
* `x_content`: Area konten responsif dengan padding lega, kompatibel dengan mode gelap dan terang.
#### C. Tabel Data Responsif (*Dense Table*)
* Border halus (`border-slate-800`), font `Inter` 13px, baris zebra-stripe halus saat hover.
* Badge status terstandarisasi:
* Online: Badge hijau lembut (`bg-emerald-500/10 text-emerald-400 border border-emerald-500/20`).
* Offline / Suspend: Badge merah lembut (`bg-rose-500/10 text-rose-400 border border-rose-500/20`).
* Isolir: Badge amber (`bg-amber-500/10 text-amber-400 border border-amber-500/20`).
---
### 3.2 Portal Pelanggan & Login (Adopsi `uiverse.io`)
#### A. Card Glassmorphism dengan Border Glow
* Card profil router, tagihan, dan paket internet menggunakan teknik glassmorphism tingkat tinggi dari `uiverse.io`:
* Background: `rgba(30, 41, 59, 0.75)` dengan `backdrop-filter: blur(16px)`.
* Border: Gradasi tipis `linear-gradient(135deg, rgba(45, 212, 191, 0.3), rgba(99, 102, 241, 0.1), transparent)`.
* Shadow: `0 10px 30px -10px rgba(0, 0, 0, 0.5)`.
#### B. Switch Toggle & Input Kustom (Form Ganti WiFi & Settings)
* Mengganti checkbox standar dengan **animated toggle switch** gaya `uiverse.io`:
* Toggle 2.4 GHz vs 5 GHz dengan slider pill bercahaya.
* Input password dengan animated floating border dan icon eye reveal responsif.
* Tombol submit: Efek hover *lift-up* (`translateY(-2px)`) dengan box-shadow glow warna Teal `#14b8a6`.
#### C. Floating Action Dock (PWA Mobile Navigation)
* Penyempurnaan 5-tab dock navigasi bawah:
* Efek frosted glass yang lebih pekat (`backdrop-blur-xl`).
* Active pill bergradasi halus dengan transisi kurva cubic-bezier (`cubic-bezier(0.34, 1.56, 0.64, 1)`).
* Touch feedback haptic visual saat tab ditekan di layar HP.
---
## 4. Design Tokens & Palette Warna
```css
:root {
/* Brand Core (Skyfiber Identity) */
--brand-teal-50: #f0fdfa;
--brand-teal-400: #2dd4bf;
--brand-teal-500: #14b8a6;
--brand-teal-600: #0d9488;
--brand-indigo: #6366f1;
--brand-cyan: #06b6d4;
/* Dark Theme Palette (Default) */
--bg-dark-base: #0b0f19;
--bg-dark-card: #1e293b;
--bg-dark-panel: #161b26;
--border-dark: rgba(148, 163, 184, 0.15);
--text-dark-high: #f8fafc;
--text-dark-med: #94a3b8;
--text-dark-low: #64748b;
/* Light Theme Palette */
--bg-light-base: #f8fafc;
--bg-light-card: #ffffff;
--bg-light-panel: #f1f5f9;
--border-light: rgba(15, 23, 42, 0.08);
--text-light-high:#0f172a;
--text-light-med: #475569;
--text-light-low: #94a3b8;
/* Status Colors */
--status-online: #10b981;
--status-offline: #f43f5e;
--status-warning: #f59e0b;
--status-info: #38bdf8;
}
```
---
## 5. Rencana Tahapan Eksekusi (Implementation Milestones)
```mermaid
gantt
title Roadmap Eksekusi Desain & Modernisasi
dateFormat X
axisFormat Fase %s
section Persiapan
Matikan Service Biznet & Verifikasi Port :done, 0, 1
Integrasi Google Fonts Universal :done, 1, 2
section Portal Pelanggan (uiverse.io)
Modernisasi Komponen WiFi & Card Glass :active, 2, 4
Input Micro-Animations & Button Glow : 4, 5
Verifikasi PWA Mobile & Responsive Check : 5, 6
section Portal Admin (Gentelella)
Implementasi Top Tile Stats Count : 6, 8
Standardisasi X-Panel & Action Toolbox : 8, 9
Penyelarasan Tabel Data & Badges : 9, 10
section Validasi & Deploy
Pengujian Lintas Browser & Dark/Light Mode : 10, 11
Restart Service & Verifikasi Uptime : 11, 12
```
### Tahap 1: Penguatan Komponen Customer Portal (Uiverse Style)
1. Terapkan card glassmorphism dengan glow gradient pada form ganti SSID & Password di `views/dashboard.ejs`.
2. Sempurnakan animated button untuk aksi "Simpan Wi-Fi", "Beli Voucher", dan "Cek Tagihan".
3. Pastikan visual state mode terang (*light mode*) memiliki kontras tajam pada teks muted dan input field.
### Tahap 2: Modernisasi Admin Dashboard (Gentelella Style)
1. Perbarui bagian atas `views/admin/dashboard.ejs` dengan modul **Gentelella Tile Stats**:
* Total Pelanggan, Status Sesi MikroTik Aktif, Status ONU (Online/Offline/Warning Redaman), dan Tagihan Lunas/Tertunda.
2. Tata ulang card monitoring ke dalam format `x_panel` yang memiliki toolbar aksi (Refresh AJAX, filter status, expand).
### Tahap 3: Uji Coba Lintas Perangkat & Validasi Akhir
1. Uji di mobile browser (Android Chrome & iOS Safari).
2. Verifikasi seluruh endpoint AJAX, ganti SSID modem, dan refresh ONU tetap berfungsi tanpa gangguan.
3. Pastikan log journalctl pada `billing-rtrw.service` di `deneva` tetap bersih dan stabil.
-729
View File
@@ -1,729 +0,0 @@
# RINGKASAN APLIKASI - RTRWNET BILLING & MANAGEMENT SYSTEM
**Status:** ✅ FULLY EXPLORED & DOCUMENTED
**Tanggal:** 14 Agustus 2026
**Format:** Quick Reference untuk understanding aplikasi
---
## 🎯 APLIKASI OVERVIEW
### What is RTRWNET?
**RTRWNET** adalah sistem manajemen terintegrasi untuk **ISP/RTRW** yang mengelola:
- ✅ Billing & invoicing otomatis
- ✅ Manajemen pelanggan & paket
- ✅ Jaringan fisik (MikroTik, OLT, ODP, cable routing)
- ✅ Device management (CPE/ONU via TR-069/GenieACS)
- ✅ Payment gateway integration (Midtrans, Tripay, Xendit, Duitku)
- ✅ Agent/reseller management
- ✅ WhatsApp automation
- ✅ Support ticketing
- ✅ Peta jaringan & GIS
- ✅ Monitoring & health check
**Single Platform:** Satu dashboard untuk admin, teknisi, agen, dan pelanggan.
---
## 📚 PORTAL UTAMA
| Portal | URL | User | Fungsi Utama |
|--------|-----|------|--------------|
| **Customer** | `/customer` | Pelanggan | Cek tagihan, bayar, ubah Wi-Fi, lihat status |
| **Admin** | `/admin` | Admin | Dashboard, billing, customer, MikroTik, settings |
| **Teknisi** | `/tech` | Teknisi | Tiket, peta, device monitoring, support |
| **Agen** | `/agent` | Reseller/Agent | Bayar tagihan pelanggan, jual voucher, jual pulsa |
| **Kolektor** | `/collector` | Collector | Cek & ajukan pembayaran (pending approval) |
| **Finance** | `/finance` | Admin/Finance | Laporan keuangan & pengeluaran |
| **Public** | `/` | Publik | Cek tagihan tanpa login, halaman info |
---
## 🏗️ ARSITEKTUR TEKNIS
### Backend Services (34 services)
```
1. billingService.js → Invoice generation, promo, prorata
2. customerService.js → CRUD customer, paket, isolir
3. mikrotikService.js → PPPoE, hotspot, firewall, monitoring
4. oltService.js → OLT management via SNMP
5. whatsappService.js → WhatsApp gateway (Baileys)
6. paymentService.js → Payment gateway integration
7. voucherPaymentService.js → E-voucher & payment methods
8. agentService.js → Agent & Digiflazz integration
9. cronService.js → Scheduled tasks automation
10. monitoringService.js → System health & metrics
11. ticketService.js → Support ticketing
12. inventoryService.js → Stock & asset management
13. auditTrailService.js → Action logging
14. backupService.js → Database backup
15. onuProvisionService.js → ONU TR-069 provisioning
... + 19 services lainnya (acsServerService, radiusServerService, dll)
```
### Database (SQLite)
```
Total: 50+ tables
Kategori:
- Master: packages, customers, routers, olts, odps
- Billing: invoices, payments, payment_approvals
- Network: pppoe_users, hotspot_users, vouchers
- Device: acs_devices, onu_mapping
- Internal: technicians, agents, collectors, audit_trail
- Other: expenses, cash_in, tickets, inventory_items
```
### Routes/Endpoints (250+ endpoints)
```
/customer/* → Customer portal (login, billing, device)
/admin/* → Admin panel (all management)
/tech/* → Technician portal (tickets, map, devices)
/agent/* → Agent portal (payment, voucher, pulsa)
/collector/* → Collector portal (approval workflow)
/finance/* → Finance reporting
/acs/* → Built-in TR-069 ACS server
/api/webhook/* → Payment webhooks
```
### Frontend (EJS Templates)
```
18 main views + partials:
- customer/ → 6 views (billing, device, voucher, etc)
- admin/ → Subdirectory dengan main routes
- tech/ → 3 views (dashboard, tickets, monitoring)
- agent/ → 3 views
- collector/ → 2 views
- Public pages → login, register, check_billing, tos, privacy
```
---
## 💰 FITUR BILLING
### Invoice Cycle
```
1. Otomatis: Cron @ 1 Jan setiap bulan jam 00:01
- Generate invoice untuk semua customer
- Kalkulasi promo price (jika applicable)
- Kalkulasi prorata (bulan pertama)
- Kalkulasi tax (PPN 11%)
- Status: 'unpaid'
2. Notifikasi WhatsApp otomatis ke customer
3. Customer bayar via:
- Payment gateway (Midtrans, Tripay, Xendit, Duitku)
- QRIS static (QR fixed dengan nominal unik)
- Manual via admin/agen
- Kolektor (pending approval)
4. Status berubah: 'unpaid' → 'paid'
5. Notifikasi paid + auto-unisolir jika dalam suspend
6. Late payment → Auto-isolir @ hari yang ditentukan
```
### Promo & Prorata
```
Promo:
- Paket A: Rp 350rb/bulan, promo Rp 250rb (3 siklus)
- Pelanggan baru: dapat promo 3 bulan pertama
- Counter ditrack di customer.promo_cycles_used
- Reset otomatis saat ganti paket
Prorata:
- Tanggal pasang: 15 Agustus
- Invoice pertama: Rp 350rb * (16 hari / 31 hari) = Rp 180rb
- Bulan berikutnya: Rp 350rb penuh
```
### Payment Gateway
```
Aktif (bisa kombinasi):
- Midtrans → CC, Transfer, E-wallet
- Tripay → Bank Transfer, QRIS, E-wallet
- Xendit → Virtual Account, QR
- Duitku → Aggregator
- QRIS Static → Fixed QR code
Webhook Generic:
- Endpoint: POST /api/webhook/v1/payment-notif
- Accept dari bank/e-wallet apapun
- Auto-match nominal → mark paid
- Log semua untuk audit
```
---
## 👥 CUSTOMER MANAGEMENT
### Data per Customer
```
Identitas:
- Nama, HP, Email, NIK, Alamat
- GPS Latitude/Longitude
- Area/Kabupaten
Jaringan:
- Paket (Starter/Pro/Premium)
- MikroTik Router assign
- PPPoE username
- Hotspot username (optional)
- Static IP (optional)
- OLT & ONU Port
- GenieACS Tag
Status & Billing:
- Status: active/suspended/inactive
- Isolir day (tanggal isolir otomatis)
- Auto-isolir enable/disable
- Promo cycles used
```
### Isolir Workflow
```
Manual Isolir:
1. Admin klik "Isolir" di customer detail
2. Sistem hapus/disable PPPoE user di MikroTik
3. Status berubah: active → suspended
4. Redirect ke page /isolated (jika online)
Auto-Isolir:
1. Cron job setiap hari jam 02:00
2. Cek customer: status=active + unpaid invoices + today >= isolate_day
3. Suspend PPPoE ke MikroTik
4. Notifikasi WhatsApp
Unisolir:
1. Pelanggan bayar
2. Admin klik "Buka Isolir"
3. Restore PPPoE user
4. Status: suspended → active
5. Customer online lagi
```
---
## 🌐 MIKROTIK INTEGRATION
### PPPoE Workflow
```
1. Create customer dengan PPPoE username
2. Sistem generate PPPoE secret (password)
3. Connect ke MikroTik Router API
4. Add user ke PPPoE service
5. Customer bisa login dengan username/password
Live Monitoring:
- View active sessions
- Bytes in/out per session
- Session duration
- IP address
FUP (Fair Usage Policy):
- Paket: limit 100GB/bulan
- Cron setiap jam cek usage
- Jika > 100GB: ganti profile ke 1Mbps
- Reset 1 Jan bulan berikutnya
Night Mode (Jam Kalong):
- 00:00 - 06:00: Speed 20Mbps
- 06:00 - 00:00: Speed 10Mbps
- Cron @ 00:00 & 06:00 switch profile
```
### Hotspot
```
Similar ke PPPoE:
- Create user dengan limit (uptime/data)
- Monitor active sessions
- Bandwidth limiting
```
### Voucher Hotspot
```
1. Admin create batch (qty 100, validity 30 hari)
2. Sistem generate 100 kode voucher unik
3. Sync ke MikroTik hotspot
4. Customer pakai untuk login hotspot portal
5. Agent bisa jual voucher ke pelanggan
6. Track status: created/active/used/expired
```
---
## 🎛️ GENIEACS & TR-069
### Built-in ACS
```
Aplikasi punya ACS internal (lightweight):
- Tidak perlu GenieACS eksternal
- Support ZTE, Huawei, FiberHome, dll
- Auto-detect device properties
- Port: 7547 (configurable)
```
### Device Management
```
Features:
- List semua ONU/CPE
- View device status (online/offline)
- Change SSID / Wi-Fi password
- Bulk SSID change
- Reboot device
- Provision WAN (VLAN, bridge, PPPoE)
- View device logs
- Parameter configuration
```
### Workflow
```
1. ONU/CPE boot → connect ke ACS server
2. Sistem auto-detect device properties
3. Admin bisa configure via panel
4. Change SSID → send ke device via TR-069
5. Device update config otomatis
6. Customer notif via WhatsApp
```
---
## 🔌 OLT & ONU MANAGEMENT
### OLT Setup
```
Konfigurasi:
- IP address OLT
- SNMP community string
- Telnet credentials (untuk config WAN)
- Web interface credentials
Multi-OLT:
- Bisa manage banyak OLT sekaligus
- Per pelanggan assign ke OLT mana
```
### ONU Monitoring
```
Via SNMP query:
- Status (online/offline)
- Serial number
- Optical signal (dBm)
- Distance ke OLT
- RX/TX power
Operations:
- Reboot ONU
- Rename (friendly name)
- Authorize ONU (EPON)
- Configure WAN (via Telnet/TR-069/REST API)
```
---
## 💳 PAYMENT FLOW
### Customer Bayar Invoice
```
1. Customer login portal
2. Lihat tagihan belum bayar
3. Click "Bayar"
4. Pilih metode pembayaran (dari gateway aktif)
5. Redirect ke payment gateway
6. Customer input data (CC/transfer/etc)
7. Gateway proses
8. Success → Webhook ke aplikasi
9. Aplikasi verify amount & signature
10. Mark invoice PAID
11. Send WhatsApp confirmation
12. Auto-unisolir jika customer dalam suspend
13. Dashboard terupdate
```
### Webhook Payment
```
Generic endpoint untuk bank/e-wallet:
POST /api/webhook/v1/payment-notif
Bisa terima dari:
- Bank (SMS gateway → webhook)
- E-wallet (API callback)
- QRIS (bank QRIS provider)
Sistem:
- Parse amount dari notifikasi
- Find invoice / voucher dengan nominal cocok
- Verify jika nominal sudah digunakan (duplikasi)
- Mark as PAID
- Log untuk audit
```
---
## 🗺️ GIS MAPPING
### Peta Admin
```
Features:
- Leaflet map (OpenStreetMap + Satellite)
- Marker customer (warna: hijau/merah/biru)
- Marker ODP (orange)
- Draw cable path (customer → ODP)
- Popup detail (klik marker)
- Zoom/pan/locate
Interaksi:
- Click customer → detail + isolir button
- Click ODP → info + available ports
- Draw polyline → cable path (simpan)
- Real-time PPPoE traffic graph
- Google Maps route (untuk tech)
```
### Cable Path
```
Simpan rute dari customer ke ODP:
- Array of [lat, lng] coordinates
- Calculate distance (KM)
- Description (aerial/underground/etc)
- Edit/delete existing path
```
### Tech GPS (Optional)
```
- Tech login portal dengan mobile
- Sistem track GPS lokasi tech
- Geofence alert (keluar/masuk area kantor)
- Nearby customers di radius tertentu
- Chat WhatsApp dari peta
```
---
## 💬 WHATSAPP AUTOMATION
### Setup
```
1. Admin buka /admin/whatsapp
2. Scan QR code dengan WhatsApp app
3. Verify & approve
4. Bot siap mengirim pesan
Gateway:
- Baileys (Unofficial WebSocket)
- Meta Cloud API (Official, jika setup)
```
### Automated Messages
```
Invoice Created:
"Tagihan Anda bulan ini siap!
Nominal: Rp 350.000
Bayar di: [link]
Due: 10 Agustus"
Invoice Paid:
"✅ Pembayaran diterima!
Tagihan Rp 350.000 LUNAS
Terima kasih!"
Isolir Reminder (H-1):
"⚠️ Reminder: Jatuh tempo BESOK
Tagihan Rp 350.000
Bayar untuk hindari isolir"
Isolir Notice:
"⚠️ Layanan Anda DIPUTUS
Penyebab: Tagihan belum dibayar
Hubungi [support]"
Unisolir:
"✅ Layanan Anda kembali AKTIF
Terima kasih pembayarannya!"
```
### Admin Commands (via WhatsApp)
```
"BILLING" → Send laporan billing hari ini
"PPPOE" → List active PPPoE sessions
"BALANCE" → Cek saldo Digiflazz
"TOPUP 1000000" → Top-up agent
"INVOICE" → List unpaid invoices
```
### Broadcast
```
Select customers → Create message → Schedule → Send
- Set delay antar pesan (3-5 detik)
- Track delivery status
- Pause/resume/stop anytime
- Retry failed messages
```
---
## 👔 AGENT MANAGEMENT
### Agent Fitur
```
Dashboard:
- Total saldo (Rp)
- Today's transaction
- Commission earned
- Top customers
Pembayaran:
- Cari pelanggan
- Lihat unpaid invoices
- Bayar dari saldo agent
- Print struk
Voucher:
- Lihat inventory
- Jual ke customer
- Print code
- Track sale
Digiflazz (Pulsa/Data):
- Browse produk (Telkomsel, Indosat, dll)
- Category → Brand → Nominal
- Buy untuk customer
- Webhook status update
```
### Admin Agent Control
```
- Create/edit agent
- Top-up balance
- Set commission %
- View transaction history
- Special pricing per agent
- Performance report
```
---
## 🎫 TICKETING SYSTEM
### Ticket Status Flow
```
open → in_progress → resolved → closed
Open:
- Customer create di portal
- Auto-assign ke admin/tech
In Progress:
- Tech ambil ticket
- Add notes/photos
- Update status
Resolved:
- Tech mark selesai
- Notif ke customer
Closed:
- Customer confirm selesai
- Optional: give rating (1-5)
```
### Admin Features
```
- Dashboard tickets (count per status)
- Assign ke technician
- Reassign jika perlu
- View ticket history
- SLA tracking
- Performance report
```
### Tech Features
```
- Pool tickets (assigned to me)
- View detail
- Take ticket
- Upload photos (before/after)
- Add resolution notes
- Mark resolved
```
### Customer Features
```
- Create ticket
- View my tickets
- See status update
- Add comment/photo
- Rate satisfaction
```
---
## ⚙️ AUTOMATION (CRON JOBS)
| Waktu | Tugas | Detail |
|-------|-------|--------|
| 1 Jan @ 00:01 | Generate invoices | Monthly billing untuk semua customer |
| Setiap hari @ 02:00 | Auto-isolir | Suspend customer overdue |
| Setiap hari @ 09:00 | Reminder tagihan | WhatsApp reminder H-1 isolir |
| 00:00 & 06:00 | Night mode | Ganti profile PPPoE malam/siang |
| Setiap jam | FUP check | Check usage vs limit, ganti profile |
| Setiap 10 menit | Usage sync | Sinkron PPPoE traffic dari MikroTik |
| Setiap hari @ 02:00 | Database backup | Auto-backup dengan retention |
---
## 📊 DATABASE OVERVIEW
### Key Tables
```
customers
├── Identitas (name, phone, email, nik)
├── Jaringan (pppoe_username, hotspot_username, static_ip)
├── Device (olt_id, onu_port, genieacs_tag)
├── Billing (package_id, isolate_day, auto_isolate)
└── Status (status, created_at, updated_at)
invoices
├── Amount (amount, status, qris_amount_unique)
├── Period (period_month, period_year)
├── Payment (paid_at, payment_method)
└── Notes (promo/prorata info)
payments
├── Transaction data (gateway_id, signature, status)
├── Customer info
└── Invoice link
pppoe_users
├── Credentials (username, secret)
├── Profile & limits
└── Session tracking
... + 40+ tables lainnya
```
---
## 🎯 SUMMARY - FITUR UTAMA YANG BERJALAN
✅ **Billing & Invoicing**
- Auto generate bulanan
- Promo cycles tracking
- Prorata bulan pertama
- Multi-gateway payment
✅ **Customer Lifecycle**
- CRUD pelanggan
- Paket management
- Auto-isolir/unisolir
- Status tracking
✅ **Network Management**
- MikroTik PPPoE/Hotspot
- Voucher hotspot
- Firewall & rate limiting
- Night mode automation
✅ **Device Management**
- Built-in TR-069 ACS
- GenieACS integration
- SSID/password change
- Device provisioning
✅ **OLT/ONU Management**
- SNMP monitoring
- Reboot/rename/authorize
- WAN configuration
- Multi-brand support
✅ **Payment Processing**
- 4 payment gateways
- QRIS static
- Generic webhook
- Payment verification
✅ **GIS Mapping**
- Leaflet maps
- Cable path tracking
- GPS tech tracking
- Real-time graphs
✅ **WhatsApp Automation**
- Invoice notification
- Payment reminder
- Isolir notice
- Broadcast messaging
✅ **Agent Management**
- Pembayaran dari saldo
- Voucher sales
- Pulsa/data (Digiflazz)
- Commission tracking
✅ **Support Ticketing**
- Create/assign/resolve
- Photo upload
- Rating system
- History tracking
✅ **Monitoring**
- System health metrics
- Service status
- Alert system
- Performance tracking
✅ **Automation**
- 7 scheduled jobs
- Configurable timing
- Error handling
- Logging
✅ **Multi-Portal**
- Customer self-service
- Admin full control
- Technician field support
- Agent sales
- Collector approval
✅ **Compliance**
- Audit trail logging
- Role-based access
- Session management
- Data validation
---
## 🚀 PRODUCTION STATUS
**Aplikasi ini SUDAH BERJALAN** dan mencakup:
- ✅ Complete billing system
- ✅ Full customer management
- ✅ Network device integration
- ✅ Payment processing
- ✅ Multi-user portals
- ✅ Automation & scheduling
- ✅ Monitoring & health check
**Siap untuk:**
- ✅ Production deployment
- ✅ Scaling operasi ISP
- ✅ Integration ke sistem existing
- ✅ Customization per requirements
---
## 📖 DOKUMENTASI TERSEDIA
1. **FITUR_APLIKASI_LENGKAP.md** ← Dokumentasi ini (16 fitur utama + detail)
2. **AUDIT_LENGKAP.md** (Existing) ← Technical deep dive
3. **DATABASE_SCHEMA.md** (Existing) ← Struktur data lengkap
4. **QUICK_REFERENCE.md** (Existing) ← Common commands
5. **AUDIT_SECURITY_2026.md** (Baru) ← Security audit findings
6. **IMPLEMENTATION_GUIDE_SECURITY.md** (Baru) ← Fix guide
7. **README.md** (Existing) ← Instalasi & setup
---
**Status:** ✅ Aplikasi komprehensif, well-documented, siap production
**Next Step:** Implementasi security fixes (bcrypt, .env, CSRF) sebelum production
-81
View File
@@ -1,81 +0,0 @@
# RUJUKAN ARSITEKTUR & FITUR SISTEM BILLING RTRW-NET
Dokumen ini disimpan sebagai **rujukan utama** arsitektur, fitur backend, frontend, dan skema database sebelum dilakukan penambahan fitur RADIUS Server.
---
## 1. DOKUMENTASI MODUL & KOMPONEN EKSISTING
### A. Core Engine & Entry Point
- **`app-customer.js`**: Main entry point Express.js v4.
- Memuat middleware security: CORS, Referer/Origin CSRF Protection, Express Session, Body Parser limit 1MB.
- Pengatur i18n multi-bahasa (`locales/`).
- Menjalankan background service: Auto-backup (`scheduleAutoBackup`), Cron Billing (`cronService.js`), WhatsApp Bot, Telegram Bot.
- Mengunci DNS ke IPv4 (`dns.setDefaultResultOrder('ipv4first')`) untuk mencegah timeout IPv6 di server produksi.
### B. Database & Stored Layer (`config/database.js`)
- **Engine**: SQLite 3 menggunakan driver `better-sqlite3` (Native C++ extension, high performance).
- **Mode**: `WAL` (Write-Ahead Logging) dengan `foreign_keys = ON`.
- **Fungsi Kustom SQLite**: `NOW_LOCAL()` (memformat tanggal-waktu berdasarkan timezone lokal `Asia/Jakarta`).
- **Tabel Utama**:
1. `settings` (Dynamic Key-Value Store)
2. `packages` (Paket Internet: harga, speed up/down, promo, jam kalong, FUP)
3. `customers` (Data Pelanggan: no_pelanggan, nama, phone, pppoe_user, pppoe_secret, status, isolir_date, router_id, odp_id, genieacs_tag)
4. `invoices` & `payments` (Tagihan & Pembayaran)
5. `routers` (Daftar Router MikroTik: host, port 8728, username, password, status)
6. `pppoe_users` & `pppoe_traffic_samples` & `pppoe_sessions`
7. `hotspot_users` & `hotspot_profiles` & `hotspot_sessions`
8. `voucher_batches` & `vouchers` & `public_voucher_orders`
9. `olts`, `odps`, `onu_mapping`, `acsdevices` (Modul FTTH & TR-069 ACS)
10. `technicians`, `cashiers`, `collectors`, `agents` (Akses multi-role)
11. `audit_trail` (Log aktivitas pengguna)
### C. Services Layer (`services/`)
1. **`mikrotikService.js`**:
- Menghubungkan aplikasi ke RouterOS via API port 8728/8729 menggunakan `routeros-client`.
- Mengelola PPPoE Secret (Add, Update, Delete, Toggle Enable/Disable).
- Mengelola Hotspot User & Active Sessions.
- Mengambil trafik interface & active connections secara realtime.
- Mengatur isolir pelanggan via MikroTik API (mengubah profile / IP pool).
2. **`customerService.js`**:
- CRUD Pelanggan, komparasi data lokal vs router.
- Penanganan siklus isolir & pemulihan status otomatis.
3. **`billingService.js`**:
- Penjadwalan & pembuatan invoice bulanan.
- Pembayaran invoice (Cash, Transfer, QRIS, Gateway Midtrans/Tripay/Xendit).
- Eksekusi auto-isolir saat jatuh tempo.
4. **`cronService.js`**:
- Menjalankan penagihan otomatis jam 00:05.
- Cek auto-isolir harian jam 01:00.
- Sampling statistik trafik jam 10 menit sekali.
- Notifikasi reminder tagihan via WA / Telegram.
5. **`oltService.js` & `onuProvisionService.js` & `acsServerService.js`**:
- Integrasi OLT ZTE/Huawei/FiberHome via SNMP & Telnet.
- TR-069 Auto-Provisioning CPE via GenieACS.
### D. Route & Portal Layer (`routes/`)
1. **`/admin` (`adminPortal.js`)**: Portal Utama Admin (Dashboard, Pelanggan, Paket, Router, Transaksi, Laporan, OLT/ACS, Settings).
2. **`/customer` (`customerPortal.js`)**: Portal Pelanggan Self-Service (Cek Tagihan, Bayar QRIS, Tiket Komplain).
3. **`/agent` (`agentPortal.js`)**: Portal Agen (Jual Voucher, Topup Pulsa Digiflazz).
4. **`/collector` (`collectorPortal.js`)**: Portal Kolektor Penagih Lapangan.
5. **`/tech` (`techPortal.js`)**: Portal Teknisi (Tiket Gangguan, Pasang Baru, Mapping ODP).
6. **`/finance` (`financePortal.js`)**: Portal Keuangan (Pemasukan & Pengeluaran Kas).
### E. Frontend & Views (`views/`)
- Menggunakan **EJS (Embedded JavaScript)** dengan template responsive.
- Menggunakan CSS Vanilla & Custom Theme (Dark/Light Mode), Bootstrap Icons / FontAwesome.
- AJAX Fetch API untuk interaksi data tanpa reload halaman.
---
## 2. ATURAN INTEGRASI FITUR RADIUS (NON-BREAKING GUARANTEE)
1. **Prinsip Utama**: Seluruh modul MikroTik API eksisting **TIDAK BOLEH DIUBAH ATAU DIHAPUS**.
2. **Fleksibilitas**: Sistem akan mendukung 2 mode otentikasi router:
- **Mode 1 (Default Eksisting)**: MikroTik API (`routeros-client`).
- **Mode 2 (Fitur Baru)**: RADIUS Server (UDP Port 1812/1813).
- **Mode 3 (Hybrid)**: MikroTik API + RADIUS Server berjalan bersamaan.
3. **Database Integration**: RADIUS Server akan langsung membaca & menulis ke `database/billing.db` eksisting:
- Mengotentikasi kredensial pengguna dari tabel `customers` / `pppoe_users` / `hotspot_users`.
- Mengirimkan atribut RADIUS standar (`Framed-IP-Address`, `Mikrotik-Rate-Limit`, `Session-Timeout`).
- Menerima paket RADIUS Accounting (Start/Interim/Stop) dan mencatat penggunaan kuota/bandwidth ke tabel `pppoe_traffic_samples` & `radius_accounting_logs`.
-98
View File
@@ -1,98 +0,0 @@
# UI/UX Rules — Billing RTRW (SKYFIBER)
> Dokumen acuan wajib untuk **semua** pekerjaan UI/UX di project ini.
> Setiap kali membuat / mengubah tampilan, ikuti aturan di bawah.
---
## Peran
**Act as an Expert Senior Frontend Developer.**
Your task is to generate a highly responsive, modern, and fluid web layout for
**[Sebutkan apa yang ingin dibuat, misal: a Dashboard / Landing Page / Navigation Bar]**
using **[Sebutkan teknologi, misal: HTML & Tailwind CSS / HTML & Vanilla CSS / EJS]**.
> Di project ini umumnya: **EJS + Vanilla CSS** (`public/css/admin.css`, `landing.css`,
> `auth.css`, `admin-theme.css`) dan **Bootstrap Icons** (bukan emoji di UI).
---
## Strict Layout & Responsiveness Requirements
### 1. Fluid Typography & Spacing
- Gunakan fungsi CSS **`clamp()`** untuk ukuran font, padding, dan margin agar skala
mulus di semua ukuran layar (mobile → desktop 4K) **tanpa** hanya mengandalkan
media query yang patah-patah.
- Contoh: `font-size: clamp(0.95rem, 0.9rem + 0.5vw, 1.25rem);`
`padding: clamp(12px, 2vw, 28px);`
### 2. Zero Overlap Guarantee
Pastikan teks, kartu, tombol, dan ikon **tidak pernah** tumpang tindih atau keluar
dari kontainernya:
- `overflow-wrap: break-word;` atau `word-break: break-word;` untuk teks panjang
(mis. link, nama panjang, kode).
- `min-width: 0;` pada **flex/grid children** agar tidak meluber keluar parent.
- Gunakan **`flex-wrap: wrap` + CSS `gap`** (bukan margin tetap) untuk jarak antar item.
- Gambar & ikon harus responsif: `max-width: 100%; height: auto;` atau
`aspect-ratio` + `object-fit: contain|cover`.
### 3. Flexible & Mappable Structure (CSS Grid / Flexbox)
- Bangun layout yang **self-adapting**.
- **CSS Grid** dengan `repeat(auto-fit, minmax(..., 1fr))` untuk grid kartu agar
otomatis memetakan ke ruang layar yang tersedia.
Contoh: `grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));`
- **Flexbox** untuk perataan (alignment) & distribusi.
- **Hindari tinggi tetap** (`height: 100px`); gunakan **`min-height`** agar konten
bisa memanjang dengan aman.
### 4. Mobile-First Approach
- Mulai dari layout **vertikal** yang solid untuk mobile.
- Lalu naikkan dengan **media query** (`@media (min-width: ...)`) atau breakpoint
Tailwind (`md:`, `lg:`) untuk memetakan ke **grid/flex horizontal** di layar besar.
### 5. Debug-Friendly & Minimal Bugs
- Tulis HTML yang **bersih, semantik, dan dangkal** (hindari *div soup*).
- Gunakan **CSS Custom Properties (Variabel) terpusat** untuk warna, spacing, dan
font agar perubahan cepat & mudah.
Contoh: `--primary: #ADFF2F; --bg: #0A0D14; --radius: 14px;`
- Beri **komentar jelas** di kode yang menjelaskan logika layout.
---
## Output
Output **tepat kode yang diperlukan**, pastikan:
- **siap dijalankan** (ready to run),
- **bebas bug** (bug-free),
- **mudah di-debug**.
---
## Catatan khusus project ini
- **Tema**: dark (default) + lime `#ADFF2F` di atas near-black `#0A0D14`; harus ada
toggle dark/light di setiap halaman.
- **Ikon**: **Bootstrap Icons** saja untuk UI (emoji hanya untuk teks WhatsApp).
- **Brand**: nama & logo diambil dari settings (`brandName` / `brandLogo`), jangan hardcode.
- **Bilingual (ID/EN)** via `t('key', 'fallback')` — identifier kode tetap Inggris.
- **Admin**: markup sidebar & `admin.css`/`dashboard.ejs` diperlakukan "frozen";
penyesuaian via overlay `admin-theme.css`.
- **Scroll**: area konten admin = `.mw` (scroll container). Jangan bikin konten
terpotong; `.page` dibiarkan tumbuh (`flex: 0 0 auto`) agar `.mw` bisa scroll.
- **Modal**: satu scroll saja (`.mb` yang scroll, `.mbody` `overflow: visible`) —
hindari *nested scroll* yang mengunci.
- **Responsif**: gunakan `100dvh` (bukan `100vh`) untuk area penuh-layar di mobile.
---
## Checklist cepat sebelum selesai
- [ ] Ukuran font/spacing pakai `clamp()` di elemen utama.
- [ ] Tidak ada teks/ikon yang meluber/overlap (cek `min-width:0`, `overflow-wrap`, `flex-wrap`).
- [ ] Grid kartu pakai `repeat(auto-fit, minmax(..., 1fr))`.
- [ ] Tidak ada `height` tetap pada blok konten (pakai `min-height`).
- [ ] Mobile-first + breakpoint untuk layar besar.
- [ ] HTML semantik & dangkal; warna/spacing via CSS variables.
- [ ] Dark/light berfungsi; ikon Bootstrap Icons; bilingual ID/EN.
- [ ] Scroll & modal normal di mobile **dan** desktop.
-567
View File
@@ -1,567 +0,0 @@
# Changelog Perubahan Kode — Billing RTRW
Project : `/opt/billing-rtrw`
Host : `deneva`
Service : `billing-rtrw` (systemd, port 3001)
Owner : `billing:billing`
> Project ini **belum memakai Git**, jadi file ini adalah satu-satunya catatan
> perubahan. Setiap perubahan kode selalu disertai backup file asli (lihat
> bagian *Rollback* pada tiap entri).
Format: `## [versi] — tanggal` (terbaru di atas).
---
## [v1.9.7] — 2026-09-28 16:55 — Full UI/UX Modernization & Database Optimization (Gentelella Tiles, uiverse.io Effects & Google Fonts)
### 1. Modernisasi Lengkap Seluruh Halaman Utama Admin (Gentelella Admin Style)
- **views/admin/dashboard.ejs**: Implementasi Tile KPI Stat horizontal (Total ONU, Online, Offline, Warning Redaman) dan Tile Keuangan (Pendapatan, Pelanggan Aktif, Tagihan Belum Bayar, Total Piutang) dengan indikator tren visual.
- **views/admin/customers.ejs**: Mengubah stat-grid filter pelanggan menjadi Gentelella Tile Cards interaktif (Total, Aktif, Disuspend, Nonaktif/Baru) dengan highlight filter aktif dan angka monospaced.
- **views/admin/billing.ejs**: Mengubah summary tagihan menjadi Gentelella Tile Cards (Total Tagihan, Sudah Bayar, Belum Bayar, Rasio Lunas).
- **views/admin/monitoring.ejs & views/admin/olts.ejs**: Menambahkan Google Fonts universal (Inter + JetBrains Mono).
### 2. Harmonisasi Desain Portal Pelanggan & Login (uiverse.io Style)
- **views/login.ejs**: Menyelaraskan seluruh palette warna dari hardcoded Indigo ke tema resmi Skyfiber (Teal #0d9488 & Cyan #06b6d4), menambahkan Google Fonts Plus Jakarta Sans, dan uiverse elevated glass card.
- **views/dashboard.ejs**: Menambahkan Google Fonts JetBrains Mono untuk pembacaan parameter teknis (IP, MAC, redaman dBm) dan efek micro-animation tombol .btn-uiverse-glow.
### 3. Optimasi Database SQLite
- Eksekusi PRAGMA integrity_check (hasil: ok).
- Eksekusi PRAGMA optimize untuk query planner performa tinggi.
### File disentuh
- public/css/admin.css
- views/admin/dashboard.ejs
- views/admin/customers.ejs
- views/admin/billing.ejs
- views/admin/monitoring.ejs
- views/admin/olts.ejs
- views/login.ejs
- views/dashboard.ejs
- views/isolated.ejs
- update-code.md
### Rollback
```bash
cp /opt/billing-rtrw/views/admin/customers.ejs.bak-gentelella /opt/billing-rtrw/views/admin/customers.ejs
cp /opt/billing-rtrw/views/admin/billing.ejs.bak-gentelella /opt/billing-rtrw/views/admin/billing.ejs
cp /opt/billing-rtrw/views/admin/dashboard.ejs.bak-gentelella /opt/billing-rtrw/views/admin/dashboard.ejs
cp /opt/billing-rtrw/views/login.ejs.bak-plusjakarta /opt/billing-rtrw/views/login.ejs
cp /opt/billing-rtrw/views/dashboard.ejs.bak-uiverse /opt/billing-rtrw/views/dashboard.ejs
sudo systemctl restart billing-rtrw.service
```
---
## [v1.9.6] — 2026-09-28 16:35 — Modernisasi UI/UX (Gentelella Admin Tiles, uiverse.io Effects & Google Fonts) + Matikan Service Biznet
### 1. Pematian Layanan di Server Biznet (Pencegahan Tabrakan Service)
- Service `billing-rtrw.service` di host `biznet` di-stop dan di-disable (`sudo systemctl stop & disable`).
- Process PM2 di `biznet` di-stop dan di-delete.
- Hasil: Menghilangkan konflik 409 pada Telegram Bot dan mencegah duplikasi trigger cron isolir/tagihan antar-server.
### 2. Adopsi Desain Gentelella Admin (views/admin/dashboard.ejs & public/css/admin.css)
- Mengganti grid metrik lama dengan modul **Gentelella Tile Stats (`.tile_count` & `.tile_stats_count`)**:
- Border samping berwarna aksen (`tile-primary`, `tile-success`, `tile-danger`, `tile-warning`).
- Label `count_top` uppercase dengan ikon jelas.
- Nilai metrik `count` berbobot 800 dengan font monospaced tabular.
- Sub-label `count_bottom` dengan indikator tren (`trend-up`, `trend-down`, `trend-warn`).
- Menambahkan class kontainer `.x_panel` & `.x_title` dan soft badges (`.badge-soft-success`, `.badge-soft-danger`, dll).
### 3. Adopsi Desain uiverse.io (views/dashboard.ejs)
- Menambahkan efek glassmorphism tingkat tinggi dengan border glow gradient dan shadow elevasi.
- Menambahkan animasi tombol mikro-interaktif (`.btn-uiverse-glow`) dengan hover lift-up (`translateY(-2px)`) dan klik aktif responsif.
### 4. Standarisasi Google Fonts Universal
- Mengintegrasikan **Inter** (antarmuka admin & tabel data), **Plus Jakarta Sans** (portal pelanggan & public landing), dan **JetBrains Mono** (data teknis IP, MAC, serial, dBm) melalui Google Fonts dengan koneksi teroptimasi (`preconnect` dan `display=swap`).
### File disentuh
- public/css/admin.css
- views/admin/dashboard.ejs
- views/dashboard.ejs
- update-code.md
### Rollback
```bash
cp /opt/billing-rtrw/public/css/admin.css.bak-gentelella /opt/billing-rtrw/public/css/admin.css
cp /opt/billing-rtrw/views/admin/dashboard.ejs.bak-gentelella /opt/billing-rtrw/views/admin/dashboard.ejs
cp /opt/billing-rtrw/views/dashboard.ejs.bak-uiverse /opt/billing-rtrw/views/dashboard.ejs
sudo systemctl restart billing-rtrw.service
```
---
## [v1.9.5] — 2026-09-28 16:20 — Fix Telegram Polling Flood, Unifikasi Tipografi (Plus Jakarta Sans) & Harmonize Login UI
### 1. Perbaikan Telegram Bot Polling Error Flood (services/telegramBot.js)
- Log server sebelumnya dibanjiri pesan error setiap 8 detik: error: Telegram Polling Error:.
- Penyebab: Terdeteksi error code 409 Conflict dari API Telegram karena bot token yang sama masih aktif dijalankan di server lama (biznet/migrasi).
- Solusi:
1. Menambahkan penanganan error detail (error?.response?.body?.description || error.message) agar terbaca jelas di logger.
2. Implementasi throttling logger (maksimal 1 log per 2 menit untuk error berulang yang sama).
3. Memisahkan level warning khusus bila terjadi 409 Conflict, sehingga journalctl bersih tanpa spam error.
### 2. Unifikasi Tipografi & Harmonisasi Desain (views/login.ejs & views/isolated.ejs)
- Menyelaraskan seluruh font pada portal publik & login pelanggan ke **Plus Jakarta Sans** (Google Fonts).
- Menyesuaikan palette warna primer & hover pada halaman login (views/login.ejs) menggunakan tema Teal/Cyan (#0d9488, #14b8a6, #2dd4bf) agar selaras dengan dashboard pelanggan.
- Menambahkan **Plus Jakarta Sans** pada halaman terisolir (views/isolated.ejs).
### File disentuh
- services/telegramBot.js
- views/login.ejs
- views/isolated.ejs
- update-code.md
### Rollback
```bash
cp /opt/billing-rtrw/services/telegramBot.js.bak-pollfix /opt/billing-rtrw/services/telegramBot.js
cp /opt/billing-rtrw/views/login.ejs.bak-plusjakarta /opt/billing-rtrw/views/login.ejs
cp /opt/billing-rtrw/views/isolated.ejs.bak-plusjakarta /opt/billing-rtrw/views/isolated.ejs
sudo systemctl restart billing-rtrw.service
```
---
## [v1.9.4] — 2026-09-27 18:35 — Fix Theme Toggle (Dark/Light Mode), FOUC Prevention & Light Theme Styling
### Masalah
- Tombol toggle tema (#theme-icon di header dashboard pelanggan) tidak berfungsi sama sekali saat diklik.
- Penyebab:
1. Logika Negasi Terbalik di toggleTheme(): Saat mode awal gelap, contains('light') bernilai false sehingga !false adalah true. Kode terus-menerus menyimpan dan menerapkan mode 'dark', tidak pernah beralih ke 'light'.
2. Class Collision pada <html>: <html class='dark'> memiliki class 'dark' permanen. applyTheme lama hanya menambahkan class 'light' tanpa menghapus class 'dark'. Tailwind CSS (darkMode: 'class') tetap mengaktifkan semua style dark:... selama class 'dark' ada di <html>.
3. FOUC saat reload: Tidak ada script sinkronisasi langsung di <head>, sehingga halaman sempat berkedip gelap sebelum DOMContentLoaded berjalan.
4. Missing data-theme: Tidak mengeset atribut data-theme='light'/'dark' yang diperlukan oleh theme.css.
### Solusi & Perbaikan
1. Perbaikan toggleTheme() & applyTheme(mode):
- Menghapus class 'dark' dan menambahkan 'light' saat beralih ke mode terang, dan sebaliknya saat mode gelap.
- Menetapkan atribut data-theme pada <html> secara konsisten.
- Sinkronisasi kedua key penyimpanan (skyfiber-customer-theme & app-theme).
- Mengubah ikon toggle secara dinamis: fa-sun text-amber-500 pada mode terang, fa-moon text-amber-400 pada mode gelap.
2. Inline Script di <head>:
- Menambahkan skrip sinkronisasi tema instan di dalam <head> sebelum browser merender body untuk mencegah kedip (FOUC).
3. Penyempurnaan Styling Mode Terang (Light Mode):
- Floating glass dock disesuaikan dengan latar frosted transparan putih dan sliding pill bergradasi lembut.
- Penyesuaian kontras input form, teks judul, card modal, dan gradien banner (tagihan lunas, tagihan aktif, info router).
### File disentuh
- views/dashboard.ejs
- update-code.md
### Rollback
```bash
cp /opt/billing-rtrw/views/dashboard.ejs.bak-themefix-20260927183449 /opt/billing-rtrw/views/dashboard.ejs
systemctl restart billing-rtrw.service
```
---
## [v1.9.3] — 2026-09-27 18:30 — Fix PWA Auto-Login, Persistent Remember Token & Session Lifetime
### Masalah
- Ketika PWA di-install di HP dan user login, saat aplikasi PWA di-kill atau ditutup, user terlempar ke halaman login lagi.
- Penyebab utama:
1. `start_url` di `manifest.webmanifest` diset ke `/customer/login?source=pwa`.
2. Rute `/customer/login` dan `/` memeriksa `req.session.customer`, sementara rute login hanya menyimpan `req.session.phone` dan `req.session.pppoe_username` (`customer` selalu undefined), sehingga selalu merender form login ulang.
3. Masa berlaku cookie sesi bawaan hanya 24 jam dan disimpan di RAM (MemoryStore) tanpa token persistensi native.
4. Service worker (`public/sw.js`) melakukan fallback navigasi ke `/customer/login` saat jaringan lambat/offline.
### Solusi & Perbaikan
1. **PWA Start URL:** Mengubah `start_url` di `/manifest.webmanifest` menjadi `/customer/dashboard`.
2. **Persistent Auto-Login Token (90 Hari):**
- Layanan baru `services/customerAuthTokenService.js` dengan tabel SQLite `customer_auth_tokens`.
- Mengeluarkan cookie `customer_remember` (90 hari, httpOnly, secure, lax) saat login langsung maupun verifikasi OTP.
- Middleware auto-login di `routes/customerPortal.js` yang secara otomatis memulihkan sesi pelanggan ketika aplikasi dibuka kembali.
3. **Penyelarasan Guard Sesi:**
- Rute `/customer/` dan `/customer/login` kini memeriksa `req.session.phone || req.session.customer` sehingga pengguna yang sudah login langsung dialihkan ke `/customer/dashboard`.
- Objek `req.session.customer` kini diisi lengkap pada direct login dan OTP.
4. **Masa Berlaku Cookie Sesi:** Ditingkatkan dari 24 jam menjadi 30 hari.
5. **Logout Bersih:** Rute `/customer/logout` (mendukung POST dan GET) mencabut token dari database dan menghapus cookie `customer_remember` serta `customer.sid`.
6. **Fallback Offline SW v6:** Navigasi offline dialihkan ke `/offline.html` dengan tombol "Coba Lagi", bukan ke halaman login.
7. **Penyimpanan Lokal Form:** Menyimpan ID pelanggan terakhir di `localStorage` pada `views/login.ejs`.
### File disentuh
- `services/customerAuthTokenService.js` (file baru)
- `app-customer.js`
- `routes/customerPortal.js`
- `public/sw.js`
- `views/login.ejs`
---
## [v1.9.2] — 2026-09-27 18:15 — Multi-Vendor WiFi SSID & Password Changer (Huawei / Nokia / FiberHome / ZTE)
### Perbaikan Ganti SSID & Password WiFi Multi-Vendor
- Mengatasi kegagalan ganti password TR-069 akibat penumpukan multiple parameter paths dalam satu payload SOAP atomik.
- Menghindari pengiriman `PreSharedKey.1.PreSharedKey` (format hex 64 digit) bersamaan dengan `KeyPassphrase`, yang sebelumnya memicu SOAP fault 9007 (Invalid Parameter Value) di modem.
- **Deteksi Vendor Otomatis (`HWTC`, `FiberHome`, `Nokia`, `ZTE`, `ZICG`, `CIOT`, `CDTC`):**
- Huawei / FiberHome / Nokia: memprioritaskan `PreSharedKey.1.KeyPassphrase` (menghindari fault 9007 karena penolakan `KeyPassphrase` top-level).
- ZTE / ZICG / CIOT: memprioritaskan `KeyPassphrase`.
- **SSID 2.4G & 5G:** Mengirim path tunggal yang tepat untuk 2.4 GHz (`WLANConfiguration.1.SSID`) dan 5 GHz (`WLANConfiguration.5.SSID`) jika perangkat dual-band, tanpa menimpa interface guest/sekunder.
- Fallback cerdas berbasis standar TR-098 per vendor.
### File disentuh
- `services/customerDeviceService.js`
### Rollback
```bash
cp /opt/billing-rtrw/services/customerDeviceService.js.bak-wififix /opt/billing-rtrw/services/customerDeviceService.js
sudo systemctl restart billing-rtrw
```
---
## [v1.9.1] — 2026-09-27 16:30 — 5-Tab Floating Dock, PPOB Soon, Profil Tiket & WhatsApp Anti-Ban
### 1. Floating Dock 5-Tab (Dashboard Pelanggan)
- Menghilangkan tombol tengah (+) Aksi Cepat dan popup modal sheet.
- Menjadikan navbar 5 tombol langsung: Beranda, Router, PPOB (badge gradient "Soon"), Tagihan (ping unread), dan Profil.
- Pusat Bantuan & Pembuatan Tiket Kendala diintegrasikan ke dalam Tab Profil (`tab-profile`) dengan tombol cepat ke form pelaporan dan WhatsApp CS.
- Ditambahkan tombol "Kembali ke Profil" di halaman tiket bantuan (`tab-bantuan`).
- Banner "Layanan PPOB Segera Hadir" pada tab PPOB (`tab-ppob`).
### 2. Sinkronisasi Upstream & Fix Anti-Ban WhatsApp
- Verifikasi repository upstream `alijayanet/billing-rtrw`: base commit `ffd3c62` sudah mencakup seluruh perbaikan OLT Hioso (gap-fill DDM, Buffer UTF-8 decoding, slow SNMP session) dan timezone midnight hourCycle h23.
- Menambahkan proteksi anti-ban WhatsApp pada alert monitoring (`services/whatsappBot.mjs`):
- Mematuhi toggle `genieacs_monitoring_enabled` (tidak kirim alert jika toggle OFF).
- Jeda acak (8-20 detik) antar penerima saat kirim broadcast alert ke admin/teknisi.
- Menjadwalkan pemeriksaan monitoring harian pada jam 07:00 pagi (`config/genieacs.js`) lewat `node-cron` untuk mencegah spam alert berulang.
### File disentuh
- `views/dashboard.ejs`
- `services/whatsappBot.mjs`
- `config/genieacs.js`
---
## [v1.9.0] — 2026-09-26 22:55 — Floating Dock Navbar + CDN Pin + SW v5
Perubahan UI besar (navbar) sekaligus perbaikan regresi PWA/offline.
### Navbar: floating glass dock
- Bottom nav lama (waterdrop, 6 tombol horizontal) diganti **floating glass
dock** sesuai referensi `adaptive_glassmorphism_floating_dock.html`.
- Struktur: 4 tombol langsung (**Beranda, Router, Tagihan, Profil**) + 1 tombol
tengah `+` yang membuka **quick-action sheet** berisi
**PPOB, Bantuan, Reconnect, Poll ONU**.
PPOB & Bantuan sengaja dipindah ke sheet agar dock tidak terlalu padat.
- ** keenam tab tetap bisa diakses**, `id="tab-*"` tidak berubah:
`tab-beranda`, `tab-router`, `tab-ppob`, `tab-tagihan`, `tab-bantuan`,
`tab-profile`.
- CSS baru: `.dock-glass`, `.dock-pill` (sliding pill), `.dock-item`,
`.dock-icon`, quick-sheet overlay. `safe-area-inset-bottom` untuk iPhone.
- JS baru: `updatePillPosition()`, `toggleQuickSheet()`, `openQuickSheet()`,
`closeQuickSheet()`, state `currentTab`, `isSheetOpen`.
`switchTab()` sekarang menandai `.dock-item` aktif dan memindahkan pill.
- Support keyboard: angka `1`–`6` pindah tab, `Escape` menutup sheet.
- Ikon memakai **Lucide** (`data-lucide`) + `lucide.createIcons()`.
- CSS `.waterdrop-active` yang tidak terpakai lagi dibuang.
### Perbaikan regresi offline/PWA
- **Semua CDN dashboard kini di-pin ke versi tetap** (sebelumnya `@latest` /
tanpa versi → bisa berubah atau break tanpa notice):
||Aset|URL|
|---|---|
|Lucide|`https://unpkg.com/lucide@1.48.0/dist/umd/lucide.min.js`|
|Chart.js|`https://cdn.jsdelivr.net/npm/chart.js@4.5.1/dist/chart.umd.min.js`|
|Tailwind|`https://cdn.tailwindcss.com/3.4.17`|
`https://unpkg.com/lucide@latest` dan `https://cdn.tailwindcss.com`
(tanpa versi) sama-sama **redirect 302** tiap load → di-pin ke URL final.
- **Bug:** service worker hanya cache-first `cdn.jsdelivr.net`, sehingga
Tailwind, FontAwesome, Google Fonts, dan Lucide **tidak tersedia saat
offline** — layout PWA hancur. Sekarang `sw.js` punya array `CDN_HOSTS`:
`cdn.jsdelivr.net`, `unpkg.com`, `cdnjs.cloudflare.com`,
`cdn.tailwindcss.com`, `fonts.googleapis.com`, `fonts.gstatic.com`.
- Cache bust: `CACHE_NAME` / `CDN_CACHE` `customer-pwa-v4` → **`-v5`**, plus
5 URL baru masuk `PRECACHE_URLS`.
### File disentuh
- `views/dashboard.ejs` (1481 baris)
- `public/sw.js` (173 baris)
### Verifikasi
- Render EJS OK, `div` seimbang `160/160`.
- Live `/customer/dashboard` HTTP 200, `div` seimbang `184/184`.
- Semua CDN membalas 200 tanpa redirect.
- `systemctl is-active billing-rtrw` = `active`, journal tanpa error.
### Rollback
```bash
cd /opt/billing-rtrw
cp views/dashboard.ejs.bak-twpin-225510 views/dashboard.ejs
cp public/sw.js.bak-twpin-225510 public/sw.js
sudo systemctl restart billing-rtrw
```
### Catatan
- Tampilan visual (sliding pill, animasi sheet, posisi icon) **belum dicek di
browser sungguhan** — verifikasi di atas semuanya server-side.
---
## [v1.8.2] — 2026-09-26 22:30 — Fix flash message toast
- `notif` dari server sebelumnya tidak tampil. Kini di-render sebagai variabel
`FLASH` (`{ text, type }`) lalu dimunculkan lewat `#toast-container`.
- Pemetaan tipe: `danger → error`, `warning → info`, selain itu `info`.
- Render diproteksi dari injeksi `</script>` via `.replace(/</g, '\u003c')`.
### File disentuh
- `views/dashboard.ejs`
### Rollback
```bash
cp views/dashboard.ejs.bak-flashfix-20260926221014 views/dashboard.ejs
sudo systemctl restart billing-rtrw
```
---
## [v1.8.1] — 2026-09-26 22:10 — Fix route `/customer/reboot` + ACS Connection Request 401
### 1. Route reboot
- `POST /customer/reboot` gagal auth karena hanya memakai `session.phone`
sebagai token device.
- Kini memakai `buildCustomerDeviceTokens()` sehingga mencoba seluruh kandidat
token (tag, username, serial, MAC, IP).
### 2. Connection Request 401 (fix kritis)
- ACS membalas **HTTP 401** saat perangkat menjalankan Connection Request karena
`ConnectionRequestUsername` / `ConnectionRequestPassword` belum diset.
- Diperbaiki dengan `provisionConnectionRequestAuth(deviceId)` di
`services/acsServerService.js`, dipanggil dari `handleInform()`.
- Alur: Basic challenge → 401 → **Digest auth → HTTP 200**.
- Default username `skyfiber`, password = `crypto.randomBytes(12)` hex per
perangkat, disimpan di database.
- Berlaku hanya untuk perangkat yang sedang online; 3 perangkat offline akan
ter-provisioning otomatis saat melakukan Inform.
### Hasil
- 102/105 perangkat sukses di-provisioning, 0 provisioning task gagal.
- Perubahan SSID/reboot kini berlaku ~10 detik (sebelumnya gagal).
- 3 perangkat offline akan ter-provisioning otomatis saat Inform berikutnya.
### File disentuh
- `routes/customerPortal.js` (3967 baris)
- `services/acsServerService.js` (1711 baris)
### Rollback
```bash
cd /opt/billing-rtrw
cp routes/customerPortal.js.bak-rebootfix-20260926221014 routes/customerPortal.js
cp services/acsServerService.js.bak-crfix-20260926221014 services/acsServerService.js
sudo systemctl restart billing-rtrw
```
---
## [v1.8.0] — 2026-09-26 18:04–21:07 — Redesign dashboard (APPS-style)
Redesain tampilan dashboard pelanggan dengan gaya APPS.
### Yang ditambahkan
- **Banner promo** dari database (`GET /customer/api/promo-slides`).
- **Chart pemakaian isolate-day** (maks. 3 bulan riwayat), menggantikan
polling traffic realtime yang sebelumnya membebani server.
Helper: `buildUsageSummary(profile, activePackage)`.
- **LibreSpeed** speedtest inline (sub-router `section-speedtest`).
- **Quick-action Reconnect** (`POST /customer/customer/reconnect`) dan
**Poll ONU** (`POST /customer/api/device/refresh`).
- Sub-tab router: **WiFi**, **Speedtest**, **Pemakaian**.
- Tampilan AXS/PPOB (`/customer/ppob`, `/customer/topup`).
- Link WhatsApp admin otomatis ke nomor pertama di `settings.whatsapp_admin_numbers`.
- Tema persisten via `localStorage`.
- Tombol toggle lihat IP (`isIpVisible`).
### Perbaikan
- Semua endpoint palsu `/api/customer/*` diarahkan ke route `/customer/*` asli.
- Badge **GPON**: merah `Bad GPON` bila ONU tidak terdeteksi **atau** PPPoE offline.
- Form WiFi/ONT otomatis disabled bila ONU absent, dengan pesan penjelasan.
- Variabel yang dihitung defensif di ketiga cabang render:
`hasUnpaid`, `latestUnpaid`, `latestInvoice`, `pppoeIP`, `pppoeUptime`,
`stMaxMbps`.
- Penghapusan modal Bootstrap yang rusak.
- Layout mobile dirapikan.
### File disentuh
- `views/dashboard.ejs`
### Rollback
```bash
cp views/dashboard.ejs.bak-prefn-20260926210734 views/dashboard.ejs
# atau ke markup/endpoint stabil terakhir:
cp views/dashboard.ejs.bak-pre-newdesign-20260926181219 views/dashboard.ejs
sudo systemctl restart billing-rtrw
```
---
## [v1.7.0] — 2026-09-26 12:55–14:24 — Self-registration, tabular layout, PWA
- **Self-registration** (`/customer/register`) + pairing tag perangkat
(`POST /customer/change-tag`).
- Tata letak tabel Payments/Fitur dikembalikan ke mode tabular
(`.bak-tabrapikan`, `.bak-tabcss`).
- Toggle lihat/sembunyikan password WiFi.
- Tampilan jatuh tempo (due) tagihan.
- Struktur `div` diperbaiki (tag tidak seimbang) — `.bak-divfix`.
- Navbar sticky di mobile — `.bak-sticky`.
- **Service worker** diperbaiki & di-cache ulang — `.bak-swfix`, `sw3`.
- **`rxPower` ACS** ditangani di `services/acsServerService.js`.
### File disentuh
- `views/dashboard.ejs`, `routes/customerPortal.js`, `public/sw.js`,
`services/acsServerService.js`
### Rollback
```bash
cd /opt/billing-rtrw
cp views/dashboard.ejs.bak-divfix-20260926142250 views/dashboard.ejs
cp public/sw.js.bak-swfix-20260926142327 public/sw.js
```
---
## Daftar endpoint yang dipakai dashboard
Semua Calls dari `views/dashboard.ejs` (sudah diverifikasi terhadap router):
| Aksi | Method | Endpoint |
|---|---|---|
| Promo slides | GET | `/customer/api/promo-slides` |
| Poll ONU / refresh | POST | `/customer/api/device/refresh` |
| Reconnect | POST | `/customer/customer/reconnect` |
| Ganti SSID | POST | `/customer/change-ssid` |
| Ganti password | POST | `/customer/change-password` |
| Reboot | POST | `/customer/reboot` |
| Buat tiket (+foto) | POST | `/customer/tickets/create` (multipart) |
| Bayar invoice | GET | `/customer/payment/create/:id` |
| PPOB | GET | `/customer/ppob` |
| Top up | GET | `/customer/topup` |
| Logout | GET | `/customer/logout` |
> Catatan: **tidak ada** route `/customer/poll-onu`. Tombol "Poll ONU"
> memanggil `POST /customer/api/device/refresh`.
---
## TODO / tindak lanjut
Detail lengkap ada di `/home/oxpoe/Documents/CATATAN-OPS-SKYFIBER.txt`
section 11 (mode `600`, jangan dibagikan).
### Keamanan — perlu tindakan
- [ ] Rotasi `telegram_bot_token` (via `@BotFather` → `/revoke`; token **baru**
jangan dikirim ke chat mana pun).
- [ ] Rotasi `admin_password`.
- [ ] Rotasi `admin_api_key`.
- [ ] Amankan backup `backups/settings_20260926_211113.json` (masih memuat
token lama) atau hapus setelah rotasi.
- [ ] Inisialisasi Git (`git init` + commit awal) supaya changelog berikutnya
punya riwayat & diff.
### Fungsional
- [ ] **Password WiFi pelanggan uji masih nilai sementara** dan harus
dikembalikan ke nilai asli (nilai uji tercatat di notes section 11).
- [ ] 3 perangkat belum punya CR credentials, akan provisioning otomatis saat
online:
- `001141-CL601-ZTEGCBBFB774`
- `B4E46B-SIGMA Z99LX XPON-ZICG27766207`
- `B4E46B-SIGMA Z99LX XPON-ZICG27766F6A`
- [ ] Alur pembayaran belum diuji end-to-end (butuh invoice uji).
- [ ] Cek visual floating dock di browser/HP sungguhan.
- [ ] Test PWA mode offline (SW v5) — belum pernah diuji.
- [ ] Keputusan: apakah FUP enforcement mengikuti isolate-day.
- [ ] Parity peta admin desktop vs mobile.
- [ ] Pemetaan kecepatan WiFi 2.4GHz vs 5GHz.
---
## Cara rollback umum
```bash
cd /opt/billing-rtrw
sudo systemctl stop billing-rtrw
# salin backup yang dikehendali ke file aslinya
sudo systemctl start billing-rtrw
sudo journalctl -u billing-rtrw -n 50 --no-pager
```
Backup tersedia (format `.bak-<label>-<timestamp>`):
| File | Backup |
|---|---|
| `views/dashboard.ejs` | `.bak-rxpower`, `.bak-selfapp-*`, `.bak-akun-*`, `.bak-due-*`, `.bak-tabcss-*`, `.bak-usage-ui-*`, `.bak-togglepass-*`, `.bak-tabrapikan-*`, `.bak-mobile-*`, `.bak-usageview-*`, `.bak-usagehelper-*`, `.bak-tailadmin-*`, `.bak-divfix-*`, `.bak-sticky-*`, `.bak-prefn-*`, `.bak-pre-newdesign-*`, `.bak-flashfix-*`, `.bak-dock-*`, `.bak-cdnpin-*`, `.bak-twpin-*` |
| `routes/customerPortal.js` | `.bak-selfapp-*`, `.bak-usageview-*`, `.bak-usagehelper-*`, `.bak-akun-*`, `.bak-rebootfix-*` |
| `services/acsServerService.js` | `.bak-rxpower-*`, `.bak-crfix-*` |
| `public/sw.js` | `.bak-selfapp-*`, `.bak-sw3-*`, `.bak-swfix-*`, `.bak-v4-*`, `.bak-twpin-*` |
---
## [v1.9.8] - 2026-09-28: Rombak Total UI/UX Multi-Portal (5-Color Modern Design System)
### Ringkasan Rilis
Rombak visual dan interaksi total pada seluruh portal aplikasi (Landing Page/Pelanggan, Customer PWA Dashboard, Admin Portal, Teknisi, Kolektor, dan Agen) menggunakan palette 5-warna konsisten, tipografi Google Fonts universal, elemen cyber-glassmorphism terinspirasi dari uiverse.io, dan panel KPI dense terinspirasi dari Gentelella.
### Design System & Palette
- **Dark 1 (Latar Utama)**: `#161A21`
- **Dark 2 (Gelap Sedang / Surfaces & Cards)**: `#1F2420`
- **Dark 3 (Abu-abu Gelap / Aksesori & Borders)**: `#2A313C`
- **Accent Lime (Aksen Hijau Neon)**: `#ADFF2F`
- **Light (Putih Terang / Teks)**: `#F2F5F1`
- **Typography**:
- `Plus Jakarta Sans`: Landing, PWA Pelanggan, Heading & Kartu
- `Inter`: Admin tabel, form controls, dense grid
- `JetBrains Mono`: Angka teknis (IP PPPoE, MAC, Serial, RX/TX Optical Power, Speed, Bandwidth)
### Modul yang Dirombak
1. **Landing Page & Customer Login (`views/login.ejs`)**:
- Cyber-glassmorphism card hero, paket internet (`#packages`), fitur (`#features`), layanan (`#services`), contact.
- PWA Install Modal dengan status online/offline otomatis.
2. **Halaman Isolir / Walled Garden (`views/isolated.ejs`)**:
- Palette `#161A21`, `#1F2420`, `#2A313C`, dan aksen neon `#ADFF2F`.
3. **Customer PWA Dashboard (`views/dashboard.ejs` & `views/partials/customer_bottom_nav.ejs`)**:
- Floating Glass Dock 5-Tab (`#1F2420` glass) dengan sliding active pill neon lime (`#ADFF2F`).
- Kartu status PPPoE/ONU dengan indikator neon pulse, telemetry optik, ganti SSID/password, speedtest gauge, dan Chart.js riwayat pemakaian.
4. **Admin Portal (`public/css/admin.css` & `views/admin/*`)**:
- Update CSS variables: `--bg: #161A21`, `--bg2: #1F2420`, `--bg3: #2A313C`, `--primary: #ADFF2F`, `--text: #F2F5F1`.
- Sidebar Gentelella modern dengan indikator aktif neon lime.
- KPI Tile Stats (`.tile_stats_count`) pada Dashboard, Customers, dan Billing.
- Perbaikan dark theme pada `views/admin/olts.ejs`, `monitoring.ejs`, `settings.ejs`, dan `login.ejs`.
5. **Technician Portal (`views/tech/*`)**:
- `views/tech/partials/header.ejs`, `views/tech/partials/bottom_nav.ejs`, `views/tech/dashboard.ejs`, `monitoring.ejs`, `attendance.ejs`, `map.ejs`, dan `login.ejs`.
6. **Collector & Agent Portals (`views/collector/*`, `views/agent/*`, `public/css/theme.css`)**:
- `public/css/theme.css` sebagai global theme provider untuk tema gelap 5-warna.
- Modernisasi login dan dashboard collector & agent.
### Verifikasi & Integritas
- 88 dari 88 template EJS aktif berhasil dikompilasi dengan `ejs.compile` (0 error).
- Seluruh endpoint HTTP menghasilkan kode `200 OK` / valid redirect.
- Database SQLite integrity check: `ok`.
- Service `billing-rtrw.service` berjalan stabil di Systemd VPS Deneva.
---
## [v1.9.9] - 2026-09-28: Penghapusan Total Komponen Light Mode (Konsistensi Eksklusif Halogen Kit)
### Ringkasan Perubahan
Menghapus seluruh komponen *light mode*, tombol pengalih tema (*theme toggle button / FAB*), script peralihan mode terang, dan aturan CSS override di semua portal aplikasi. Seluruh aplikasi kini konsisten 100% berjalan dalam **Mode Gelap Halogen Kit**:
1. **`public/js/theme.js`**: Menghapus fungsi injeksi tombol floating tema (`injectThemeFab`), fungsi `toggleAppTheme`, dan memaksa `data-theme=dark` di localStorage secara permanen.
2. **`public/css/theme.css`**: Menghapus seluruh blok aturan `html[data-theme=light]` dan styling tombol mengambang `.theme-fab`.
3. **Admin Sidebar (`views/admin/partials/sidebar.ejs`)`**: Menghapus tombol toggle tema pada bagian brand/header sidebar dan menyederhanakan inisialisasi dark mode.
4. **Customer PWA Dashboard (`views/dashboard.ejs`)`**: Menghapus tombol pengalih tema (`#theme-toggle-btn`) pada header topbar, menghapus fungsi `toggleTheme()` / `applyTheme()`, dan membersihkan seluruh override class CSS `html.light`.
5. **Verifikasi**: Seluruh 88 template EJS tervalidasi 0 error dan service berjalan stabil.
---
## [v1.9.9] - 2026-09-28: Penghapusan Total Komponen Light Mode (Konsistensi Eksklusif Halogen Kit)
### Ringkasan Perubahan
Menghapus seluruh komponen *light mode*, tombol pengalih tema (*theme toggle button / FAB*), script peralihan mode terang, dan aturan CSS override `html.light` di semua portal aplikasi. Seluruh aplikasi kini konsisten 100% berjalan dalam **Mode Gelap Halogen Kit**:
1. **`public/js/theme.js`**: Menghapus fungsi injeksi tombol floating tema (`injectThemeFab`), fungsi `toggleAppTheme`, dan memaksa `data-theme="dark"` di localStorage secara permanen.
2. **`public/css/theme.css`**: Menghapus seluruh blok aturan `html[data-theme="light"]` dan styling tombol mengambang `.theme-fab`.
3. **Admin Sidebar (`views/admin/partials/sidebar.ejs`)**: Menghapus tombol toggle tema pada bagian brand/header sidebar dan menyederhanakan inisialisasi dark mode.
4. **Customer PWA Dashboard (`views/dashboard.ejs`)**: Menghapus tombol pengalih tema (`#theme-toggle-btn`) pada header topbar, menghapus fungsi `toggleTheme()` / `applyTheme()`, dan membersihkan seluruh override class CSS `html.light`.
5. **Verifikasi**: Seluruh 88 template EJS tervalidasi 0 error dan service berjalan stabil.
+3 -1
View File
@@ -14,7 +14,7 @@ let TARGET = process.env.ISOLIR_REDIRECT_URL || '';
// Coba ambil target dari settings.json bila tersedia // Coba ambil target dari settings.json bila tersedia
try { try {
const { getSetting } = require('/opt/billing-rtrw/config/settingsManager'); const { getSetting } = require('./config/settingsManager');
const s = getSetting('isolir_portal_url', '') || ''; const s = getSetting('isolir_portal_url', '') || '';
if (s) TARGET = s; if (s) TARGET = s;
} catch (_) { /* abaikan */ } } catch (_) { /* abaikan */ }
@@ -37,6 +37,8 @@ const server = http.createServer((req, res) => {
); );
}); });
server.on('error', (e) => console.error('[isolir-redirect] listen error:', e.message));
server.listen(PORT, '0.0.0.0', () => { server.listen(PORT, '0.0.0.0', () => {
console.log(`[isolir-redirect] listening on 0.0.0.0:${PORT} -> ${TARGET}`); console.log(`[isolir-redirect] listening on 0.0.0.0:${PORT} -> ${TARGET}`);
}); });
+51 -2437
View File
File diff suppressed because it is too large. Load diff
+5 -6
View File
@@ -1,6 +1,6 @@
{ {
"name": "open-isp", "name": "open-isp",
"version": "0.5.0", "version": "0.5.1",
"description": "OPEN-ISP — sistem billing & manajemen ISP open-source (PWA pelanggan, multi-OLT, RADIUS, WhatsApp, QRIS)", "description": "OPEN-ISP — sistem billing & manajemen ISP open-source (PWA pelanggan, multi-OLT, RADIUS, WhatsApp, QRIS)",
"main": "app-customer.js", "main": "app-customer.js",
"scripts": { "scripts": {
@@ -10,9 +10,8 @@
"fix-permissions": "sudo chown -R $USER:$USER ." "fix-permissions": "sudo chown -R $USER:$USER ."
}, },
"dependencies": { "dependencies": {
"@whiskeysockets/baileys": "^7.0.0-rc11",
"@zxing/library": "^0.21.3", "@zxing/library": "^0.21.3",
"axios": "^1.6.7", "axios": "^1.20.0",
"better-sqlite3": "^11.3.0", "better-sqlite3": "^11.3.0",
"compression": "^1.8.2", "compression": "^1.8.2",
"dotenv": "^16.3.1", "dotenv": "^16.3.1",
@@ -22,10 +21,10 @@
"express-session": "^1.18.1", "express-session": "^1.18.1",
"jimp": "^1.6.1", "jimp": "^1.6.1",
"jsqr": "^1.4.0", "jsqr": "^1.4.0",
"multer": "^2.1.1", "multer": "^2.4.0",
"net-snmp": "^3.26.1", "net-snmp": "^3.26.1",
"node-cron": "^4.2.1", "node-cron": "^4.2.1",
"node-telegram-bot-api": "^0.66.0", "node-telegram-bot-api": "^2.1.0",
"pdfkit": "^0.19.1", "pdfkit": "^0.19.1",
"pino": "^9.6.0", "pino": "^9.6.0",
"qrcode": "^1.5.4", "qrcode": "^1.5.4",
@@ -34,7 +33,7 @@
"sharp": "^0.35.5", "sharp": "^0.35.5",
"ssh2": "^1.15.0", "ssh2": "^1.15.0",
"winston": "^3.8.2", "winston": "^3.8.2",
"xlsx": "^0.18.5" "xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz"
}, },
"devDependencies": { "devDependencies": {
"nodemon": "^3.1.14" "nodemon": "^3.1.14"
+2 -1
View File
@@ -1052,6 +1052,7 @@ router.get('/', async (req, res) => {
model: d._deviceId?._ProductClass || '-', model: d._deviceId?._ProductClass || '-',
rx_power: rxPower, rx_power: rxPower,
pppoe_ip: ip, pppoe_ip: ip,
ip: ip,
pppoe_user: pppoeUser, pppoe_user: pppoeUser,
customer_name: customerName, customer_name: customerName,
acs_server_id: server.id, acs_server_id: server.id,
@@ -1317,7 +1318,7 @@ router.put('/api/servers/legacy', requireAdmin, express.json(), async (req, res)
if (clearPassword) currentSettings.genieacs_password = ''; if (clearPassword) currentSettings.genieacs_password = '';
else if (String(passwordInput || '').trim()) currentSettings.genieacs_password = String(passwordInput || ''); else if (String(passwordInput || '').trim()) currentSettings.genieacs_password = String(passwordInput || '');
const settingsPath = path.join(__dirname, '../settings.json'); const settingsPath = require('../config/paths').settingsPath;
fs.writeFileSync(settingsPath, JSON.stringify(currentSettings, null, 2), 'utf8'); fs.writeFileSync(settingsPath, JSON.stringify(currentSettings, null, 2), 'utf8');
res.json({ success: true, message: 'Default ACS berhasil diperbarui.' }); res.json({ success: true, message: 'Default ACS berhasil diperbarui.' });
+25 -6
View File
@@ -384,7 +384,9 @@ function getUpdateInfo(repoRoot) {
const inside = runCmd('git', ['rev-parse', '--is-inside-work-tree'], repoRoot); const inside = runCmd('git', ['rev-parse', '--is-inside-work-tree'], repoRoot);
if (!inside.ok) { if (!inside.ok) {
info.error = 'Folder ini belum menjadi git repository.'; // Tanpa .git (Docker/container): update dilakukan via rebuild image (bukan git)
info.container = true;
info.containerMessage = 'Instalasi ini berjalan sebagai kontainer/gambar (tanpa .git). Pembaruan dilakukan dengan membangun ulang image dari repo terbaru.';
return info; return info;
} }
@@ -4554,6 +4556,10 @@ router.get('/update', requireAdminSession, requireSidebarMenuAccess('update'), r
router.post('/update/run', requireAdminSession, restrictToAdmin, (req, res) => { router.post('/update/run', requireAdminSession, restrictToAdmin, (req, res) => {
const repoRoot = path.resolve(__dirname, '..'); const repoRoot = path.resolve(__dirname, '..');
if (!runCmd('git', ['rev-parse', '--is-inside-work-tree'], repoRoot).ok) {
req.session._msg = { type: 'error', text: 'Instalasi kontainer (tanpa .git): pembaruan dilakukan dengan rebuild image, bukan lewat git.' };
return res.redirect('/admin/update');
}
const log = []; const log = [];
const pushCmd = (label, r) => { const pushCmd = (label, r) => {
log.push(`$ ${label}`.trim()); log.push(`$ ${label}`.trim());
@@ -4732,6 +4738,8 @@ router.post('/settings', requireAdminSession, express.urlencoded({ extended: tru
const newSettings = { ...req.body }; const newSettings = { ...req.body };
if (newSettings.whatsapp_enabled === 'true') newSettings.whatsapp_enabled = true; if (newSettings.whatsapp_enabled === 'true') newSettings.whatsapp_enabled = true;
else if (newSettings.whatsapp_enabled === 'false') newSettings.whatsapp_enabled = false; else if (newSettings.whatsapp_enabled === 'false') newSettings.whatsapp_enabled = false;
if (newSettings.voucher_enabled === 'true') newSettings.voucher_enabled = true;
else if (newSettings.voucher_enabled === 'false') newSettings.voucher_enabled = false;
if (newSettings.qris_static_enabled === 'true') newSettings.qris_static_enabled = true; if (newSettings.qris_static_enabled === 'true') newSettings.qris_static_enabled = true;
else if (newSettings.qris_static_enabled === 'false') newSettings.qris_static_enabled = false; else if (newSettings.qris_static_enabled === 'false') newSettings.qris_static_enabled = false;
@@ -4954,6 +4962,7 @@ router.post('/backup/create', requireAdminSession, express.urlencoded({ extended
}); });
router.post('/backup/restore', requireAdminSession, express.urlencoded({ extended: true }), (req, res) => { router.post('/backup/restore', requireAdminSession, express.urlencoded({ extended: true }), (req, res) => {
let restartNeeded = false;
try { try {
const { fileName, type } = req.body; const { fileName, type } = req.body;
let result; let result;
@@ -4968,7 +4977,12 @@ router.post('/backup/restore', requireAdminSession, express.urlencoded({ extende
} }
if (result.success) { if (result.success) {
req.session._msg = { type: 'success', text: `Restore berhasil: ${fileName}` }; if (type === 'database') {
restartNeeded = true;
req.session._msg = { type: 'success', text: `Restore database berhasil: ${fileName}. Aplikasi restart otomatis untuk menerapkan...` };
} else {
req.session._msg = { type: 'success', text: `Restore berhasil: ${fileName}` };
}
} else { } else {
req.session._msg = { type: 'error', text: `Gagal restore: ${result.error}` }; req.session._msg = { type: 'error', text: `Gagal restore: ${result.error}` };
} }
@@ -4976,9 +4990,12 @@ router.post('/backup/restore', requireAdminSession, express.urlencoded({ extende
req.session._msg = { type: 'error', text: `Gagal: ${e.message}` }; req.session._msg = { type: 'error', text: `Gagal: ${e.message}` };
} }
res.redirect('/admin/backup'); res.redirect('/admin/backup');
// DB di-swap: koneksi lama masih memakai file lama -> muat ulang proses (PM2/Docker restart otomatis)
if (restartNeeded) setTimeout(() => process.exit(0), 2000);
}); });
router.post('/backup/upload-restore', requireAdminSession, upload.single('backupFile'), (req, res) => { router.post('/backup/upload-restore', requireAdminSession, upload.single('backupFile'), (req, res) => {
let restartNeeded = false;
try { try {
const file = req.file; const file = req.file;
if (!file || !file.buffer || !file.originalname) { if (!file || !file.buffer || !file.originalname) {
@@ -5002,6 +5019,7 @@ router.post('/backup/upload-restore', requireAdminSession, upload.single('backup
const savePath = path.join(backupDir, savedFileName); const savePath = path.join(backupDir, savedFileName);
fs.writeFileSync(savePath, file.buffer); fs.writeFileSync(savePath, file.buffer);
result = backupSvc.restoreDatabase(savedFileName); result = backupSvc.restoreDatabase(savedFileName);
restartNeeded = true;
} else if (ext === '.json' || originalName.includes('settings')) { } else if (ext === '.json' || originalName.includes('settings')) {
savedFileName = `uploaded_settings_${timestamp}_${originalName}`; savedFileName = `uploaded_settings_${timestamp}_${originalName}`;
const savePath = path.join(backupDir, savedFileName); const savePath = path.join(backupDir, savedFileName);
@@ -5033,6 +5051,7 @@ router.post('/backup/upload-restore', requireAdminSession, upload.single('backup
req.session._msg = { type: 'error', text: 'Gagal upload & restore: ' + e.message }; req.session._msg = { type: 'error', text: 'Gagal upload & restore: ' + e.message };
} }
res.redirect('/admin/backup'); res.redirect('/admin/backup');
if (restartNeeded && result && result.success) setTimeout(() => process.exit(0), 2000);
}); });
router.post('/backup/delete', requireAdminSession, express.urlencoded({ extended: true }), (req, res) => { router.post('/backup/delete', requireAdminSession, express.urlencoded({ extended: true }), (req, res) => {
@@ -5221,7 +5240,7 @@ router.post('/api/genieacs/settings', requireAdmin, async (req, res) => {
// Save to file // Save to file
const fs = require('fs'); const fs = require('fs');
const path = require('path'); const path = require('path');
const settingsPath = path.join(__dirname, '../settings.json'); const settingsPath = require('../config/paths').settingsPath;
fs.writeFileSync(settingsPath, JSON.stringify(currentSettings, null, 2), 'utf8'); fs.writeFileSync(settingsPath, JSON.stringify(currentSettings, null, 2), 'utf8');
// Log audit // Log audit
@@ -6483,7 +6502,7 @@ router.get('/whatsapp/gowa/qr.png', requireAdminSession, async (req, res) => {
}); });
router.get('/whatsapp', requireAdminSession, requireSidebarMenuAccess('whatsapp'), async (req, res) => { router.get('/whatsapp', requireAdminSession, requireSidebarMenuAccess('whatsapp'), async (req, res) => {
const waGatewayType = getSetting('wa_gateway_type', 'baileys'); const waGatewayType = getSetting('wa_gateway_type', 'gowa');
const metaSettings = { const metaSettings = {
meta_phone_number_id: getSetting('meta_phone_number_id', ''), meta_phone_number_id: getSetting('meta_phone_number_id', ''),
meta_waba_id: getSetting('meta_waba_id', ''), meta_waba_id: getSetting('meta_waba_id', ''),
@@ -6598,7 +6617,7 @@ router.post('/whatsapp/test-fonnte', requireAdminSession, express.json(), async
// LIVE CHAT ROUTES // LIVE CHAT ROUTES
router.get('/whatsapp/live-chat', requireAdminSession, requireSidebarMenuAccess('whatsapp'), async (req, res) => { router.get('/whatsapp/live-chat', requireAdminSession, requireSidebarMenuAccess('whatsapp'), async (req, res) => {
const waGatewayType = getSetting('wa_gateway_type', 'baileys'); const waGatewayType = getSetting('wa_gateway_type', 'gowa');
const customers = customerSvc.getAllCustomers(); const customers = customerSvc.getAllCustomers();
res.render('admin/whatsapp_live_chat', { res.render('admin/whatsapp_live_chat', {
title: 'Live Chat WhatsApp', title: 'Live Chat WhatsApp',
@@ -7062,7 +7081,7 @@ router.post('/whatsapp/run-reminder-now', requireAdminSession, async (req, res)
router.get('/api/whatsapp/status', requireAdmin, async (req, res) => { router.get('/api/whatsapp/status', requireAdmin, async (req, res) => {
try { try {
const gatewayType = getSetting('wa_gateway_type', 'baileys'); const gatewayType = getSetting('wa_gateway_type', 'gowa');
if (gatewayType === 'fonnte') { if (gatewayType === 'fonnte') {
const token = getSetting('fonnte_token', ''); const token = getSetting('fonnte_token', '');
res.json({ res.json({
+2 -2
View File
@@ -2088,7 +2088,7 @@ router.get('/app/admin/olts', requireAdminApiAuth, (req, res) => {
// ─── ADMIN NATIVE: WHATSAPP STATUS ─────────────────────────────────────────── // ─── ADMIN NATIVE: WHATSAPP STATUS ───────────────────────────────────────────
router.get('/app/admin/whatsapp/status', requireAdminApiAuth, async (req, res) => { router.get('/app/admin/whatsapp/status', requireAdminApiAuth, async (req, res) => {
try { try {
const gwType = String(require('../config/settingsManager').getSetting('wa_gateway_type', 'baileys') || 'baileys').toLowerCase(); const gwType = String(require('../config/settingsManager').getSetting('wa_gateway_type', 'gowa') || 'baileys').toLowerCase();
let isConnected = false; let isConnected = false;
let waNumber = '-'; let waNumber = '-';
let waName = gwType.toUpperCase(); let waName = gwType.toUpperCase();
@@ -2190,7 +2190,7 @@ router.post('/app/admin/settings/update', requireAdminApiAuth, (req, res) => {
try { try {
const allowed = ['company_header', 'company_subheader', 'company_phone', 'company_address', 'timezone', 'qris_enabled', 'auto_isolate_enabled', 'webhook_secret']; const allowed = ['company_header', 'company_subheader', 'company_phone', 'company_address', 'timezone', 'qris_enabled', 'auto_isolate_enabled', 'webhook_secret'];
const updates = req.body || {}; const updates = req.body || {};
const settingsPath = require('path').join(__dirname, '../settings.json'); const settingsPath = require('../config/paths').settingsPath;
const current = JSON.parse(require('fs').readFileSync(settingsPath, 'utf8')); const current = JSON.parse(require('fs').readFileSync(settingsPath, 'utf8'));
let changed = 0; let changed = 0;
for (const key of allowed) { for (const key of allowed) {
+6
View File
@@ -2,6 +2,7 @@ const express = require('express');
const router = express.Router(); const router = express.Router();
const customerDevice = require('../services/customerDeviceService'); const customerDevice = require('../services/customerDeviceService');
const { getSettingsWithCache, getNowLocal, getCurrentTimeInfo, getNowLocalISO, formatDateLocal, formatTimeLocal } = require('../config/settingsManager'); const { getSettingsWithCache, getNowLocal, getCurrentTimeInfo, getNowLocalISO, formatDateLocal, formatTimeLocal } = require('../config/settingsManager');
function voucherFeatureOn() { try { const s = getSettingsWithCache() || {}; const v = s.voucher_enabled; return !(v === false || v === 'false' || v === '0' || v === 0); } catch (e) { return true; } }
const noticeSvc = require('../services/serviceNoticeService'); const noticeSvc = require('../services/serviceNoticeService');
const ssidChangeCooldown = new Map(); // key: customer id / loginId -> timestamp terakhir ubah SSID const ssidChangeCooldown = new Map(); // key: customer id / loginId -> timestamp terakhir ubah SSID
const registrationService = require('../services/registrationService'); const registrationService = require('../services/registrationService');
@@ -890,6 +891,7 @@ router.get('/check-billing', async (req, res) => {
let _voucherLastGoodProfiles = null; let _voucherLastGoodProfiles = null;
router.get('/voucher', async (req, res) => { router.get('/voucher', async (req, res) => {
if (!voucherFeatureOn()) return res.redirect('/customer/login?err=' + encodeURIComponent('Fitur voucher sedang dinonaktifkan'));
const settings = getSettingsWithCache(); const settings = getSettingsWithCache();
const error = String(req.query.err || '').trim() || null; const error = String(req.query.err || '').trim() || null;
const info = String(req.query.info || '').trim() || null; const info = String(req.query.info || '').trim() || null;
@@ -1149,6 +1151,7 @@ router.get('/voucher', async (req, res) => {
}); });
router.get('/voucher/qris/:orderId', async (req, res) => { router.get('/voucher/qris/:orderId', async (req, res) => {
if (!voucherFeatureOn()) return res.redirect('/customer/login?err=' + encodeURIComponent('Fitur voucher sedang dinonaktifkan'));
const settings = getSettingsWithCache(); const settings = getSettingsWithCache();
const orderId = Number(req.params.orderId || 0); const orderId = Number(req.params.orderId || 0);
const secret = settings.session_secret || 'rahasia-portal-pelanggan-default-ganti-ini'; const secret = settings.session_secret || 'rahasia-portal-pelanggan-default-ganti-ini';
@@ -1196,6 +1199,7 @@ router.get('/voucher/qris/:orderId', async (req, res) => {
}); });
// API endpoint untuk cek status voucher order (untuk auto-polling di halaman QRIS) // API endpoint untuk cek status voucher order (untuk auto-polling di halaman QRIS)
router.get('/voucher/status/:orderId', async (req, res) => { router.get('/voucher/status/:orderId', async (req, res) => {
if (!voucherFeatureOn()) return res.json({ success: false, status: 'disabled', message: 'Fitur voucher dinonaktifkan' });
const settings = getSettingsWithCache(); const settings = getSettingsWithCache();
const orderId = Number(req.params.orderId || 0); const orderId = Number(req.params.orderId || 0);
const secret = settings.session_secret || 'rahasia-portal-pelanggan-default-ganti-ini'; const secret = settings.session_secret || 'rahasia-portal-pelanggan-default-ganti-ini';
@@ -1224,6 +1228,7 @@ router.get('/voucher/status/:orderId', async (req, res) => {
router.post('/public/voucher/create-payment', async (req, res) => { router.post('/public/voucher/create-payment', async (req, res) => {
if (!voucherFeatureOn()) return res.status(403).json({ success: false, message: 'Fitur voucher dinonaktifkan' });
const settings = getSettingsWithCache(); const settings = getSettingsWithCache();
const buyerPhone = normalizeBuyerPhone(req.body.buyer_phone); const buyerPhone = normalizeBuyerPhone(req.body.buyer_phone);
@@ -3352,6 +3357,7 @@ router.post('/payment/proof/:invoiceId', uploadProof.single('proof'), async (req
}); });
router.post('/voucher/proof/:orderId', uploadProof.single('proof'), async (req, res) => { router.post('/voucher/proof/:orderId', uploadProof.single('proof'), async (req, res) => {
if (!voucherFeatureOn()) return res.redirect('/customer/login?err=' + encodeURIComponent('Fitur voucher sedang dinonaktifkan'));
const settings = getSettingsWithCache(); const settings = getSettingsWithCache();
const secret = settings.session_secret || 'rahasia-portal-pelanggan-default-ganti-ini'; const secret = settings.session_secret || 'rahasia-portal-pelanggan-default-ganti-ini';
const token = String(req.body && req.body.token ? req.body.token : '').trim(); const token = String(req.body && req.body.token ? req.body.token : '').trim();
+19 -10
View File
@@ -3,6 +3,7 @@
* Melakukan backup otomatis database dan settings * Melakukan backup otomatis database dan settings
*/ */
const fs = require('fs'); const fs = require('fs');
const Database = require('better-sqlite3');
const path = require('path'); const path = require('path');
const { logger } = require('../config/logger'); const { logger } = require('../config/logger');
const { getSetting, getCurrentDateInTimezone, getNowLocalISO } = require('../config/settingsManager'); const { getSetting, getCurrentDateInTimezone, getNowLocalISO } = require('../config/settingsManager');
@@ -10,7 +11,7 @@ const { getSetting, getCurrentDateInTimezone, getNowLocalISO } = require('../con
const projectRoot = path.join(__dirname, '..'); const projectRoot = path.join(__dirname, '..');
const backupDir = path.join(projectRoot, 'backups'); const backupDir = path.join(projectRoot, 'backups');
const dbPath = path.join(projectRoot, 'database', 'billing.db'); const dbPath = path.join(projectRoot, 'database', 'billing.db');
const settingsPath = path.join(projectRoot, 'settings.json'); const { settingsPath } = require('../config/paths');
// Pastikan direktori backup ada // Pastikan direktori backup ada
if (!fs.existsSync(backupDir)) { if (!fs.existsSync(backupDir)) {
@@ -41,8 +42,15 @@ function backupDatabase() {
const backupFileName = `billing_db_${timestamp}.db`; const backupFileName = `billing_db_${timestamp}.db`;
const backupFilePath = path.join(backupDir, backupFileName); const backupFilePath = path.join(backupDir, backupFileName);
// Copy database file // VACUUM INTO: snapshot konsisten & aman di mode WAL
fs.copyFileSync(dbPath, backupFilePath); // (menyalin file mentah bisa kehilangan transaksi terbaru yang masih di -wal)
if (fs.existsSync(backupFilePath)) fs.unlinkSync(backupFilePath);
const srcDb = new Database(dbPath, { readonly: true });
try {
srcDb.prepare('VACUUM INTO ?').run(backupFilePath);
} finally {
srcDb.close();
}
// Compress backup (optional - bisa ditambahkan nanti) // Compress backup (optional - bisa ditambahkan nanti)
const stats = fs.statSync(backupFilePath); const stats = fs.statSync(backupFilePath);
@@ -132,18 +140,19 @@ function restoreDatabase(backupFileName) {
logger.warn('[Backup] Failed to create pre-restore backup'); logger.warn('[Backup] Failed to create pre-restore backup');
} }
// Restore database // Tunda restore ke saat startup: menimpa DB di bawah koneksi hidup bisa merusak (WAL bercampur).
fs.copyFileSync(backupFilePath, dbPath); // Penanda dibaca config/database.js saat boot, sebelum koneksi dibuka.
const pendingPath = path.join(projectRoot, 'database', '.restore-pending');
fs.writeFileSync(pendingPath, backupFileName, 'utf8');
const size = fs.statSync(backupFilePath).size;
const stats = fs.statSync(dbPath); logger.info(`[Backup] Restore dijadwalkan (diterapkan saat restart): ${backupFileName}`);
const sizeKB = Math.round(stats.size / 1024);
logger.info(`[Backup] Database restored from: ${backupFileName} (${sizeKB} KB)`);
return { return {
success: true, success: true,
fileName: backupFileName, fileName: backupFileName,
size: stats.size, size,
pendingRestart: true,
timestamp: getNowLocalISO(), timestamp: getNowLocalISO(),
preRestoreBackup: preRestoreBackup.fileName preRestoreBackup: preRestoreBackup.fileName
}; };
+1 -1
View File
@@ -62,7 +62,7 @@ async function runAutoBillingReminder() {
const billingEnabled = getSetting('whatsapp_billing_to_customer_enabled', true); const billingEnabled = getSetting('whatsapp_billing_to_customer_enabled', true);
if (!enabled || !waEnabled || !billingEnabled) return { target: 0, sent: 0, failed: 0, skipped: 'disabled' }; if (!enabled || !waEnabled || !billingEnabled) return { target: 0, sent: 0, failed: 0, skipped: 'disabled' };
const gatewayType = getSetting('wa_gateway_type', 'baileys'); const gatewayType = getSetting('wa_gateway_type', 'gowa');
const waSvc = require('./whatsappService'); const waSvc = require('./whatsappService');
if (gatewayType === 'meta') { if (gatewayType === 'meta') {
+45 -29
View File
@@ -17,6 +17,7 @@ async function checkDependencies() {
mikrotik: [], mikrotik: [],
genieacs: { status: 'unknown', message: '' }, genieacs: { status: 'unknown', message: '' },
whatsapp: { status: 'unknown', message: '' }, whatsapp: { status: 'unknown', message: '' },
telegram: { status: 'unknown', message: '' },
paymentGateways: [], paymentGateways: [],
timestamp: new Date().toISOString() timestamp: new Date().toISOString()
}; };
@@ -104,39 +105,54 @@ async function checkDependencies() {
} }
} }
// 3. Check WhatsApp Gateway (using whatsappStatus from whatsappBot) // 3. Check WhatsApp Gateway (GoWA / Baileys sesuai setting wa_gateway_type)
try { try {
// Import whatsappStatus dynamically to get real-time status const { getSetting } = require('../config/settingsManager');
const whatsappBotModule = await import('./whatsappBot.mjs'); const gw = String(getSetting('wa_gateway_type', 'gowa') || 'baileys').toLowerCase();
const waStatus = whatsappBotModule.whatsappStatus; if (gw === 'gowa') {
const base = String(getSetting('gowa_base_url', 'http://127.0.0.1:3000') || '').replace(/\/+$/, '');
// Check connection status const u = getSetting('gowa_username', '');
// Possible values: 'connecting', 'qr', 'open', 'loggedOut', 'close' const p = getSetting('gowa_password', '');
const isOnline = waStatus.connection === 'open'; const cfg = { timeout: 6000, validateStatus: (s) => s < 500 };
const isQR = waStatus.connection === 'qr'; if (u && p) cfg.auth = { username: u, password: p };
const isLoggedOut = waStatus.connection === 'loggedOut'; const resp = await axios.get(`${base}/app/status`, cfg);
const res = (resp.data && resp.data.results) || {};
let message = 'WhatsApp is disconnected'; const isOnline = !!res.is_connected;
if (isOnline) { results.whatsapp = {
const phone = waStatus.user?.id ? String(waStatus.user.id).split(':')[0] : 'Unknown'; status: isOnline ? 'online' : 'offline',
message = `WhatsApp connected (${phone})`; message: isOnline ? `GoWA connected (${res.jid || res.device_id || '-'})` : 'GoWA tidak terhubung'
} else if (isQR) { };
message = 'WhatsApp waiting for QR scan';
} else if (isLoggedOut) {
message = 'WhatsApp logged out - scan QR again';
} else { } else {
message = `WhatsApp ${waStatus.connection || 'disconnected'}`; const whatsappBotModule = await import('./whatsappBot.mjs');
const waStatus = whatsappBotModule.whatsappStatus;
const isOnline = waStatus && waStatus.connection === 'open';
const isQR = waStatus && waStatus.connection === 'qr';
const isLoggedOut = waStatus && waStatus.connection === 'loggedOut';
let message = 'WhatsApp is disconnected';
if (isOnline) { const phone = waStatus.user && waStatus.user.id ? String(waStatus.user.id).split(':')[0] : 'Unknown'; message = `WhatsApp connected (${phone})`; }
else if (isQR) message = 'WhatsApp waiting for QR scan';
else if (isLoggedOut) message = 'WhatsApp logged out - scan QR again';
else message = `WhatsApp ${(waStatus && waStatus.connection) || 'disconnected'}`;
results.whatsapp = { status: isOnline ? 'online' : 'offline', message };
} }
results.whatsapp = {
status: isOnline ? 'online' : 'offline',
message: message
};
} catch (err) { } catch (err) {
results.whatsapp = { results.whatsapp = { status: 'offline', message: `WhatsApp check error: ${err.message}` };
status: 'offline', }
message: `WhatsApp check error: ${err.message}`
}; // 4. Check Telegram Bot
try {
const { getSetting } = require('../config/settingsManager');
const enabled = !!getSetting('telegram_enabled', false);
const token = String(getSetting('telegram_bot_token', '') || '').trim();
if (!enabled || !token) {
results.telegram = { status: 'offline', message: !enabled ? 'Telegram dinonaktifkan' : 'Token belum diisi' };
} else {
const resp = await axios.get(`https://api.telegram.org/bot${token}/getMe`, { timeout: 6000, validateStatus: (s) => s < 500 });
if (resp.data && resp.data.ok) results.telegram = { status: 'online', message: 'Terhubung sebagai @' + ((resp.data.result && resp.data.result.username) || '-') };
else results.telegram = { status: 'offline', message: (resp.data && resp.data.description) || 'getMe gagal' };
}
} catch (err) {
results.telegram = { status: 'offline', message: `Telegram check error: ${err.message}` };
} }
return results; return results;
+1 -1
View File
@@ -111,7 +111,7 @@ async function sendFonnteMessage(toPhone, messageText, options = {}) {
* Test Koneksi / Kirim Pesan Uji Coba Fonnte * Test Koneksi / Kirim Pesan Uji Coba Fonnte
*/ */
async function testFonnteConnection(targetPhone, customMessage) { async function testFonnteConnection(targetPhone, customMessage) {
const msg = customMessage || '🔔 *Tes Koneksi Fonnte WhatsApp Gateway Berhasil!*\n\nSistem Billing RTRW Net siap mengirimkan notifikasi otomatis melalui gateway Fonnte.'; const msg = customMessage || '🔔 *Tes Koneksi Fonnte WhatsApp Gateway Berhasil!*\n\nOPEN-ISP siap mengirimkan notifikasi otomatis melalui gateway Fonnte.';
return await sendFonnteMessage(targetPhone, msg); return await sendFonnteMessage(targetPhone, msg);
} }
+4 -4
View File
@@ -1555,13 +1555,13 @@ async function generateIsolirPortalScript() {
let port = 443; let port = 443;
let isHttps = true; let isHttps = true;
try { try {
const u = new URL(normalized || 'http://127.0.0.1:4555'); const u = new URL(normalized || 'http://127.0.0.1:3001');
hostname = u.hostname; hostname = u.hostname;
port = u.port ? parseInt(u.port, 10) : (u.protocol === 'https:' ? 443 : 80); port = u.port ? parseInt(u.port, 10) : (u.protocol === 'https:' ? 443 : 80);
isHttps = u.protocol === 'https:'; isHttps = u.protocol === 'https:';
} catch { } catch {
hostname = 'GANTI-host-portal-billing'; hostname = 'GANTI-host-portal-billing';
port = 4555; port = 3001;
isHttps = false; isHttps = false;
} }
@@ -1724,7 +1724,7 @@ async function setupIsolirFirewall(routerId = null) {
try { try {
const raw = String(settings.app_url || '').trim(); const raw = String(settings.app_url || '').trim();
const normalized = raw && /^https?:\/\//i.test(raw) ? raw : (raw ? `https://${raw}` : ''); const normalized = raw && /^https?:\/\//i.test(raw) ? raw : (raw ? `https://${raw}` : '');
const u = new URL(normalized || 'http://127.0.0.1:4555'); const u = new URL(normalized || 'http://127.0.0.1:3001');
const isHttps = u.protocol === 'https:'; const isHttps = u.protocol === 'https:';
const port = u.port ? parseInt(u.port, 10) : (isHttps ? 443 : 80); const port = u.port ? parseInt(u.port, 10) : (isHttps ? 443 : 80);
httpNatPort = isHttps ? 80 : port; httpNatPort = isHttps ? 80 : port;
@@ -2042,7 +2042,7 @@ async function getBackup(routerId = null) {
rscLines.push(`# Router : ${routerName} (${routerHost})`); rscLines.push(`# Router : ${routerName} (${routerHost})`);
rscLines.push(`# Tanggal : ${nowStr}`); rscLines.push(`# Tanggal : ${nowStr}`);
rscLines.push(`# Format : RouterOS Script (.rsc) ROS7 Ready`); rscLines.push(`# Format : RouterOS Script (.rsc) ROS7 Ready`);
rscLines.push(`# Generator: Billing RTRW System`); rscLines.push(`# Generator: OPEN-ISP Billing System`);
rscLines.push(`# ===================================================\n`); rscLines.push(`# ===================================================\n`);
// 1. IP Pools // 1. IP Pools
+22 -2
View File
@@ -1,4 +1,4 @@
const TelegramBot = require('node-telegram-bot-api'); const { CompatBot } = require('./telegramCompat');
const { getSetting, getNowLocal } = require('../config/settingsManager'); const { getSetting, getNowLocal } = require('../config/settingsManager');
const { logger } = require('../config/logger'); const { logger } = require('../config/logger');
const customerSvc = require('./customerService'); const customerSvc = require('./customerService');
@@ -38,7 +38,7 @@ function initTelegram() {
return; return;
} }
bot = new TelegramBot(token, { polling: true }); bot = new CompatBot(token);
// ── Wrapper aman: bila Telegram gagal parse Markdown (mis. underscore pada // ── Wrapper aman: bila Telegram gagal parse Markdown (mis. underscore pada
// username pelanggan), kirim ulang otomatis TANPA parse_mode. // username pelanggan), kirim ulang otomatis TANPA parse_mode.
@@ -823,6 +823,26 @@ function initTelegram() {
catch (e) { bot.sendMessage(msg.chat.id, 'Gagal: ' + e.message); } catch (e) { bot.sendMessage(msg.chat.id, 'Gagal: ' + e.message); }
}); });
bot.onText(/\/cekterhubung\s+(\S+)/i, async (msg, match) => {
if (!isAdmin(msg)) return;
const chatId = msg.chat.id;
const tag = match[1];
try {
let dev = null;
try { dev = await customerDevice.findDeviceByTag(tag); } catch (e) {}
if (!dev) { try { dev = await customerDevice.findDeviceWithTagVariants(tag); } catch (e) {} }
if (!dev) return bot.sendMessage(chatId, '❌ Perangkat/tag "' + tag + '" tidak ditemukan.');
const m = customerDevice.mapDeviceData(dev, tag, false) || {};
const st = String(m.status || '').toLowerCase() === 'online' ? '🟢 Online' : '🔴 Offline';
let t = '📶 *CEK TERHUBUNG*\n\n';
t += '🏷️ Tag: ' + tag + '\n';
t += '📡 Status: ' + st + '\n';
if (m.lastInform) t += '🕒 Last Inform: ' + m.lastInform + '\n';
if (m.ssid && m.ssid !== '-') t += '📶 SSID: ' + m.ssid + '\n';
bot.sendMessage(chatId, t, { parse_mode: 'Markdown' });
} catch (e) { bot.sendMessage(chatId, 'Gagal: ' + e.message); }
});
bot.onText(/\/ringkasan/i, async (msg) => { bot.onText(/\/ringkasan/i, async (msg) => {
if (!isAdmin(msg)) return; if (!isAdmin(msg)) return;
const chatId = msg.chat.id; const chatId = msg.chat.id;
+84
View File
@@ -0,0 +1,84 @@
/**
* Adapter node-telegram-bot-api v1 -> v2.
* v2 (>=2.0) tidak kompatibel dengan v1 (bot.onText, bot.sendMessage(chatId, text, opts), ...).
* Adapter ini menyediakan permukaan API v1 yang dipakai services/telegramBot.js,
* sehingga file itu tak perlu dirombak. Terisolasi di sini agar mudah dirawat.
*/
const { Bot } = require('node-telegram-bot-api');
class CompatBot {
constructor(token) {
this.token = token;
this._bot = new Bot(token);
this._msgHandlers = [];
this._textHandlers = [];
this._cbHandlers = [];
this._errHandlers = [];
// v2 = middleware (bukan EventEmitter). Satu middleware meneruskan ke semua handler v1.
this._bot.on('message', (ctx) => this._dispatchMessage(ctx));
this._bot.on('callback_query', (ctx) => this._dispatchCallback(ctx));
// v1 `{ polling: true }` autostart; ditunda agar handler selesai didaftarkan lebih dulu.
setImmediate(() => this.startPolling());
}
_run(handler, args) {
try {
const r = handler(...args);
if (r && typeof r.catch === 'function') r.catch((e) => this._emitError(e));
} catch (e) {
this._emitError(e);
}
}
_dispatchMessage(ctx) {
const msg = ctx.message;
if (!msg) return;
for (const h of this._msgHandlers) this._run(h, [msg]);
if (typeof msg.text === 'string') {
for (const { re, h } of this._textHandlers) {
const m = msg.text.match(re);
if (m) this._run(h, [msg, m]);
}
}
}
_dispatchCallback(ctx) {
const q = ctx.callbackQuery;
if (!q) return;
for (const h of this._cbHandlers) this._run(h, [q]);
}
_emitError(e) {
const err = e instanceof Error ? e : new Error(String(e));
if (this._errHandlers.length) {
for (const h of this._errHandlers) { try { h(err); } catch (_) {} }
} else {
console.error('Telegram error:', err.message);
}
}
on(kind, handler) {
if (kind === 'message') this._msgHandlers.push(handler);
else if (kind === 'callback_query') this._cbHandlers.push(handler);
else if (kind === 'polling_error' || kind === 'error') {
this._errHandlers.push(handler);
this._bot.catch((err) => this._emitError(err));
}
return this;
}
onText(re, handler) { this._textHandlers.push({ re, h: handler }); return this; }
sendMessage(chatId, text, opts) { return this._bot.api.sendMessage({ chat_id: chatId, text, ...(opts || {}) }); }
editMessageText(text, opts) { return this._bot.api.editMessageText({ text, ...(opts || {}) }); }
answerCallbackQuery(id, opts) { return this._bot.api.answerCallbackQuery({ callback_query_id: id, ...(opts || {}) }); }
getMe() { return this._bot.api.getMe(); }
deleteWebHook() { return this._bot.api.deleteWebhook({}); }
stopPolling() { try { this._bot.stop(); } catch (e) {} }
startPolling() { this._bot.startPolling().catch((e) => this._emitError(e)); }
isPolling() { return this._bot.isRunning(); }
}
module.exports = { CompatBot };
+1 -1
View File
@@ -8,7 +8,7 @@ const path = require('path');
const { getSetting } = require('../config/settingsManager'); const { getSetting } = require('../config/settingsManager');
function gateway() { function gateway() {
return String(getSetting('wa_gateway_type', 'baileys') || 'baileys').toLowerCase(); return String(getSetting('wa_gateway_type', 'gowa') || 'baileys').toLowerCase();
} }
function spx(t) { function spx(t) {
+8 -8
View File
@@ -933,7 +933,7 @@ export const whatsappStatus = {
_connection: 'connecting', _connection: 'connecting',
get connection() { get connection() {
try { try {
const gw = getSetting('wa_gateway_type', 'baileys'); const gw = getSetting('wa_gateway_type', 'gowa');
if (gw === 'fonnte') { if (gw === 'fonnte') {
const token = getSetting('fonnte_token', ''); const token = getSetting('fonnte_token', '');
return token ? 'open' : 'close'; return token ? 'open' : 'close';
@@ -954,7 +954,7 @@ export const whatsappStatus = {
_user: null, _user: null,
get user() { get user() {
try { try {
const gw = getSetting('wa_gateway_type', 'baileys'); const gw = getSetting('wa_gateway_type', 'gowa');
if (gw === 'fonnte') { if (gw === 'fonnte') {
return { id: 'fonnte_gateway', name: 'Fonnte Gateway' }; return { id: 'fonnte_gateway', name: 'Fonnte Gateway' };
} }
@@ -973,7 +973,7 @@ export const whatsappStatus = {
_phoneNumber: null, _phoneNumber: null,
get phoneNumber() { get phoneNumber() {
try { try {
const gw = getSetting('wa_gateway_type', 'baileys'); const gw = getSetting('wa_gateway_type', 'gowa');
if (gw === 'fonnte') return 'Fonnte Gateway'; if (gw === 'fonnte') return 'Fonnte Gateway';
if (gw === 'meta') return getSetting('meta_phone_number_id', 'Meta API'); if (gw === 'meta') return getSetting('meta_phone_number_id', 'Meta API');
if (gw === 'gowa') return getSetting('gowa_device_id', '') || 'GOWA Gateway'; if (gw === 'gowa') return getSetting('gowa_device_id', '') || 'GOWA Gateway';
@@ -986,7 +986,7 @@ export const whatsappStatus = {
_pushName: null, _pushName: null,
get pushName() { get pushName() {
try { try {
const gw = getSetting('wa_gateway_type', 'baileys'); const gw = getSetting('wa_gateway_type', 'gowa');
if (gw === 'fonnte') return 'Fonnte Gateway'; if (gw === 'fonnte') return 'Fonnte Gateway';
if (gw === 'meta') return 'Meta Cloud API'; if (gw === 'meta') return 'Meta Cloud API';
if (gw === 'gowa') return 'GOWA Gateway'; if (gw === 'gowa') return 'GOWA Gateway';
@@ -1032,7 +1032,7 @@ function loadWhatsappAdminSendList() {
* @param {string} priority - Priority level: 'high', 'medium', 'low' * @param {string} priority - Priority level: 'high', 'medium', 'low'
*/ */
export async function sendMonitoringAlert(message, priority = 'medium') { export async function sendMonitoringAlert(message, priority = 'medium') {
const gatewayType = getSetting('wa_gateway_type', 'baileys'); const gatewayType = getSetting('wa_gateway_type', 'gowa');
if (gatewayType === 'baileys' && (!currentSock || whatsappStatus._connection !== 'open')) { if (gatewayType === 'baileys' && (!currentSock || whatsappStatus._connection !== 'open')) {
logger.warn('[WhatsApp] Bot belum siap (koneksi belum terbuka), tidak dapat mengirim alert monitoring'); logger.warn('[WhatsApp] Bot belum siap (koneksi belum terbuka), tidak dapat mengirim alert monitoring');
return { success: false, message: 'Bot belum siap' }; return { success: false, message: 'Bot belum siap' };
@@ -1139,7 +1139,7 @@ export async function simulateHumanTyping(sock, jid, text = '') {
} }
export async function sendWA(to, text, options = {}) { export async function sendWA(to, text, options = {}) {
const gatewayType = getSetting('wa_gateway_type', 'baileys'); const gatewayType = getSetting('wa_gateway_type', 'gowa');
let finalText = text; let finalText = text;
if (options.spintax !== false && typeof text === 'string' && text.includes('{') && text.includes('}')) { if (options.spintax !== false && typeof text === 'string' && text.includes('{') && text.includes('}')) {
@@ -1219,7 +1219,7 @@ export async function sendWA(to, text, options = {}) {
} }
export async function sendWAImage(to, imageBuffer, caption = '', options = {}) { export async function sendWAImage(to, imageBuffer, caption = '', options = {}) {
const gatewayType = getSetting('wa_gateway_type', 'baileys'); const gatewayType = getSetting('wa_gateway_type', 'gowa');
let finalCaption = caption; let finalCaption = caption;
if (options.spintax !== false && typeof caption === 'string' && caption.includes('{') && caption.includes('}')) { if (options.spintax !== false && typeof caption === 'string' && caption.includes('{') && caption.includes('}')) {
@@ -1286,7 +1286,7 @@ export async function sendWAImage(to, imageBuffer, caption = '', options = {}) {
} }
export async function sendWADocument(to, documentBuffer, filename = 'Invoice.pdf', caption = '', mimetype = 'application/pdf', options = {}) { export async function sendWADocument(to, documentBuffer, filename = 'Invoice.pdf', caption = '', mimetype = 'application/pdf', options = {}) {
const gatewayType = getSetting('wa_gateway_type', 'baileys'); const gatewayType = getSetting('wa_gateway_type', 'gowa');
let finalCaption = caption; let finalCaption = caption;
if (options.spintax !== false && typeof caption === 'string' && caption.includes('{') && caption.includes('}')) { if (options.spintax !== false && typeof caption === 'string' && caption.includes('{') && caption.includes('}')) {
+1 -1
View File
@@ -140,7 +140,7 @@ function formatUnpaidInvoicesSummary(unpaidInvoices) {
* @param {object} options Opsi tambahan: { templateName, parameters, langCode, url, filename } * @param {object} options Opsi tambahan: { templateName, parameters, langCode, url, filename }
*/ */
async function sendWhatsAppMessage(toPhone, messageText, options = {}) { async function sendWhatsAppMessage(toPhone, messageText, options = {}) {
const gatewayType = getSetting('wa_gateway_type', 'baileys'); // 'baileys', 'fonnte', or 'meta' const gatewayType = getSetting('wa_gateway_type', 'gowa'); // 'baileys', 'fonnte', or 'meta'
if (gatewayType === 'fonnte') { if (gatewayType === 'fonnte') {
// Mode FONNTE API GATEWAY (Cloud / Self-Hosted) // Mode FONNTE API GATEWAY (Cloud / Self-Hosted)
+1 -1
View File
@@ -1 +1 @@
B 4.8.1-R18 0.5.1
+1 -1
View File
@@ -174,7 +174,7 @@
<span class="text-muted" style="font-size:12px;">-</span> <span class="text-muted" style="font-size:12px;">-</span>
<% } %> <% } %>
</td> </td>
<td><code><%= d.ip %></code></td> <td><code><%= d.ip || d.pppoe_ip || '-' %></code></td>
<td><%= d.rx_power %> dBm</td> <td><%= d.rx_power %> dBm</td>
<td><%= d.pppoe_user %></td> <td><%= d.pppoe_user %></td>
<td><span class="acs-server-tag"><%= d.acs_server_name %></span></td> <td><span class="acs-server-tag"><%= d.acs_server_name %></span></td>
+1 -1
View File
@@ -247,7 +247,7 @@
<td class="text-muted"><%= i+1 %></td> <td class="text-muted"><%= i+1 %></td>
<td class="fw6"> <td class="fw6">
<a href="javascript:void(0)" onclick="viewCustDetail(<%= i %>)" style="color:var(--primary);text-decoration:none;font-weight:700;display:inline-flex;align-items:center;gap:5px;cursor:pointer;" title="<%= t('admin.cust.tt_detail','Klik untuk lihat detail pelanggan & MikroTik') %>"> <a href="javascript:void(0)" onclick="viewCustDetail(<%= i %>)" style="color:var(--primary);text-decoration:none;font-weight:700;display:inline-flex;align-items:center;gap:5px;cursor:pointer;" title="<%= t('admin.cust.tt_detail','Klik untuk lihat detail pelanggan & MikroTik') %>">
<%= c.name %> <%= c.name %><% var _miss=[]; if(!c.phone)_miss.push('HP'); if(!c.lat||!c.lng)_miss.push('koordinat'); if(!c.genieacs_tag)_miss.push('ACS'); %><% if(_miss.length===0){ %><i class="bi bi-check-circle-fill" style="font-size:12px;color:#22c55e;vertical-align:middle;" title="<%= t('admin.customers.data_ok','Data lengkap') %>"></i><% } else { %><i class="bi bi-x-circle-fill" style="font-size:12px;color:#ef4444;vertical-align:middle;" title="<%= t('admin.customers.data_incomplete','Belum lengkap') %>: <%- _miss.join(', ') %>"></i><% } %>
<i class="bi bi-info-circle-fill" style="font-size:12px;opacity:0.75;"></i> <i class="bi bi-info-circle-fill" style="font-size:12px;opacity:0.75;"></i>
</a> </a>
<% if (c.customer_code) { %><div style="font-size:11px;font-weight:700;color:var(--muted);margin-top:2px;display:flex;align-items:center;gap:4px;" title="ID Pelanggan"><i class="bi bi-person-badge"></i> <%= c.customer_code %></div><% } %> <% if (c.customer_code) { %><div style="font-size:11px;font-weight:700;color:var(--muted);margin-top:2px;display:flex;align-items:center;gap:4px;" title="ID Pelanggan"><i class="bi bi-person-badge"></i> <%= c.customer_code %></div><% } %>
+9 -1
View File
@@ -568,13 +568,21 @@
</td> </td>
</tr> </tr>
<tr> <tr>
<td>WhatsApp Bot</td> <td>WhatsApp Bot<div class="sc-label" style="font-size:11px"><%= dependencies.whatsapp.message %></div></td>
<td class="text-right"> <td class="text-right">
<span class="badge <%= dependencies.whatsapp.status === 'online' ? 'bs' : 'bd' %>"> <span class="badge <%= dependencies.whatsapp.status === 'online' ? 'bs' : 'bd' %>">
<%= dependencies.whatsapp.status.toUpperCase() %> <%= dependencies.whatsapp.status.toUpperCase() %>
</span> </span>
</td> </td>
</tr> </tr>
<tr>
<td>Telegram Bot<div class="sc-label" style="font-size:11px"><%= dependencies.telegram ? dependencies.telegram.message : '' %></div></td>
<td class="text-right">
<span class="badge <%= (dependencies.telegram && dependencies.telegram.status === 'online') ? 'bs' : 'bd' %>">
<%= dependencies.telegram ? dependencies.telegram.status.toUpperCase() : 'UNKNOWN' %>
</span>
</td>
</tr>
</table> </table>
<p class="sc-label mt16 mb8">Router MikroTik</p> <p class="sc-label mt16 mb8">Router MikroTik</p>
+1 -1
View File
@@ -133,7 +133,7 @@ document.addEventListener('DOMContentLoaded', () => {
OPEN-ISP BILLING SYSTEM OPEN-ISP BILLING SYSTEM
</a> </a>
<div style="color:#64748b;font-size:10px;margin-top:3px;line-height:1.5"> <div style="color:#64748b;font-size:10px;margin-top:3px;line-height:1.5">
v<%= (typeof appVersion !== 'undefined' ? appVersion : '0.5') %> (pickle-lime) · ISC · oxpoe v<%= (typeof appVersion !== 'undefined' ? appVersion : '0.5.1') %> (pickle-lime) · ISC · oxpoe
</div> </div>
</div> </div>
</nav> </nav>
+2 -2
View File
@@ -415,8 +415,8 @@
/radius incoming set accept=yes port=3799 /radius incoming set accept=yes port=3799
# 3. Tambahkan Server RADIUS Billing ke MikroTik # 3. Tambahkan Server RADIUS Billing ke MikroTik
/radius remove [find comment="RTRW-Billing-RADIUS"] /radius remove [find comment="OPEN-ISP-Billing-RADIUS"]
/radius add service=ppp address=<%= typeof serverIp !== 'undefined' ? serverIp : '192.168.8.88' %> secret="<%= radiusStatus.secret %>" authentication-port=<%= radiusStatus.authPort %> accounting-port=<%= radiusStatus.acctPort %> timeout=3000ms comment="RTRW-Billing-RADIUS" /radius add service=ppp address=<%= typeof serverIp !== 'undefined' ? serverIp : '192.168.8.88' %> secret="<%= radiusStatus.secret %>" authentication-port=<%= radiusStatus.authPort %> accounting-port=<%= radiusStatus.acctPort %> timeout=3000ms comment="OPEN-ISP-Billing-RADIUS"
# 4. Tambahkan IP Address Gateway Subnet Isolir RADIUS # 4. Tambahkan IP Address Gateway Subnet Isolir RADIUS
/ip address remove [find comment="Gateway-Isolir-RADIUS"] /ip address remove [find comment="Gateway-Isolir-RADIUS"]
+13
View File
@@ -432,6 +432,19 @@
</div> </div>
</div> </div>
<!-- Fitur Opsional -->
<div data-setcat="notif">
<h6 style="font-size:13px;color:var(--muted);margin-bottom:12px;text-transform:uppercase;letter-spacing:1px"><i class="bi bi-toggles"></i> Fitur Opsional</h6>
<div class="fg">
<label class="fl"><%= t('admin.set.voucher_enabled','Fitur Voucher (Beli Voucher Wi-Fi)') %></label>
<select class="fs" name="voucher_enabled">
<option value="true" <%= (settings.voucher_enabled === false || settings.voucher_enabled === 'false' || settings.voucher_enabled === '0' || settings.voucher_enabled === 0) ? '' : 'selected' %>><%= t('common.active','Aktif') %></option>
<option value="false" <%= (settings.voucher_enabled === false || settings.voucher_enabled === 'false' || settings.voucher_enabled === '0' || settings.voucher_enabled === 0) ? 'selected' : '' %>><%= t('common.inactive','Nonaktif') %></option>
</select>
<div style="font-size:10px; color:var(--muted); margin-top:4px">Nonaktifkan untuk menyembunyikan halaman Beli Voucher publik (mis. saat deploy).</div>
</div>
</div>
<!-- WhatsApp --> <!-- WhatsApp -->
<div data-setcat="notif"> <div data-setcat="notif">
<h6 style="font-size:13px;color:var(--muted);margin-bottom:12px;text-transform:uppercase;letter-spacing:1px"><i class="bi bi-whatsapp"></i> Integrasi WhatsApp</h6> <h6 style="font-size:13px;color:var(--muted);margin-bottom:12px;text-transform:uppercase;letter-spacing:1px"><i class="bi bi-whatsapp"></i> Integrasi WhatsApp</h6>
+13
View File
@@ -29,6 +29,17 @@
<% if (info && info.error) { %> <% if (info && info.error) { %>
<div class="alert alert-d"><i class="bi bi-exclamation-circle"></i> <%= info.error %></div> <div class="alert alert-d"><i class="bi bi-exclamation-circle"></i> <%= info.error %></div>
<% } %> <% } %>
<% if (info && info.container) { %>
<div class="alert" style="background:rgba(59,130,246,.08);border:1px solid rgba(59,130,246,.3);border-radius:10px;padding:12px 14px;font-size:13px;line-height:1.55">
<b><i class="bi bi-box-seam"></i> <%= t('admin.update.container_title', 'Instalasi Container (tanpa .git)') %></b><br>
<%= t('admin.update.container_msg', 'Pembaruan TIDAK lewat git. Bangun ulang image dari repo terbaru lalu ganti container:') %>
<div style="margin-top:8px;font-family:monospace;font-size:12px;background:rgba(0,0,0,.25);padding:8px 10px;border-radius:8px">
docker compose build &amp;&amp; docker compose up -d<br>
# atau: build image baru, lalu ganti tag/container
</div>
<%= t('admin.update.container_safe', 'Data (settings.json, database, uploads) tetap aman bila disimpan di volume/mount.') %>
</div>
<% } %>
<div class="form-row" style="align-items:flex-start"> <div class="form-row" style="align-items:flex-start">
<div class="fg"> <div class="fg">
<label class="fl"><%= t('admin.update.server_version', 'Versi Server (version.txt)') %></label> <label class="fl"><%= t('admin.update.server_version', 'Versi Server (version.txt)') %></label>
@@ -51,6 +62,7 @@
</div> </div>
</div> </div>
<% if (!(info && info.container)) { %>
<div style="display:flex;gap:10px;flex-wrap:wrap;margin-top:12px"> <div style="display:flex;gap:10px;flex-wrap:wrap;margin-top:12px">
<form method="GET" action="/admin/update"> <form method="GET" action="/admin/update">
<button type="submit" class="btn btn-g"><i class="bi bi-arrow-repeat"></i> <%= t('admin.update.check', 'Cek Versi') %></button> <button type="submit" class="btn btn-g"><i class="bi bi-arrow-repeat"></i> <%= t('admin.update.check', 'Cek Versi') %></button>
@@ -63,6 +75,7 @@
<div style="margin-top:12px;color:var(--muted);font-size:12px;line-height:1.5"> <div style="margin-top:12px;color:var(--muted);font-size:12px;line-height:1.5">
<%= t('admin.update.keep_files', 'File yang tidak ditimpa saat update: settings.json dan folder database. Setelah update selesai, restart aplikasi agar versi baru aktif.') %> <%= t('admin.update.keep_files', 'File yang tidak ditimpa saat update: settings.json dan folder database. Setelah update selesai, restart aplikasi agar versi baru aktif.') %>
</div> </div>
<% } %>
</div> </div>
</div> </div>
+2 -18
View File
@@ -51,9 +51,9 @@
<a href="/admin/whatsapp/live-chat" class="wa-tab <%= waTab==='livechat' ? 'active' : '' %>"><i class="bi bi-chat-dots-fill"></i> <%= t('admin.wa.tab_livechat','Live Chat') %></a> <a href="/admin/whatsapp/live-chat" class="wa-tab <%= waTab==='livechat' ? 'active' : '' %>"><i class="bi bi-chat-dots-fill"></i> <%= t('admin.wa.tab_livechat','Live Chat') %></a>
</div> </div>
<!-- PENGATURAN HYBRID GATEWAY (BAILEYS VS FONNTE VS META API) --> <!-- PENGATURAN GATEWAY (GOWA / FONNTE / META API) -->
<div class="card mb16" style="padding: 20px;"> <div class="card mb16" style="padding: 20px;">
<h6 style="margin-bottom: 15px; font-weight: 700; color: var(--primary);"><i class="bi bi-sliders"></i> Mode WhatsApp Gateway (Baileys / Fonnte / GOWA / Meta API)</h6> <h6 style="margin-bottom: 15px; font-weight: 700; color: var(--primary);"><i class="bi bi-sliders"></i> Mode WhatsApp Gateway (GoWA / Fonnte / Meta API)</h6>
<form method="POST" action="/admin/whatsapp/gateway-settings"> <form method="POST" action="/admin/whatsapp/gateway-settings">
<div class="form-row mb16"> <div class="form-row mb16">
@@ -334,14 +334,6 @@ async function sendTestGowa() {
<h2 id="status-text"><%= t('admin.whatsapp.checking', 'Mengecek status...') %></h2> <h2 id="status-text"><%= t('admin.whatsapp.checking', 'Mengecek status...') %></h2>
<p id="status-desc" class="text-muted"><%= t('admin.whatsapp.please_wait', 'Harap tunggu sebentar.') %></p> <p id="status-desc" class="text-muted"><%= t('admin.whatsapp.please_wait', 'Harap tunggu sebentar.') %></p>
<% if (waGatewayType === 'baileys') { %>
<div id="qr-section" style="display:none">
<p class="mb-3"><%= t('admin.whatsapp.qr_instruction', 'Silakan pindai QR Code ini dengan aplikasi WhatsApp Anda:') %></p>
<div id="qr-container"></div>
<p class="mt-3 text-sm text-muted"><%= t('admin.whatsapp.qr_auto_refresh', 'QR Code akan diperbarui secara otomatis.') %></p>
</div>
<% } %>
<div id="info-section" style="display:none" class="wa-info"> <div id="info-section" style="display:none" class="wa-info">
<div class="alert alert-s"> <div class="alert alert-s">
<i class="bi bi-check-circle-fill"></i> <%= t('admin.whatsapp.connected_title', 'WhatsApp Terhubung') %> <i class="bi bi-check-circle-fill"></i> <%= t('admin.whatsapp.connected_title', 'WhatsApp Terhubung') %>
@@ -355,14 +347,6 @@ async function sendTestGowa() {
<form method="POST" action="/admin/whatsapp/test-notification" onsubmit="return confirm('<%= t('admin.whatsapp.confirm_test', 'Kirim test notifikasi WhatsApp ke admin sekarang?') %>');"> <form method="POST" action="/admin/whatsapp/test-notification" onsubmit="return confirm('<%= t('admin.whatsapp.confirm_test', 'Kirim test notifikasi WhatsApp ke admin sekarang?') %>');">
<button class="btn btn-p"><i class="bi bi-send-check"></i> <%= t('admin.whatsapp.test_notification', 'Kirim Test Notif') %></button> <button class="btn btn-p"><i class="bi bi-send-check"></i> <%= t('admin.whatsapp.test_notification', 'Kirim Test Notif') %></button>
</form> </form>
<% if (waGatewayType === 'baileys') { %>
<form method="POST" action="/admin/whatsapp/repair-keys" onsubmit="return confirm('Bersihkan cache pre-keys & sesi usang untuk mengatasi masalah pesan pending / Bad-MAC tanpa perlu scan QR ulang?');">
<button class="btn btn-g" style="border-color: #3b82f6; color: #2563eb;"><i class="bi bi-wrench-adjustable"></i> Perbaiki Sesi (Anti Waiting)</button>
</form>
<form method="POST" action="/admin/whatsapp/reset" onsubmit="return confirm('<%= t('admin.whatsapp.confirm_reset', 'Apakah Anda yakin ingin menghapus sesi WhatsApp? Anda harus scan QR ulang setelah ini.') %>')">
<button class="btn btn-d"><i class="bi bi-trash"></i> <%= t('admin.whatsapp.reset_session', 'Hapus Sesi / Reset') %></button>
</form>
<% } %>
</div> </div>
</div> </div>
</div> </div>
+1 -1
View File
@@ -180,7 +180,7 @@
</div> </div>
<div class="iso-foot"> <div class="iso-foot">
&copy; <%= new Date().getFullYear() %> • <%= settings.company_header || 'OPEN-ISP' %> • OPEN-ISP v0.5 (pickle-lime) · ISC · oxpoe — terima kasih atas dukungan Anda. &copy; <%= new Date().getFullYear() %> • <%= settings.company_header || 'OPEN-ISP' %> • <%= settings.company_header || 'OPEN-ISP' %> v<%= (typeof appVersion !== 'undefined' ? appVersion : '0.5.1') %> (<%= (typeof appVersionName !== 'undefined' ? appVersionName : 'pickle-lime') %>) · ISC · oxpoe — terima kasih atas dukungan Anda.
<div style="margin-top:6px"> <div style="margin-top:6px">
<a href="/admin" style="color:var(--muted)"><i class="bi bi-speedometer2"></i> <%= t('donate.admin_panel','Admin Panel') %></a> <a href="/admin" style="color:var(--muted)"><i class="bi bi-speedometer2"></i> <%= t('donate.admin_panel','Admin Panel') %></a>
&nbsp;•&nbsp; &nbsp;•&nbsp;
+1 -1
View File
@@ -352,7 +352,7 @@
</div> </div>
<div class="foot-bottom"> <div class="foot-bottom">
<span>&copy; <span id="year"></span> <%= brand %>. Seluruh hak cipta dilindungi.</span> <span>&copy; <span id="year"></span> <%= brand %>. Seluruh hak cipta dilindungi.</span>
<span class="mono">OPEN-ISP v<%= (typeof appVersion !== 'undefined' ? appVersion : '0.5') %> (<%= (typeof appVersionName !== 'undefined' ? appVersionName : 'pickle-lime') %>) · ISC · oxpoe</span> <span class="mono"><%= (typeof settings !== 'undefined' && settings && settings.company_header) ? settings.company_header : 'OPEN-ISP' %> v<%= (typeof appVersion !== 'undefined' ? appVersion : '0.5.1') %> (<%= (typeof appVersionName !== 'undefined' ? appVersionName : 'pickle-lime') %>) · ISC · oxpoe</span>
</div> </div>
</div> </div>
</footer> </footer>
+5 -4
View File
@@ -1,5 +1,6 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="<%= lang || 'id' %>" data-theme="dark"> <html lang="<%= lang || 'id' %>" data-theme="dark">
<% const voucherOn = !(settings && (settings.voucher_enabled === false || settings.voucher_enabled === 'false' || settings.voucher_enabled === '0' || settings.voucher_enabled === 0)); %>
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover"> <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
@@ -41,7 +42,7 @@
<a href="#packages">Paket</a> <a href="#packages">Paket</a>
<a href="#features"><%= t('landing.nav.features','Keunggulan') %></a> <a href="#features"><%= t('landing.nav.features','Keunggulan') %></a>
<a href="/customer/check-billing"><%= t('landing.hero.check','Cek Tagihan') %></a> <a href="/customer/check-billing"><%= t('landing.hero.check','Cek Tagihan') %></a>
<a href="/customer/voucher"><%= t('login.voucher_nav','Voucher') %></a> <% if (voucherOn) { %><a href="/customer/voucher"><%= t('login.voucher_nav','Voucher') %></a><% } %>
<% if (typeof settings !== 'undefined' && settings && settings.status_url) { %><a href="<%= settings.status_url %>" target="_blank" rel="noopener">Status</a><% } %> <% if (typeof settings !== 'undefined' && settings && settings.status_url) { %><a href="<%= settings.status_url %>" target="_blank" rel="noopener">Status</a><% } %>
</nav> </nav>
<div class="header-actions"> <div class="header-actions">
@@ -56,7 +57,7 @@
<a href="#packages" onclick="toggleMenu()"><%= t('landing.nav.packages_full','Paket & Harga') %></a> <a href="#packages" onclick="toggleMenu()"><%= t('landing.nav.packages_full','Paket & Harga') %></a>
<a href="#features" onclick="toggleMenu()"><%= t('landing.nav.features','Keunggulan') %></a> <a href="#features" onclick="toggleMenu()"><%= t('landing.nav.features','Keunggulan') %></a>
<a href="/customer/check-billing" onclick="toggleMenu()"><i class="bi bi-receipt"></i> <%= t('landing.hero.check','Cek Tagihan') %></a> <a href="/customer/check-billing" onclick="toggleMenu()"><i class="bi bi-receipt"></i> <%= t('landing.hero.check','Cek Tagihan') %></a>
<a href="/customer/voucher" onclick="toggleMenu()"><i class="bi bi-ticket-perforated"></i> <%= t('login.voucher_short','Beli Voucher') %></a> <% if (voucherOn) { %><a href="/customer/voucher" onclick="toggleMenu()"><i class="bi bi-ticket-perforated"></i> <%= t('login.voucher_short','Beli Voucher') %></a><% } %>
<a href="/customer/register" onclick="toggleMenu()"><i class="bi bi-person-plus"></i> <%= t('login.register_new','Daftar Baru') %></a> <a href="/customer/register" onclick="toggleMenu()"><i class="bi bi-person-plus"></i> <%= t('login.register_new','Daftar Baru') %></a>
<% if (typeof settings !== 'undefined' && settings && settings.status_url) { %><a href="<%= settings.status_url %>" target="_blank" rel="noopener" onclick="toggleMenu()"><i class="bi bi-activity"></i> Status Server</a><% } %> <% if (typeof settings !== 'undefined' && settings && settings.status_url) { %><a href="<%= settings.status_url %>" target="_blank" rel="noopener" onclick="toggleMenu()"><i class="bi bi-activity"></i> Status Server</a><% } %>
</div> </div>
@@ -120,7 +121,7 @@
<div class="quick-tools"> <div class="quick-tools">
<a href="/customer/check-billing" class="qtool"><i class="bi bi-receipt"></i> <%= t('landing.hero.check','Cek Tagihan') %></a> <a href="/customer/check-billing" class="qtool"><i class="bi bi-receipt"></i> <%= t('landing.hero.check','Cek Tagihan') %></a>
<a href="/customer/voucher" class="qtool"><i class="bi bi-ticket-perforated"></i> <%= t('login.voucher_short','Beli Voucher') %></a> <% if (voucherOn) { %><a href="/customer/voucher" class="qtool"><i class="bi bi-ticket-perforated"></i> <%= t('login.voucher_short','Beli Voucher') %></a><% } %>
</div> </div>
<div class="auth-foot"> <div class="auth-foot">
@@ -209,7 +210,7 @@
<h4>Portal</h4> <h4>Portal</h4>
<div class="foot-links"> <div class="foot-links">
<a href="/customer/check-billing"><%= t('login.check_full','Cek & Bayar Tagihan') %></a> <a href="/customer/check-billing"><%= t('login.check_full','Cek & Bayar Tagihan') %></a>
<a href="/customer/voucher"><%= t('login.voucher','Beli Voucher Wi-Fi') %></a> <% if (voucherOn) { %><a href="/customer/voucher"><%= t('login.voucher','Beli Voucher Wi-Fi') %></a><% } %>
<a href="/customer/register"><%= t('login.new_install','Pasang Baru') %></a> <a href="/customer/register"><%= t('login.new_install','Pasang Baru') %></a>
<a href="/customer/dashboard"><%= t('login.dashboard','Dashboard Pelanggan') %></a> <a href="/customer/dashboard"><%= t('login.dashboard','Dashboard Pelanggan') %></a>
</div> </div>
+3 -3
View File
@@ -916,7 +916,7 @@
</div> </div>
<div> <div>
<h1 class="font-bold text-base tracking-tight" style="font-family:'Space Grotesk',sans-serif;color:var(--text);"> <h1 class="font-bold text-base tracking-tight" style="font-family:'Space Grotesk',sans-serif;color:var(--text);">
<% if (brandName === 'OPEN-ISP') { %>SKY<span style="color:var(--lime);">FIBER</span><% } else { %><%= brandName %><% } %> <%= brandName %>
</h1> </h1>
<p class="text-[10px]" style="color:var(--muted);margin-top:1px;"><%= t('pwa.title','Portal Pelanggan') %></p> <p class="text-[10px]" style="color:var(--muted);margin-top:1px;"><%= t('pwa.title','Portal Pelanggan') %></p>
</div> </div>
@@ -959,7 +959,7 @@
<img src="<%= brandLogo %>" alt="<%= brandName %>" style="width:100%;height:100%;object-fit:contain;padding:3px;" onerror="this.style.display='none';if(this.nextElementSibling)this.nextElementSibling.style.display=''"><i class="ph-bold ph-wifi-high" style="display:none;color:var(--lime);font-size:15px;"></i> <img src="<%= brandLogo %>" alt="<%= brandName %>" style="width:100%;height:100%;object-fit:contain;padding:3px;" onerror="this.style.display='none';if(this.nextElementSibling)this.nextElementSibling.style.display=''"><i class="ph-bold ph-wifi-high" style="display:none;color:var(--lime);font-size:15px;"></i>
</div> </div>
<span class="brand-word font-bold text-base tracking-tight" style="font-family:'Space Grotesk',sans-serif;color:var(--text);"> <span class="brand-word font-bold text-base tracking-tight" style="font-family:'Space Grotesk',sans-serif;color:var(--text);">
<% if (brandName === 'OPEN-ISP') { %>SKY<span style="color:var(--lime);">FIBER</span><% } else { %><%= brandName %><% } %> <%= brandName %>
</span> </span>
</div> </div>
<div class="hidden md:block desktop-title"> <div class="hidden md:block desktop-title">
@@ -1014,7 +1014,7 @@
<div class="cc-brand-mark" style="overflow:hidden;"> <div class="cc-brand-mark" style="overflow:hidden;">
<img src="<%= brandLogo %>" alt="<%= brandName %>" style="width:100%;height:100%;object-fit:contain;padding:2px;" onerror="this.style.display='none';if(this.nextElementSibling)this.nextElementSibling.style.display=''"><i class="ph-bold ph-wifi-high" style="display:none;font-size:11px;"></i> <img src="<%= brandLogo %>" alt="<%= brandName %>" style="width:100%;height:100%;object-fit:contain;padding:2px;" onerror="this.style.display='none';if(this.nextElementSibling)this.nextElementSibling.style.display=''"><i class="ph-bold ph-wifi-high" style="display:none;font-size:11px;"></i>
</div> </div>
<span style="color:#fff;"><% if (brandName === 'OPEN-ISP') { %>SKY<span>FIBER</span><% } else { %><%= brandName %><% } %></span> <span style="color:#fff;"><%= brandName %></span>
</div> </div>
<div class="cc-top-badges"> <div class="cc-top-badges">
<span class="cc-status"><span class="dot"></span><%= portal.statusText %></span> <span class="cc-status"><span class="dot"></span><%= portal.statusText %></span>