OPEN-ISP v0.5 (pickle-lime) — rilis open-source perdana
Aplikasi billing & manajemen ISP (fiber optic) — PWA pelanggan, portal teknisi & kolektor, admin panel lengkap, multi-vendor OLT, RADIUS, WhatsApp/Telegram, pembayaran QRIS. Kredit & lisensi (ISC): - Base kode & repo: alijayanet/billing-rtrw oleh Alijaya Net — terima kasih banyak atas base kode dan repo yang bermanfaat. - Add-on: GoWA, LibreSpeed/OpenSpeedTest, Uptime Kuma, notification-forwarder. - Dikembangkan menjadi OPEN-ISP oleh oxpoe — https://github.com/oxpoe/open-isp
This commit is contained in:
commit
eaf05f2811
280 files changed
+123975
No files matched your search
@@ -0,0 +1,354 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const crypto = require('crypto');
|
||||
const { logger } = require('./logger');
|
||||
|
||||
let runtimeSessionSecret = null;
|
||||
function getSecureSessionSecretFallback() {
|
||||
if (!runtimeSessionSecret) {
|
||||
runtimeSessionSecret = crypto.randomBytes(32).toString('hex');
|
||||
}
|
||||
return runtimeSessionSecret;
|
||||
}
|
||||
|
||||
// Cache untuk settings dengan timestamp
|
||||
let settingsCache = null;
|
||||
let settingsCacheTime = 0;
|
||||
const CACHE_DURATION = 2000; // 2 detik
|
||||
|
||||
// File system watcher untuk auto-reload settings
|
||||
const settingsPath = path.join(__dirname, '../settings.json');
|
||||
let watcher = null;
|
||||
|
||||
// Helper untuk baca settings.json secara dinamis
|
||||
function getSettings() {
|
||||
try {
|
||||
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8')) || {};
|
||||
|
||||
// Secure fallback for session_secret
|
||||
const defaultSecret = 'rahasia-portal-pelanggan-default-ganti-ini';
|
||||
if (!settings.session_secret || settings.session_secret === defaultSecret) {
|
||||
settings.session_secret = getSecureSessionSecretFallback();
|
||||
}
|
||||
|
||||
const fallbackTz = 'Asia/Jakarta';
|
||||
const tz = typeof settings.timezone === 'string' ? settings.timezone.trim() : '';
|
||||
|
||||
if (!tz) {
|
||||
settings.timezone = fallbackTz;
|
||||
return settings;
|
||||
}
|
||||
|
||||
try {
|
||||
new Intl.DateTimeFormat('en-US', { timeZone: tz }).format(new Date());
|
||||
settings.timezone = tz;
|
||||
} catch (e) {
|
||||
settings.timezone = fallbackTz;
|
||||
}
|
||||
|
||||
return settings;
|
||||
} catch (error) {
|
||||
logger.error(`[settings] Error reading settings.json: ${error.message}`);
|
||||
// Kembalikan cache terakhir yang valid (hindari kehilangan setting karena file setengah tertulis)
|
||||
return settingsCache || {};
|
||||
}
|
||||
}
|
||||
|
||||
// Helper untuk baca settings.json dengan cache
|
||||
function getSettingsWithCache() {
|
||||
const now = Date.now();
|
||||
if (!settingsCache || (now - settingsCacheTime) > CACHE_DURATION) {
|
||||
settingsCache = getSettings();
|
||||
settingsCacheTime = now;
|
||||
}
|
||||
return settingsCache;
|
||||
}
|
||||
|
||||
// Helper untuk mendapatkan nilai setting dengan fallback
|
||||
function getSetting(key, defaultValue = null) {
|
||||
const settings = getSettingsWithCache();
|
||||
return settings[key] !== undefined ? settings[key] : defaultValue;
|
||||
}
|
||||
|
||||
// Helper untuk mendapatkan multiple settings
|
||||
function getSettingsByKeys(keys) {
|
||||
const settings = getSettingsWithCache();
|
||||
const result = {};
|
||||
keys.forEach(key => {
|
||||
result[key] = settings[key];
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
// File system watcher untuk auto-reload settings
|
||||
function startSettingsWatcher() {
|
||||
try {
|
||||
// Hapus watcher lama jika ada
|
||||
if (watcher) {
|
||||
watcher.close();
|
||||
}
|
||||
|
||||
// Buat watcher baru
|
||||
watcher = fs.watch(settingsPath, (eventType, filename) => {
|
||||
if (eventType !== 'change') return;
|
||||
// Di Windows `filename` sering null; hanya abaikan jika jelas bukan settings.json
|
||||
if (filename != null && filename !== 'settings.json') return;
|
||||
|
||||
settingsCache = null;
|
||||
settingsCacheTime = 0;
|
||||
|
||||
try {
|
||||
const s = getSettingsWithCache();
|
||||
const port = s.server_port ?? 4555;
|
||||
const host = s.server_host || 'localhost';
|
||||
const gurl = s.genieacs_url || '(tidak diatur)';
|
||||
const company = s.company_header || '(default)';
|
||||
logger.info(`[settings] settings.json dimuat ulang — port ${port}, host ${host}, company: ${company}, GenieACS: ${gurl}`);
|
||||
} catch (error) {
|
||||
logger.error(`[settings] Gagal memuat ulang settings.json: ${error.message}`);
|
||||
}
|
||||
});
|
||||
|
||||
logger.info('[settings] Memantau perubahan settings.json');
|
||||
} catch (error) {
|
||||
logger.error(`[settings] Error starting settings watcher: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Mulai watcher saat modul dimuat
|
||||
startSettingsWatcher();
|
||||
|
||||
// Menyimpan pengaturan ke settings.json
|
||||
function saveSettings(newSettings) {
|
||||
try {
|
||||
const currentSettings = getSettings();
|
||||
const updatedSettings = { ...currentSettings, ...newSettings };
|
||||
// Cadangan rolling versi sebelumnya (mencegah kehilangan setting bila tertimpa salah)
|
||||
try {
|
||||
if (fs.existsSync(settingsPath)) {
|
||||
fs.copyFileSync(settingsPath, settingsPath + '.prev');
|
||||
}
|
||||
} catch (e) { /* abaikan */ }
|
||||
const tmpPath = settingsPath + '.tmp-' + process.pid;
|
||||
fs.writeFileSync(tmpPath, JSON.stringify(updatedSettings, null, 2), 'utf-8');
|
||||
fs.renameSync(tmpPath, settingsPath); // atomik: pembaca tidak pernah melihat file setengah jadi
|
||||
settingsCache = updatedSettings;
|
||||
settingsCacheTime = Date.now();
|
||||
return true;
|
||||
} catch (error) {
|
||||
logger.error(`[settings] Error saving settings.json: ${error.message}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper untuk mendapatkan waktu sekarang dalam format lokal
|
||||
* sesuai timezone yang diatur di settings.json
|
||||
*/
|
||||
function getNowLocal() {
|
||||
const tz = getSetting('timezone', 'Asia/Jakarta');
|
||||
const now = new Date();
|
||||
const options = {
|
||||
timeZone: tz,
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
hourCycle: 'h23' // 💡 Paksa format jam 00-23 (mencegah bug '24' di tengah malam)
|
||||
};
|
||||
const formatter = new Intl.DateTimeFormat('en-US', options);
|
||||
const parts = formatter.formatToParts(now);
|
||||
const p = {};
|
||||
parts.forEach(part => p[part.type] = part.value);
|
||||
return `${p.year}-${p.month}-${p.day} ${p.hour}:${p.minute}:${p.second}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper untuk mendapatkan objek Date yang sudah disesuaikan dengan timezone di settings.
|
||||
* Mengembalikan objek Date yang "angkanya" sudah sesuai dengan waktu lokal.
|
||||
*/
|
||||
function getCurrentDateInTimezone() {
|
||||
const tz = getSetting('timezone', 'Asia/Jakarta');
|
||||
const now = new Date();
|
||||
|
||||
// Ambil string format ISO lokal
|
||||
const formatter = new Intl.DateTimeFormat('en-US', {
|
||||
timeZone: tz,
|
||||
year: 'numeric', month: '2-digit', day: '2-digit',
|
||||
hour: '2-digit', minute: '2-digit', second: '2-digit',
|
||||
hourCycle: 'h23' // 💡 Paksa format jam 00-23 (mencegah bug '24' di tengah malam)
|
||||
});
|
||||
|
||||
const parts = formatter.formatToParts(now);
|
||||
const p = {};
|
||||
parts.forEach(part => p[part.type] = part.value);
|
||||
|
||||
// Buat objek Date baru dengan nilai lokal tersebut
|
||||
return new Date(`${p.year}-${p.month}-${p.day}T${p.hour}:${p.minute}:${p.second}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mendapatkan info waktu sekarang (year, month, day, dll) dalam timezone yang diatur.
|
||||
*/
|
||||
function getCurrentTimeInfo() {
|
||||
const tz = getSetting('timezone', 'Asia/Jakarta');
|
||||
const now = new Date();
|
||||
const formatter = new Intl.DateTimeFormat('en-US', {
|
||||
timeZone: tz,
|
||||
year: 'numeric', month: 'numeric', day: 'numeric',
|
||||
hour: 'numeric', minute: 'numeric', second: 'numeric',
|
||||
hourCycle: 'h23' // 💡 Paksa format jam 00-23 (mencegah bug '24' di tengah malam)
|
||||
});
|
||||
|
||||
|
||||
const parts = formatter.formatToParts(now);
|
||||
const p = {};
|
||||
parts.forEach(part => p[part.type] = part.value);
|
||||
|
||||
return {
|
||||
year: parseInt(p.year),
|
||||
month: parseInt(p.month),
|
||||
day: parseInt(p.day),
|
||||
hour: parseInt(p.hour),
|
||||
minute: parseInt(p.minute),
|
||||
second: parseInt(p.second)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Mendapatkan string ISO-like tapi dalam waktu lokal (bukan UTC).
|
||||
* Berguna untuk timestamp log/backup.
|
||||
*/
|
||||
function getNowLocalISO() {
|
||||
const info = getCurrentTimeInfo();
|
||||
const pad = (n) => String(n).padStart(2, '0');
|
||||
return `${info.year}-${pad(info.month)}-${pad(info.day)}T${pad(info.hour)}:${pad(info.minute)}:${pad(info.second)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Memparse string tanggal (YYYY-MM-DD HH:mm:ss) menjadi objek Date
|
||||
* dengan asumsi string tersebut adalah waktu lokal sesuai setting timezone.
|
||||
*/
|
||||
function parseDateInTimezone(dateStr) {
|
||||
if (!dateStr) return null;
|
||||
const tz = getSetting('timezone', 'Asia/Jakarta');
|
||||
|
||||
const date = new Date(dateStr.replace(' ', 'T'));
|
||||
if (isNaN(date.getTime())) return null;
|
||||
|
||||
const localDateStr = date.toLocaleString('en-US', { timeZone: tz, hourCycle: 'h23' });
|
||||
const localDate = new Date(localDateStr);
|
||||
const diff = localDate.getTime() - date.getTime();
|
||||
|
||||
return new Date(date.getTime() - diff);
|
||||
}
|
||||
|
||||
function formatDateLocal(date) {
|
||||
if (!date) return '-';
|
||||
const tz = getSetting('timezone', 'Asia/Jakarta');
|
||||
let d;
|
||||
if (typeof date === 'string') {
|
||||
d = parseDateInTimezone(date);
|
||||
} else {
|
||||
d = typeof date === 'number' ? new Date(date) : date;
|
||||
}
|
||||
if (!d || isNaN(d.getTime())) return '-';
|
||||
return d.toLocaleString('id-ID', { timeZone: tz });
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper untuk memformat objek Date menjadi string waktu lokal (hanya Jam:Menit)
|
||||
*/
|
||||
function formatTimeLocal(date) {
|
||||
if (!date) return '-';
|
||||
const tz = getSetting('timezone', 'Asia/Jakarta');
|
||||
let d;
|
||||
if (typeof date === 'string') {
|
||||
d = parseDateInTimezone(date);
|
||||
} else {
|
||||
d = typeof date === 'number' ? new Date(date) : date;
|
||||
}
|
||||
if (!d || isNaN(d.getTime())) return '-';
|
||||
return d.toLocaleTimeString('id-ID', { timeZone: tz, hour: '2-digit', minute: '2-digit' });
|
||||
}
|
||||
|
||||
/**
|
||||
* Memastikan semua setting wajib ada di settings.json dengan default value.
|
||||
* Dijalankan saat startup aplikasi untuk migrasi/inisialisasi setting baru.
|
||||
*/
|
||||
function ensureDefaultSettings() {
|
||||
try {
|
||||
const currentSettings = getSettings();
|
||||
let needsSave = false;
|
||||
|
||||
// Default settings yang wajib ada
|
||||
const defaultSettings = {
|
||||
// RADIUS Server settings (ditambahkan untuk update dari GitHub)
|
||||
radius_enabled: '0',
|
||||
radius_secret: 'secret123',
|
||||
radius_auth_port: '1812',
|
||||
radius_acct_port: '1813',
|
||||
radius_isolir_action: 'pool',
|
||||
radius_isolir_pool: 'isolir',
|
||||
radius_limit_simultaneous: '1',
|
||||
radius_default_rate_limit: '5M/10M',
|
||||
radius_isolir_rate_limit: '512k/512k',
|
||||
radius_isolir_ip_pool_enabled: '1',
|
||||
radius_isolir_ip_pool_start: '10.10.99.2',
|
||||
radius_isolir_ip_pool_end: '10.10.99.254',
|
||||
radius_ip_pool_enabled: '1',
|
||||
radius_ip_pool_start: '10.10.10.2',
|
||||
radius_ip_pool_end: '10.10.10.254',
|
||||
radius_framed_pool: 'pool-pppoe',
|
||||
radius_send_group: '0',
|
||||
webhook_notif_retention_enabled: '1',
|
||||
webhook_notif_retention_days: '30',
|
||||
finance_ppn_pct: '11',
|
||||
finance_pph_pct: '22',
|
||||
finance_uso_pct: '1.75',
|
||||
finance_bhp_pct: '0.5',
|
||||
finance_revenue_includes_ppn: '0'
|
||||
};
|
||||
|
||||
// Cek dan tambahkan setting yang belum ada
|
||||
for (const [key, defaultValue] of Object.entries(defaultSettings)) {
|
||||
if (currentSettings[key] === undefined) {
|
||||
currentSettings[key] = defaultValue;
|
||||
needsSave = true;
|
||||
logger.info(`[settings] Added missing setting: ${key} = ${defaultValue}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Simpan jika ada perubahan
|
||||
if (needsSave) {
|
||||
fs.writeFileSync(settingsPath, JSON.stringify(currentSettings, null, 2), 'utf-8');
|
||||
settingsCache = currentSettings;
|
||||
settingsCacheTime = Date.now();
|
||||
logger.info('[settings] settings.json updated with default RADIUS settings');
|
||||
}
|
||||
|
||||
return needsSave;
|
||||
} catch (error) {
|
||||
logger.error(`[settings] Error ensuring default settings: ${error.message}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getSettings,
|
||||
getSettingsWithCache,
|
||||
getSetting,
|
||||
getSettingsByKeys,
|
||||
saveSettings,
|
||||
getNowLocal,
|
||||
formatDateLocal,
|
||||
formatTimeLocal,
|
||||
getCurrentDateInTimezone,
|
||||
getCurrentTimeInfo,
|
||||
getNowLocalISO,
|
||||
parseDateInTimezone,
|
||||
startSettingsWatcher,
|
||||
ensureDefaultSettings
|
||||
};
|
||||
Reference in new issue
Block a user