Files
open-isp/services/sidebarMenuService.js
T
oxpoe eaf05f2811 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
2026-10-09 05:51:21 +07:00

376 lines
20 KiB
JavaScript

const crypto = require('crypto');
const { getSetting, getSettings, saveSettings } = require('../config/settingsManager');
const { getAppSetting, saveAppSetting } = require('../config/database');
const FEATURE_PASSWORD_HASH = 'af03458614be223016c5bc1c0788eaefc6a6bfd0c11cdcd4bc520f49d0cb474a'; // sha256 kode aktivasi — plaintext hanya di settings.json (activation_code), tidak di repo
const FEATURE_CONTACT_PHONE = '081947215703';
function getFeaturePasswordHash() {
return getSetting('feature_password_hash', FEATURE_PASSWORD_HASH);
}
function getFeatureContactPhone() {
return getSetting('feature_contact_phone', FEATURE_CONTACT_PHONE);
}
const SETTINGS_KEY = 'sidebar_menu_states';
const STATE_VISIBLE = 'visible';
const STATE_HIDDEN = 'hidden';
const STATE_LOCKED = 'locked';
const VALID_STATES = new Set([STATE_VISIBLE, STATE_HIDDEN, STATE_LOCKED]);
const MENU_DEFINITIONS = [
{ key: 'dashboard', section: 'main', href: '/admin', icon: 'bi bi-speedometer2', labelKey: 'admin.nav.dashboard', labelDefault: 'Dashboard', roles: ['admin', 'cashier'], bottomNav: true, bottomNavOrder: 1, activePages: ['dashboard'] },
{ key: 'network', section: 'main', href: '/admin/mikrotik', icon: 'bi bi-diagram-3', labelKey: 'admin.nav.network', labelDefault: 'Jaringan', roles: ['admin', 'cashier'], activePages: ['mikrotik', 'radius_settings', 'acs_pro', 'onu_provision', 'olts', 'monitoring'], children: [
{ key: 'mikrotik', section: 'main', href: '/admin/mikrotik', icon: 'bi bi-router', labelKey: 'admin.nav.mikrotik', labelDefault: 'MikroTik', roles: ['admin', 'cashier'], activePages: ['mikrotik'] },
{ key: 'radius_server', section: 'main', href: '/admin/radius-settings', icon: 'bi bi-broadcast', labelKey: 'admin.nav.radius_server', labelDefault: 'RADIUS Server', roles: ['admin'], activePages: ['radius_settings'] },
{ key: 'acs_pro', section: 'main', href: '/admin/acs', icon: 'bi bi-hdd-network', labelKey: 'admin.nav.acs_pro', labelDefault: 'GenieACS Pro', roles: ['admin'], activePages: ['acs_pro'] },
{ key: 'onu_provision', section: 'main', href: '/admin/onu-provision', icon: 'bi bi-hdd-network-fill', labelKey: 'admin.nav.onu_provision', labelDefault: 'ONU Provision', roles: ['admin'], activePages: ['onu_provision'] },
{ key: 'olts', section: 'main', href: '/admin/olts', icon: 'bi bi-hdd-fill', labelKey: 'admin.nav.olt_management', labelDefault: 'Manajemen OLT', roles: ['admin'], activePages: ['olts'] },
{ key: 'monitoring', section: 'main', href: '/admin/monitoring', icon: 'bi bi-activity', labelKey: 'admin.nav.monitoring', labelDefault: 'Monitoring Sistem', roles: ['admin'], activePages: ['monitoring'] }
] },
{ key: 'map', section: 'main', href: '/admin/map', icon: 'bi bi-map', labelKey: 'admin.nav.network_map', labelDefault: 'Peta Jaringan', roles: ['admin', 'cashier'], bottomNav: true, bottomNavOrder: 4, activePages: ['map'] },
{ key: 'whatsapp', section: 'main', href: '/admin/whatsapp', icon: 'bi bi-whatsapp', labelKey: 'admin.nav.whatsapp', labelDefault: 'WhatsApp', roles: ['admin', 'cashier'], activePages: ['whatsapp','broadcast','whatsapp_live_chat','whatsapp_templates'] },
{ key: 'automation', section: 'main', href: '/admin/automation', icon: 'bi bi-robot', labelKey: 'admin.nav.automation', labelDefault: 'Automation', roles: ['admin'], activePages: ['automation'] },
{ key: 'grp_customers', section: 'billing', href: '/admin/customers', icon: 'bi bi-people', labelKey: 'admin.nav.customers', labelDefault: 'Pelanggan', roles: ['admin', 'cashier'], bottomNav: true, bottomNavOrder: 2, activePages: ['customers', 'onu_stickers'], children: [
{ key: 'customers', section: 'billing', href: '/admin/customers', icon: 'bi bi-people', labelKey: 'admin.nav.customers_data', labelDefault: 'Data Pelanggan', roles: ['admin', 'cashier'], activePages: ['customers'] },
{ key: 'onu_stickers', section: 'billing', href: '/admin/onu-stickers', icon: 'bi bi-qr-code', labelKey: 'admin.nav.onu_stickers', labelDefault: 'Stiker Modem ONU', roles: ['admin', 'cashier'], activePages: ['onu_stickers'] }
] },
{ key: 'grp_packages', section: 'billing', href: '/admin/packages', icon: 'bi bi-box-seam', labelKey: 'admin.nav.grp_packages', labelDefault: 'Paket & Voucher', roles: ['admin', 'cashier'], activePages: ['packages', 'voucher_packages', 'vouchers_discounts', 'discounts', 'promo_slides'], children: [
{ key: 'packages', section: 'billing', href: '/admin/packages', icon: 'bi bi-box-seam', labelKey: 'admin.nav.internet_packages', labelDefault: 'Paket Internet', roles: ['admin', 'cashier'], activePages: ['packages'] },
{ key: 'voucher_packages', section: 'billing', href: '/admin/vouchers/packages', icon: 'bi bi-ticket-detailed', labelKey: 'admin.nav.voucher_packages', labelDefault: 'Paket Voucher', roles: ['admin', 'cashier'], activePages: ['voucher_packages'] },
{ key: 'vouchers_discounts', section: 'billing', href: '/admin/vouchers-discounts', icon: 'bi bi-ticket-perforated', labelKey: 'admin.nav.vouchers_discounts', labelDefault: 'Biaya Pasang', roles: ['admin'], activePages: ['vouchers_discounts'] },
{ key: 'discounts', section: 'billing', href: '/admin/discounts', icon: 'bi bi-percent', labelKey: 'admin.nav.discounts', labelDefault: 'Diskon', roles: ['admin', 'cashier'], activePages: ['discounts'] },
{ key: 'promo_slides', section: 'billing', href: '/admin/promo-slides', icon: 'bi bi-image', labelKey: 'admin.nav.promo_slides', labelDefault: 'Promo Slides', roles: ['admin'], activePages: ['promo_slides'] }
] },
{ key: 'grp_billing', section: 'billing', href: '/admin/billing', icon: 'bi bi-receipt', labelKey: 'admin.nav.invoices', labelDefault: 'Tagihan', roles: ['admin', 'cashier'], bottomNav: true, bottomNavOrder: 3, activePages: ['billing', 'collector_payments'], children: [
{ key: 'billing', section: 'billing', href: '/admin/billing', icon: 'bi bi-receipt', labelKey: 'admin.nav.invoices_list', labelDefault: 'Data Tagihan', roles: ['admin', 'cashier'], activePages: ['billing'] },
{ key: 'collector_payments', section: 'billing', href: '/admin/collector-payments', icon: 'bi bi-check2-square', labelKey: 'admin.nav.collector_payments', labelDefault: 'Approval Kolektor', roles: ['admin', 'cashier'], activePages: ['collector_payments'] }
] },
{ key: 'grp_finance', section: 'finance', href: '/admin/reports', icon: 'bi bi-cash-stack', labelKey: 'admin.nav.grp_finance', labelDefault: 'Keuangan', roles: ['admin', 'cashier'], activePages: ['reports', 'cashiers_reports', 'payroll', 'cash_in', 'expenses', 'expense_categories'], children: [
{ key: 'reports', section: 'finance', href: '/admin/reports', icon: 'bi bi-bar-chart-line', labelKey: 'admin.nav.finance_report', labelDefault: 'Laporan Keuangan', roles: ['admin', 'cashier'], activePages: ['reports'] },
{ key: 'cashiers_reports', section: 'finance', href: '/admin/cashiers/reports', icon: 'bi bi-journal-text', labelKey: 'admin.nav.cashiers_reports', labelDefault: 'Laporan Kasir', roles: ['admin', 'cashier'], activePages: ['cashiers_reports'] },
{ key: 'cash_in', section: 'finance', href: '/admin/finance/cash-in', icon: 'bi bi-cash-stack', labelKey: 'admin.nav.cash_in', labelDefault: 'Kas Masuk', roles: ['admin', 'cashier'], activePages: ['cash_in'] },
{ key: 'expenses', section: 'finance', href: '/admin/finance/expenses', icon: 'bi bi-wallet2', labelKey: 'admin.nav.expenses', labelDefault: 'Pengeluaran', roles: ['admin', 'cashier'], activePages: ['expenses'] },
{ key: 'expense_categories', section: 'finance', href: '/admin/finance/expense-categories', icon: 'bi bi-tags', labelKey: 'admin.nav.expense_categories', labelDefault: 'Kategori Pengeluaran', roles: ['admin'], activePages: ['expense_categories'] },
{ key: 'payroll', section: 'finance', href: '/admin/payroll', icon: 'bi bi-wallet2', labelKey: 'admin.nav.payroll', labelDefault: 'Gaji & Payroll', roles: ['admin'], activePages: ['payroll'] }
] },
{ key: 'grp_service', section: 'service', href: '/admin/tickets', icon: 'bi bi-headset', labelKey: 'admin.nav.grp_service', labelDefault: 'Layanan', roles: ['admin', 'cashier'], activePages: ['tickets', 'inventory', 'attendance'], children: [
{ key: 'tickets', section: 'service', href: '/admin/tickets', icon: 'bi bi-headset', labelKey: 'admin.nav.customer_tickets', labelDefault: 'Keluhan Pelanggan', roles: ['admin', 'cashier'], activePages: ['tickets'] },
{ key: 'inventory', section: 'service', href: '/admin/inventory', icon: 'bi bi-boxes', labelKey: 'admin.nav.inventory', labelDefault: 'Inventaris (Stok)', roles: ['admin', 'cashier'], activePages: ['inventory'] },
{ key: 'attendance', section: 'service', href: '/admin/attendance', icon: 'bi bi-calendar-check', labelKey: 'admin.nav.attendance', labelDefault: 'Absensi Karyawan', roles: ['admin', 'cashier'], activePages: ['attendance'] }
] },
{ key: 'cashier_attendance', section: 'cashier', href: '/admin/cashiers/attendance', icon: 'bi bi-calendar-check', labelKey: 'admin.nav.cashier_attendance', labelDefault: 'Absensi Saya', roles: ['cashier'], activePages: ['cashier_attendance'] },
{ key: 'grp_users', section: 'user_management', href: '/admin/technicians', icon: 'bi bi-person-gear', labelKey: 'admin.nav.grp_users', labelDefault: 'Pengguna', roles: ['admin', 'cashier'], activePages: ['technicians', 'cashiers', 'collectors', 'areas'], children: [
{ key: 'technicians', section: 'user_management', href: '/admin/technicians', icon: 'bi bi-person-gear', labelKey: 'admin.nav.technicians', labelDefault: 'Teknisi', roles: ['admin'], activePages: ['technicians'] },
{ key: 'cashiers', section: 'user_management', href: '/admin/cashiers', icon: 'bi bi-person-vcard', labelKey: 'admin.nav.cashiers', labelDefault: 'Kasir', roles: ['admin'], activePages: ['cashiers'] },
{ key: 'collectors', section: 'user_management', href: '/admin/collectors', icon: 'bi bi-person-badge', labelKey: 'admin.nav.collectors', labelDefault: 'Kolektor', roles: ['admin'], activePages: ['collectors'] },
{ key: 'areas', section: 'user_management', href: '/admin/areas', icon: 'bi bi-geo-alt', labelKey: 'admin.nav.areas', labelDefault: 'Manajemen Area', roles: ['admin', 'cashier'], activePages: ['areas'] }
] },
{ key: 'grp_system', section: 'system', href: '/admin/settings', icon: 'bi bi-gear', labelKey: 'admin.nav.grp_system', labelDefault: 'Sistem', roles: ['admin'], activePages: ['update', 'service_notice', 'settings', 'ewallet_logs', 'backup', 'audit_logs'], children: [
{ key: 'update', section: 'system', href: '/admin/update', icon: 'bi bi-cloud-arrow-down', labelKey: 'admin.nav.update', labelDefault: 'Update GitHub', roles: ['admin'], activePages: ['update'] },
{ key: 'service_notice', section: 'system', href: '/admin/service-notice', icon: 'bi bi-megaphone-fill', labelKey: 'admin.nav.service_notice', labelDefault: 'Status Layanan', roles: ['admin'], activePages: ['service_notice'] },
{ key: 'settings', section: 'system', href: '/admin/settings', icon: 'bi bi-gear', labelKey: 'admin.nav.settings', labelDefault: 'Pengaturan', roles: ['admin'], activePages: ['settings'] },
{ key: 'ewallet_logs', section: 'system', href: '/admin/ewallet-logs', icon: 'bi bi-journal-text', labelKey: 'admin.nav.log_rekap', labelDefault: 'Log & Rekap', roles: ['admin'], activePages: ['ewallet_logs'] },
{ key: 'backup', section: 'system', href: '/admin/backup', icon: 'bi bi-hdd-stack', labelKey: 'admin.nav.backup', labelDefault: 'Backup & Recovery', roles: ['admin'], activePages: ['backup'] },
{ key: 'audit_logs', section: 'system', href: '/admin/audit-logs', icon: 'bi bi-shield-lock', labelKey: 'admin.nav.audit_logs', labelDefault: 'Log Aktivitas', roles: ['admin'], activePages: ['audit_logs'] }
] }
];
const DEFAULT_MENU_STATES = {
dashboard: STATE_VISIBLE,
mikrotik: STATE_VISIBLE,
network: STATE_VISIBLE,
grp_customers: STATE_VISIBLE,
grp_packages: STATE_VISIBLE,
grp_billing: STATE_VISIBLE,
grp_finance: STATE_VISIBLE,
grp_service: STATE_VISIBLE,
grp_users: STATE_VISIBLE,
grp_system: STATE_VISIBLE,
map: STATE_VISIBLE,
radius_server: STATE_VISIBLE,
map: STATE_VISIBLE,
acs_pro: STATE_VISIBLE,
onu_provision: STATE_VISIBLE,
olts: STATE_HIDDEN,
whatsapp: STATE_VISIBLE,
automation: STATE_VISIBLE,
broadcast: STATE_VISIBLE,
whatsapp_live_chat: STATE_VISIBLE,
promo_slides: STATE_VISIBLE,
customers: STATE_VISIBLE,
onu_stickers: STATE_VISIBLE,
packages: STATE_VISIBLE,
voucher_packages: STATE_VISIBLE,
billing: STATE_VISIBLE,
reports: STATE_VISIBLE,
cashiers_reports: STATE_VISIBLE,
collector_payments: STATE_VISIBLE,
tickets: STATE_VISIBLE,
inventory: STATE_LOCKED,
attendance: STATE_LOCKED,
payroll: STATE_LOCKED,
cash_in: STATE_VISIBLE,
expenses: STATE_VISIBLE,
expense_categories: STATE_VISIBLE,
cashier_attendance: STATE_VISIBLE,
technicians: STATE_LOCKED,
cashiers: STATE_LOCKED,
collectors: STATE_LOCKED,
areas: STATE_VISIBLE,
update: STATE_VISIBLE,
vouchers_discounts: STATE_VISIBLE,
discounts: STATE_VISIBLE,
service_notice: STATE_VISIBLE,
settings: STATE_VISIBLE,
ewallet_logs: STATE_VISIBLE,
backup: STATE_VISIBLE,
monitoring: STATE_VISIBLE,
audit_logs: STATE_VISIBLE
};
const SECTION_DEFINITIONS = [
{ key: 'main', labelKey: 'admin.section.main', labelDefault: 'UTAMA' },
{ key: 'billing', labelKey: 'admin.section.billing', labelDefault: 'BILLING' },
{ key: 'finance', labelKey: 'admin.section.finance', labelDefault: 'KEUANGAN' },
{ key: 'service', labelKey: 'admin.section.service', labelDefault: 'LAYANAN' },
{ key: 'cashier', labelKey: 'admin.section.cashier', labelDefault: 'KASIR' },
{ key: 'user_management', labelKey: 'admin.section.user_management', labelDefault: 'MANAJEMEN USER' },
{ key: 'system', labelKey: 'admin.section.system', labelDefault: 'SISTEM' }
];
function sha256(input) {
return crypto.createHash('sha256').update(String(input || '')).digest('hex');
}
function normalizeState(value) {
const normalized = String(value || '').trim().toLowerCase();
return VALID_STATES.has(normalized) ? normalized : STATE_VISIBLE;
}
function allMenuDefs() {
const out = [];
for (const m of MENU_DEFINITIONS) {
out.push(m);
if (Array.isArray(m.children)) { for (const c of m.children) out.push(c); }
}
return out;
}
function getStoredMenuStates() {
// Coba ambil dari Database dulu (Lebih Aman)
let raw = getAppSetting(SETTINGS_KEY, null);
let activationKeys = getAppSetting('sidebar_activation_keys', {});
// Fallback ke settings.json jika di DB masih kosong (Migration)
if (raw === null) {
raw = getSetting(SETTINGS_KEY, {});
activationKeys = getSetting('sidebar_activation_keys', {});
// Langsung migrasi ke DB agar kedepannya pakai DB
if (Object.keys(raw).length > 0) {
saveAppSetting(SETTINGS_KEY, raw);
saveAppSetting('sidebar_activation_keys', activationKeys);
}
}
const stateMap = {};
for (const menu of allMenuDefs()) {
const defaultState = DEFAULT_MENU_STATES[menu.key] || STATE_VISIBLE;
let storedState = raw && raw[menu.key] ? raw[menu.key] : defaultState;
const normalized = normalizeState(storedState);
// Jika menu aslinya LOCKED tapi diubah jadi VISIBLE/HIDDEN, cek kunci aktivasinya
if (defaultState === STATE_LOCKED && normalized !== STATE_LOCKED) {
const expectedKey = sha256(menu.key + getFeaturePasswordHash());
const providedKey = activationKeys[menu.key];
if (providedKey !== expectedKey) {
// Kunci tidak cocok! Kembalikan ke LOCKED
stateMap[menu.key] = STATE_LOCKED;
continue;
}
}
stateMap[menu.key] = normalized;
}
return stateMap;
}
function saveMenuStates(stateMap) {
const activationKeys = getAppSetting('sidebar_activation_keys', {});
const passwordHash = getFeaturePasswordHash();
for (const key in stateMap) {
const newState = stateMap[key];
const defaultState = DEFAULT_MENU_STATES[key] || STATE_VISIBLE;
// Jika menu yang aslinya LOCKED diaktifkan (jadi visible/hidden), generate kunci
if (defaultState === STATE_LOCKED && newState !== STATE_LOCKED) {
activationKeys[key] = sha256(key + passwordHash);
} else if (newState === STATE_LOCKED) {
// Jika dikunci kembali, hapus kuncinya
delete activationKeys[key];
}
}
// Simpan ke Database (Utama)
saveAppSetting(SETTINGS_KEY, sanitizeMenuStates(stateMap));
saveAppSetting('sidebar_activation_keys', activationKeys);
// Hanya gunakan database, tidak perlu backup ke settings.json
return true;
}
function sanitizeMenuStates(input) {
const clean = {};
for (const menu of allMenuDefs()) {
const defaultState = DEFAULT_MENU_STATES[menu.key] || STATE_VISIBLE;
let state = input && input[menu.key] ? input[menu.key] : defaultState;
clean[menu.key] = normalizeState(state);
}
return clean;
}
function isMenuAllowedForSession(menu, session) {
const roles = Array.isArray(menu.roles) ? menu.roles : ['admin'];
if (roles.includes('admin') && session && session.isAdmin) return true;
if (roles.includes('cashier') && session && session.isCashier) return true;
return false;
}
function enrichMenu(menu, states) {
const state = states[menu.key] || DEFAULT_MENU_STATES[menu.key] || STATE_VISIBLE;
const locked = state === STATE_LOCKED;
const hidden = state === STATE_HIDDEN;
const out = {
...menu,
state,
locked,
hidden,
hrefResolved: menu.href,
lockedMessage: locked ? `Menu "${menu.labelDefault}" terkunci. Hubungi ${getFeatureContactPhone()} untuk mendapatkan password aktivasi.` : ''
};
if (Array.isArray(menu.children)) {
out.children = menu.children.map((ch) => enrichMenu(ch, states));
}
return out;
}
function getSidebarSections(session) {
const states = getStoredMenuStates();
return SECTION_DEFINITIONS.map((section) => {
const items = MENU_DEFINITIONS
.filter((menu) => menu.section === section.key)
.filter((menu) => isMenuAllowedForSession(menu, session))
.map((menu) => {
const enriched = enrichMenu(menu, states);
if (Array.isArray(enriched.children)) {
enriched.children = enriched.children
.filter((ch) => isMenuAllowedForSession(ch, session))
.filter((ch) => !ch.hidden);
}
return enriched;
})
.filter((menu) => !menu.hidden);
return {
...section,
items
};
}).filter((section) => section.items.length > 0);
}
function getBottomNavItems(session) {
const states = getStoredMenuStates();
return MENU_DEFINITIONS
.filter((menu) => menu.bottomNav)
.filter((menu) => isMenuAllowedForSession(menu, session))
.map((menu) => enrichMenu(menu, states))
.filter((menu) => !menu.hidden)
.sort((a, b) => (a.bottomNavOrder || 99) - (b.bottomNavOrder || 99));
}
function getConfigMenus() {
const states = getStoredMenuStates();
return MENU_DEFINITIONS.map((menu) => {
const section = SECTION_DEFINITIONS.find((s) => s.key === menu.section);
return {
...menu,
state: states[menu.key] || DEFAULT_MENU_STATES[menu.key] || STATE_VISIBLE,
roleLabel: menu.roles.includes('admin') && menu.roles.includes('cashier')
? 'Admin & Kasir'
: menu.roles.includes('cashier')
? 'Kasir'
: 'Admin',
sectionLabel: section?.labelDefault || menu.section,
sectionLabelKey: section?.labelKey || ''
};
});
}
function getMenuDefinition(key) {
const top = MENU_DEFINITIONS.find((menu) => menu.key === key);
if (top) return top;
for (const menu of MENU_DEFINITIONS) {
if (Array.isArray(menu.children)) {
const ch = menu.children.find((c) => c.key === key);
if (ch) return ch;
}
}
return null;
}
function isFeaturePasswordValid(password) {
return sha256(password) === getFeaturePasswordHash();
}
function evaluateMenuAccess(menuKey, session) {
const menu = getMenuDefinition(menuKey);
if (!menu) {
return { allowed: true, state: STATE_VISIBLE, menu: null };
}
if (!isMenuAllowedForSession(menu, session)) {
return { allowed: false, state: 'forbidden', menu, reason: 'forbidden' };
}
const states = getStoredMenuStates();
const state = states[menu.key] || DEFAULT_MENU_STATES[menu.key] || STATE_VISIBLE;
if (state === STATE_HIDDEN) {
return { allowed: false, state, menu, reason: 'hidden' };
}
if (state === STATE_LOCKED) {
return { allowed: false, state, menu, reason: 'locked' };
}
return { allowed: true, state, menu, reason: null };
}
module.exports = {
getFeatureContactPhone,
getFeaturePasswordHash,
STATE_VISIBLE,
STATE_HIDDEN,
STATE_LOCKED,
MENU_DEFINITIONS,
getSidebarSections,
getBottomNavItems,
getConfigMenus,
getMenuDefinition,
getStoredMenuStates,
sanitizeMenuStates,
isFeaturePasswordValid,
saveMenuStates,
evaluateMenuAccess,
};