Files
open-isp/config/settingsManager.js
T

355 lines
11 KiB
JavaScript

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 } = require('./paths');
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
};