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
1332 lines
48 KiB
JavaScript
1332 lines
48 KiB
JavaScript
/**
|
|
* Logika GenieACS yang dipakai portal web dan bot WhatsApp.
|
|
* Updated to support multi-server GenieACS setup.
|
|
*/
|
|
const axios = require('axios');
|
|
const db = require('../config/database');
|
|
const { getSettingsWithCache } = require('../config/settingsManager');
|
|
const auditTrail = require('./auditTrailService');
|
|
const { logger } = require('../config/logger');
|
|
const genieacsApi = require('../config/genieacs');
|
|
const mikrotikService = require('./mikrotikService');
|
|
|
|
// Helper: Search device across all servers (always get full data)
|
|
async function searchDeviceAcrossServers(query, fullData = true) {
|
|
try {
|
|
const servers = genieacsApi.getAllACSServers();
|
|
|
|
for (const server of servers) {
|
|
try {
|
|
const instance = genieacsApi.createAxiosInstance(server);
|
|
const params = {
|
|
query: JSON.stringify(query)
|
|
};
|
|
|
|
// Only add projection if explicitly requesting minimal data
|
|
if (!fullData) {
|
|
params.projection = '_id,_tags';
|
|
}
|
|
|
|
let response;
|
|
try {
|
|
response = await instance.get('/devices', {
|
|
params,
|
|
timeout: 2000
|
|
});
|
|
} catch (e) {
|
|
response = await instance.get('/api/devices', {
|
|
params,
|
|
timeout: 2000
|
|
});
|
|
}
|
|
|
|
if (response.data && response.data.length > 0) {
|
|
const device = response.data[0];
|
|
device._acs_server_id = server.id;
|
|
device._acs_server_name = server.name;
|
|
logger.debug(`[CustomerDevice] Device found on ${server.name}`);
|
|
return device;
|
|
}
|
|
} catch (error) {
|
|
logger.debug(`[CustomerDevice] Device not found on ${server.name}: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
return null;
|
|
} catch (error) {
|
|
logger.error(`[CustomerDevice] Error searching device: ${error.message}`);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
async function findDeviceByTag(tag) {
|
|
try {
|
|
const query = { $or: [{ _id: tag }, { _tags: tag }] };
|
|
// Get full data by default
|
|
return await searchDeviceAcrossServers(query, true);
|
|
} catch (e) {
|
|
logger.error(`[CustomerDevice] Error finding device by tag: ${e.message}`);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
async function findDeviceByPppoe(pppoeUser) {
|
|
try {
|
|
const user = String(pppoeUser || '').trim();
|
|
if (!user) return null;
|
|
const keys = [
|
|
'VirtualParameters.pppoeUsername',
|
|
'VirtualParameters.pppUsername',
|
|
...PPPOE_USER_KEYS
|
|
];
|
|
const query = { $or: keys.map(k => ({ [k]: user })) };
|
|
// Get full data by default
|
|
return await searchDeviceAcrossServers(query, true);
|
|
} catch (e) {
|
|
logger.error(`[CustomerDevice] Error finding device by PPPoE: ${e.message}`);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
async function fetchFullDevice(tag) {
|
|
try {
|
|
const query = { $or: [{ _id: tag }, { _tags: tag }] };
|
|
// Always get full data
|
|
return await searchDeviceAcrossServers(query, true);
|
|
} catch (e) {
|
|
logger.error(`[CustomerDevice] Error fetching full device: ${e.message}`);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
async function resolveDeviceToken(input) {
|
|
const token = String(input ?? '').replace(/[\r\n\t]+/g, '').trim();
|
|
if (!token) return null;
|
|
|
|
const direct = await findDeviceByTag(token);
|
|
if (direct && direct._id) return direct;
|
|
|
|
const byPppoe = await findDeviceByPppoe(token);
|
|
if (byPppoe && byPppoe._id) return byPppoe;
|
|
|
|
const found = await findDeviceWithTagVariants(token);
|
|
if (found && found.device && found.device._id) return found.device;
|
|
|
|
return null;
|
|
}
|
|
|
|
const parameterPaths = {
|
|
serialNumber: [
|
|
'DeviceID.SerialNumber',
|
|
'InternetGatewayDevice.DeviceInfo.SerialNumber',
|
|
'Device.DeviceInfo.SerialNumber'
|
|
],
|
|
model: [
|
|
'DeviceID.ProductClass',
|
|
'InternetGatewayDevice.DeviceInfo.ModelName',
|
|
'Device.DeviceInfo.ModelName',
|
|
'ModelName'
|
|
],
|
|
softwareVersion: [
|
|
'InternetGatewayDevice.DeviceInfo.SoftwareVersion',
|
|
'Device.DeviceInfo.SoftwareVersion'
|
|
],
|
|
ssid: [
|
|
'InternetGatewayDevice.LANDevice.1.WLANConfiguration.1.SSID',
|
|
'Device.WiFi.SSID.1.SSID',
|
|
'Device.WiFi.SSID.2.SSID'
|
|
],
|
|
rxPower: [
|
|
'VirtualParameters.RXPower',
|
|
'VirtualParameters.redaman',
|
|
'InternetGatewayDevice.WANDevice.1.WANPONInterfaceConfig.RXPower',
|
|
'InternetGatewayDevice.DeviceInfo.XponInterface.RXPower',
|
|
'InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.WANOAM.RXPower',
|
|
'InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.X_HW_OpticalSignal.RXPower',
|
|
'InternetGatewayDevice.WANDevice.1.X_GponInterfaceConfig.RXPower',
|
|
'InternetGatewayDevice.WANDevice.1.X_GponInterfaceConfig.RxPower',
|
|
'InternetGatewayDevice.WANDevice.1.X_GponInterafceConfig.RXPower',
|
|
'InternetGatewayDevice.WANDevice.1.X_GponInterafceConfig.RxPower',
|
|
'InternetGatewayDevice.WANDevice.1.X_ZTE_GponInterfaceConfig.RXPower',
|
|
'InternetGatewayDevice.WANDevice.1.X_ZTE_GponInterfaceConfig.RxPower',
|
|
'InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.X_ZTE_OpticalSignal.RXPower',
|
|
'InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.X_ZTE_OpticalSignal.RxPower',
|
|
'InternetGatewayDevice.WANDevice.1.X_HW_GponInterfaceConfig.RXPower',
|
|
'InternetGatewayDevice.WANDevice.1.X_HW_GponInterfaceConfig.RxPower',
|
|
'InternetGatewayDevice.WANDevice.1.X_ZTE-COM_WANPONInterfaceConfig.RXPower',
|
|
'InternetGatewayDevice.WANDevice.1.X_FH_GponInterfaceConfig.RXPower',
|
|
'InternetGatewayDevice.WANDevice.1.X_CMCC_EponInterfaceConfig.RXPower',
|
|
'InternetGatewayDevice.WANDevice.1.X_CMCC_GponInterfaceConfig.RXPower',
|
|
'InternetGatewayDevice.WANDevice.1.X_CT-COM_EponInterfaceConfig.RXPower',
|
|
'InternetGatewayDevice.WANDevice.1.X_CT-COM_GponInterfaceConfig.RXPower',
|
|
'InternetGatewayDevice.WANDevice.1.X_CU_WANEPONInterfaceConfig.OpticalTransceiver.RXPower',
|
|
'Device.Optical.Interface.1.OpticalSignalLevel',
|
|
'Device.XPON.Interface.1.Stats.RXPower'
|
|
],
|
|
pppoeIP: [
|
|
'VirtualParameters.pppoeIP',
|
|
'VirtualParameters.pppIP',
|
|
'InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.WANPPPConnection.1.ExternalIPAddress',
|
|
'InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.WANIPConnection.1.ExternalIPAddress',
|
|
'InternetGatewayDevice.WANDevice.1.WANConnectionDevice.2.WANPPPConnection.1.ExternalIPAddress',
|
|
'InternetGatewayDevice.WANDevice.1.WANConnectionDevice.2.WANIPConnection.1.ExternalIPAddress',
|
|
'InternetGatewayDevice.WANDevice.1.WANConnectionDevice.3.WANPPPConnection.1.ExternalIPAddress',
|
|
'InternetGatewayDevice.WANDevice.1.WANConnectionDevice.3.WANIPConnection.1.ExternalIPAddress',
|
|
'Device.PPP.Interface.1.ExternalIPAddress',
|
|
'Device.IP.Interface.1.IPv4Address.1.IPAddress'
|
|
],
|
|
pppUsername: [
|
|
'VirtualParameters.pppoeUsername',
|
|
'VirtualParameters.pppUsername',
|
|
'InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.WANPPPConnection.1.Username',
|
|
'InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.WANPPPConnection.2.Username',
|
|
'InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.WANPPPConnection.3.Username',
|
|
'InternetGatewayDevice.WANDevice.1.WANConnectionDevice.2.WANPPPConnection.1.Username',
|
|
'InternetGatewayDevice.WANDevice.1.WANConnectionDevice.2.WANPPPConnection.2.Username',
|
|
'InternetGatewayDevice.WANDevice.1.WANConnectionDevice.3.WANPPPConnection.1.Username',
|
|
'InternetGatewayDevice.WANDevice.1.WANConnectionDevice.3.WANPPPConnection.2.Username',
|
|
'Device.PPP.Interface.1.Username',
|
|
'Device.PPP.Interface.2.Username',
|
|
'Device.PPP.Interface.3.Username'
|
|
],
|
|
uptime: [
|
|
'VirtualParameters.getdeviceuptime',
|
|
'InternetGatewayDevice.DeviceInfo.UpTime',
|
|
'Device.DeviceInfo.UpTime'
|
|
],
|
|
userConnected: [
|
|
'InternetGatewayDevice.LANDevice.1.WLANConfiguration.1.TotalAssociations',
|
|
'InternetGatewayDevice.LANDevice.1.WLANConfiguration.5.TotalAssociations',
|
|
'InternetGatewayDevice.LANDevice.1.WLANConfiguration.1.AssociatedDeviceNumberOfEntries',
|
|
'InternetGatewayDevice.LANDevice.1.WLANConfiguration.5.AssociatedDeviceNumberOfEntries',
|
|
'InternetGatewayDevice.LANDevice.1.WLANConfiguration.1.Associations',
|
|
'InternetGatewayDevice.LANDevice.1.WLANConfiguration.5.Associations',
|
|
'InternetGatewayDevice.LANDevice.1.Hosts.HostNumberOfEntries',
|
|
'Device.WiFi.AccessPoint.1.AssociatedDeviceNumberOfEntries',
|
|
'Device.WiFi.AccessPoint.2.AssociatedDeviceNumberOfEntries',
|
|
'Device.Hosts.HostNumberOfEntries'
|
|
]
|
|
};
|
|
|
|
// PPPoE IP search keys matching user's template
|
|
const PPPOE_IP_KEYS = [
|
|
'InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.WANPPPConnection.1.ExternalIPAddress',
|
|
'InternetGatewayDevice.WANDevice.*.WANConnectionDevice.1.WANPPPConnection.2.ExternalIPAddress',
|
|
'InternetGatewayDevice.WANDevice.1.WANConnectionDevice.2.WANPPPConnection.1.ExternalIPAddress',
|
|
'InternetGatewayDevice.WANDevice.1.WANConnectionDevice.2.WANPPPConnection.2.ExternalIPAddress',
|
|
'InternetGatewayDevice.WANDevice.1.WANConnectionDevice.3.WANPPPConnection.1.ExternalIPAddress',
|
|
'InternetGatewayDevice.WANDevice.1.WANConnectionDevice.4.WANPPPConnection.1.ExternalIPAddress',
|
|
'InternetGatewayDevice.WANDevice.1.WANConnectionDevice.5.WANPPPConnection.1.ExternalIPAddress',
|
|
'InternetGatewayDevice.WANDevice.*.WANConnectionDevice.*.WANPPPConnection.*.ExternalIPAddress',
|
|
'Device.PPP.Interface.1.ExternalIPAddress',
|
|
'Device.IP.Interface.1.IPv4Address.1.IPAddress'
|
|
];
|
|
|
|
// PPPoE Username search keys matching user's template
|
|
const PPPOE_USER_KEYS = [
|
|
'InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.WANPPPConnection.1.Username',
|
|
'InternetGatewayDevice.WANDevice.*.WANConnectionDevice.1.WANPPPConnection.2.Username',
|
|
'InternetGatewayDevice.WANDevice.1.WANConnectionDevice.2.WANPPPConnection.1.Username',
|
|
'InternetGatewayDevice.WANDevice.1.WANConnectionDevice.2.WANPPPConnection.2.Username',
|
|
'InternetGatewayDevice.WANDevice.1.WANConnectionDevice.3.WANPPPConnection.1.Username',
|
|
'InternetGatewayDevice.WANDevice.1.WANConnectionDevice.3.WANPPPConnection.2.Username',
|
|
'InternetGatewayDevice.WANDevice.1.WANConnectionDevice.4.WANPPPConnection.1.Username',
|
|
'InternetGatewayDevice.WANDevice.1.WANConnectionDevice.4.WANPPPConnection.2.Username',
|
|
'InternetGatewayDevice.WANDevice.1.WANConnectionDevice.5.WANPPPConnection.1.Username',
|
|
'InternetGatewayDevice.WANDevice.*.WANConnectionDevice.*.WANPPPConnection.*.Username',
|
|
'Device.PPP.Interface.1.Username',
|
|
'Device.PPP.Interface.2.Username',
|
|
'Device.PPP.Interface.3.Username'
|
|
];
|
|
|
|
function getNestedValue(obj, path) {
|
|
try {
|
|
const parts = path.split('.');
|
|
let current = obj;
|
|
for (const part of parts) {
|
|
if (!current) return null;
|
|
current = current[part];
|
|
}
|
|
if (current && typeof current === 'object' && '_value' in current) {
|
|
return current._value;
|
|
}
|
|
if (current && typeof current === 'object' && current.hasOwnProperty('_value')) {
|
|
return current._value;
|
|
}
|
|
return current;
|
|
} catch (e) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function getWildcardMatches(device, path) {
|
|
const parts = path.split('.');
|
|
const results = [];
|
|
|
|
function recurse(current, index, currentPathParts) {
|
|
if (current === undefined || current === null) return;
|
|
|
|
if (index === parts.length) {
|
|
let val = current;
|
|
if (typeof current === 'object' && '_value' in current) {
|
|
val = current._value;
|
|
}
|
|
results.push({
|
|
path: currentPathParts.join('.'),
|
|
value: val
|
|
});
|
|
return;
|
|
}
|
|
|
|
const part = parts[index];
|
|
if (part === '*') {
|
|
if (typeof current === 'object') {
|
|
for (const key of Object.keys(current)) {
|
|
if (!key.startsWith('_')) {
|
|
recurse(current[key], index + 1, [...currentPathParts, key]);
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
if (typeof current === 'object') {
|
|
const targetLower = part.toLowerCase();
|
|
for (const key of Object.keys(current)) {
|
|
if (key.toLowerCase() === targetLower) {
|
|
recurse(current[key], index + 1, [...currentPathParts, key]);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
recurse(device, 0, []);
|
|
return results;
|
|
}
|
|
|
|
function getDeviceParameterValue(device, keys, filterFn) {
|
|
for (const key of keys) {
|
|
const matches = getWildcardMatches(device, key);
|
|
for (const match of matches) {
|
|
if (filterFn) {
|
|
if (filterFn(match.path, match.value, device)) {
|
|
return match.value;
|
|
}
|
|
} else if (match.value !== undefined && match.value !== null && match.value !== '') {
|
|
return match.value;
|
|
}
|
|
}
|
|
}
|
|
return '';
|
|
}
|
|
|
|
function extractPppoeIp(d) {
|
|
const ip = getDeviceParameterValue(d, PPPOE_IP_KEYS, (matchedPath, value, device) => {
|
|
if (!value || value === '0.0.0.0' || value === '-') return false;
|
|
|
|
if (matchedPath.includes('WANPPPConnection.')) {
|
|
const connectionTypePath = matchedPath.replace('ExternalIPAddress', 'ConnectionType');
|
|
const connTypeMatches = getWildcardMatches(device, connectionTypePath);
|
|
if (connTypeMatches.length > 0 && connTypeMatches[0].value === 'bridge') {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
});
|
|
|
|
if (ip) return ip;
|
|
if (d._ip && d._ip !== '-' && d._ip !== '0.0.0.0') return d._ip;
|
|
return 'N/A';
|
|
}
|
|
|
|
function extractPppoeUser(d) {
|
|
const user = getDeviceParameterValue(d, PPPOE_USER_KEYS, (matchedPath, value, device) => {
|
|
if (!value || value === '-') return false;
|
|
|
|
if (matchedPath.includes('WANPPPConnection.')) {
|
|
const connectionTypePath = matchedPath.replace('Username', 'ConnectionType');
|
|
const connTypeMatches = getWildcardMatches(device, connectionTypePath);
|
|
if (connTypeMatches.length > 0 && connTypeMatches[0].value === 'PPPoE_Bridged') {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
});
|
|
|
|
return user || 'N/A';
|
|
}
|
|
|
|
function formatUptime(seconds) {
|
|
if (!seconds || seconds === 'N/A' || seconds === '-') return seconds || 'N/A';
|
|
if (typeof seconds === 'string' && (seconds.includes('d') || seconds.includes(':')) && isNaN(seconds)) {
|
|
return seconds;
|
|
}
|
|
const totalSecs = parseInt(seconds, 10);
|
|
if (isNaN(totalSecs)) return seconds || 'N/A';
|
|
const days = Math.floor(totalSecs / 86400);
|
|
const rem = totalSecs % 86400;
|
|
let hrs = Math.floor(rem / 3600);
|
|
if (hrs < 10) hrs = "0" + hrs;
|
|
const rem2 = rem % 3600;
|
|
let mins = Math.floor(rem2 / 60);
|
|
if (mins < 10) mins = "0" + mins;
|
|
let secs = rem2 % 60;
|
|
if (secs < 10) secs = "0" + secs;
|
|
return days + "d " + hrs + ":" + mins + ":" + secs;
|
|
}
|
|
|
|
function formatDeviceTimestamp(value) {
|
|
if (!value) return '-';
|
|
const d = new Date(value);
|
|
if (isNaN(d.getTime())) return '-';
|
|
return d.toLocaleString('id-ID');
|
|
}
|
|
|
|
function formatRelativeTime(value) {
|
|
if (!value) return '-';
|
|
const ts = new Date(value).getTime();
|
|
if (!Number.isFinite(ts)) return '-';
|
|
const diffMs = Date.now() - ts;
|
|
if (diffMs < 0) return 'baru saja';
|
|
const sec = Math.floor(diffMs / 1000);
|
|
if (sec < 60) return `${sec} detik lalu`;
|
|
const min = Math.floor(sec / 60);
|
|
if (min < 60) return `${min} menit lalu`;
|
|
const hour = Math.floor(min / 60);
|
|
if (hour < 24) return `${hour} jam lalu`;
|
|
const day = Math.floor(hour / 24);
|
|
return `${day} hari lalu`;
|
|
}
|
|
|
|
function collectRefreshObjects(device) {
|
|
const objects = [];
|
|
const isTr181 = !!device?.Device;
|
|
|
|
if (isTr181) {
|
|
// TR-181 Device - validate before pushing
|
|
if (device?.Device?.Hosts?.Host) {
|
|
objects.push('Device.Hosts.Host');
|
|
}
|
|
if (device?.Device?.WiFi?.AccessPoint?.['1']) {
|
|
objects.push('Device.WiFi.AccessPoint.1.AssociatedDevice');
|
|
}
|
|
if (device?.Device?.WiFi?.AccessPoint?.['2']) {
|
|
objects.push('Device.WiFi.AccessPoint.2.AssociatedDevice');
|
|
}
|
|
} else {
|
|
// TR-098 Device (InternetGatewayDevice) - validate before pushing
|
|
// FIX for Error 9005: Only query objects that exist in device cache
|
|
// Some devices (e.g. ZTE GM220-S XPON) don't have LANDevice/WiFi support
|
|
if (device?.InternetGatewayDevice?.LANDevice?.['1']?.Hosts?.Host) {
|
|
objects.push('InternetGatewayDevice.LANDevice.1.Hosts.Host');
|
|
}
|
|
if (device?.InternetGatewayDevice?.LANDevice?.['1']?.WLANConfiguration?.['1']?.AssociatedDevice) {
|
|
objects.push('InternetGatewayDevice.LANDevice.1.WLANConfiguration.1.AssociatedDevice');
|
|
}
|
|
if (device?.InternetGatewayDevice?.LANDevice?.['1']?.WLANConfiguration?.['5']) {
|
|
objects.push('InternetGatewayDevice.LANDevice.1.WLANConfiguration.5.AssociatedDevice');
|
|
}
|
|
}
|
|
|
|
return Array.from(new Set(objects));
|
|
}
|
|
|
|
function getBuiltinSyncState(deviceId) {
|
|
const empty = {
|
|
syncInProgress: false,
|
|
syncPendingCount: 0,
|
|
syncLastQueueAt: '',
|
|
syncLastQueueLabel: '-',
|
|
syncStatusLabel: 'Idle'
|
|
};
|
|
if (!deviceId) return empty;
|
|
|
|
try {
|
|
const pending = db.prepare(
|
|
`SELECT COUNT(*) AS count, MAX(created_at) AS last_queue_at
|
|
FROM acs_tasks
|
|
WHERE device_id = ?
|
|
AND name IN ('refreshObject', 'getParameterValues', 'getParameterNames')
|
|
AND status IN ('pending', 'in_progress')`
|
|
).get(deviceId);
|
|
|
|
const count = Number(pending?.count || 0);
|
|
const lastQueueAt = String(pending?.last_queue_at || '');
|
|
return {
|
|
syncInProgress: count > 0,
|
|
syncPendingCount: count,
|
|
syncLastQueueAt: lastQueueAt,
|
|
syncLastQueueLabel: formatDeviceTimestamp(lastQueueAt),
|
|
syncStatusLabel: count > 0 ? `Sinkronisasi berjalan (${count})` : 'Idle'
|
|
};
|
|
} catch (e) {
|
|
logger.debug(`[CustomerDevice] Failed reading ACS sync state for ${deviceId}: ${e.message}`);
|
|
return empty;
|
|
}
|
|
}
|
|
|
|
const PPPOE_UPTIME_KEYS = [
|
|
'InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.WANPPPConnection.1.Uptime',
|
|
'InternetGatewayDevice.WANDevice.*.WANConnectionDevice.1.WANPPPConnection.2.Uptime',
|
|
'InternetGatewayDevice.WANDevice.1.WANConnectionDevice.2.WANPPPConnection.1.Uptime',
|
|
'InternetGatewayDevice.WANDevice.1.WANConnectionDevice.2.WANPPPConnection.2.Uptime',
|
|
'InternetGatewayDevice.WANDevice.1.WANConnectionDevice.3.WANPPPConnection.1.Uptime',
|
|
'InternetGatewayDevice.WANDevice.1.WANConnectionDevice.4.WANPPPConnection.1.Uptime',
|
|
'InternetGatewayDevice.WANDevice.1.WANConnectionDevice.5.WANPPPConnection.1.Uptime',
|
|
'InternetGatewayDevice.WANDevice.*.WANConnectionDevice.*.WANPPPConnection.*.Uptime',
|
|
'Device.PPP.Interface.1.UpTime'
|
|
];
|
|
|
|
function extractPppoeUptime(d) {
|
|
let uptimeVal = getDeviceParameterValue(d, PPPOE_UPTIME_KEYS, (matchedPath, value, device) => {
|
|
if (value === undefined || value === null || value === '' || value === '-') return false;
|
|
|
|
if (matchedPath.toLowerCase().includes('wanpppconnection')) {
|
|
const connTypePath = matchedPath.substring(0, matchedPath.toLowerCase().lastIndexOf('.uptime')) + '.ConnectionType';
|
|
const connTypeMatches = getWildcardMatches(device, connTypePath);
|
|
if (connTypeMatches.length > 0 && connTypeMatches[0].value === 'PPPoE_Bridged') {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
});
|
|
|
|
if (!uptimeVal || uptimeVal === '-') {
|
|
const UPTIME_PATHS = [
|
|
'VirtualParameters.getdeviceuptime',
|
|
'InternetGatewayDevice.DeviceInfo.UpTime',
|
|
'Device.DeviceInfo.UpTime'
|
|
];
|
|
for (const path of UPTIME_PATHS) {
|
|
const val = getNestedValue(d, path);
|
|
if (val && val !== '-' && val !== '') {
|
|
uptimeVal = val;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (uptimeVal) {
|
|
return formatUptime(uptimeVal);
|
|
}
|
|
return 'N/A';
|
|
}
|
|
|
|
function getParameterWithPaths(device, paths) {
|
|
let values = [];
|
|
for (const p of paths) {
|
|
const parts = p.split('.');
|
|
let value = device;
|
|
for (const part of parts) {
|
|
if (value && typeof value === 'object' && part in value) {
|
|
value = value[part];
|
|
if (value && value._value !== undefined) value = value._value;
|
|
} else {
|
|
value = undefined;
|
|
break;
|
|
}
|
|
}
|
|
if (value !== undefined && value !== null && value !== '' && value !== 'N/A') {
|
|
const isIpPath = p.toLowerCase().includes('ipaddress') || p.toLowerCase().includes('pppoeip') || p.toLowerCase().includes('pppip') || p.toLowerCase().includes('pppusername') || p.toLowerCase().includes('pppoeusername');
|
|
if (isIpPath && String(value) === '0.0.0.0') {
|
|
continue;
|
|
}
|
|
const isCountParam = p.includes('TotalAssociations') ||
|
|
p.includes('AssociatedDeviceNumberOfEntries') ||
|
|
p.includes('HostNumberOfEntries');
|
|
|
|
if (isCountParam) {
|
|
// Ensure we push a number
|
|
const val = (typeof value === 'object' && value._value !== undefined) ? value._value : value;
|
|
values.push(parseInt(val) || 0);
|
|
} else {
|
|
// If it's still an object, try to get _value or stringify it
|
|
if (typeof value === 'object') {
|
|
if (value._value !== undefined) return String(value._value);
|
|
return 'N/A'; // Don't return raw object
|
|
}
|
|
return String(value);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (values.length > 0) {
|
|
// If it's a count parameter, sum them up (for dual band)
|
|
return values.reduce((a, b) => a + b, 0);
|
|
}
|
|
|
|
return 'N/A';
|
|
}
|
|
|
|
function expandTagCandidates(input) {
|
|
const t = String(input || '').trim();
|
|
if (!t) return [];
|
|
if (/^\d+$/.test(t)) {
|
|
const d = t.replace(/\D/g, '');
|
|
const set = new Set([d]);
|
|
if (d.startsWith('62') && d.length > 2) set.add('0' + d.slice(2));
|
|
if (d.startsWith('0')) set.add('62' + d.slice(1));
|
|
return [...set];
|
|
}
|
|
return [t];
|
|
}
|
|
|
|
/** Coba beberapa varian tag (62/0 untuk nomor) sampai device ketemu */
|
|
async function findDeviceWithTagVariants(input) {
|
|
for (const c of expandTagCandidates(input)) {
|
|
const dev = await findDeviceByTag(c);
|
|
if (dev) return { device: dev, canonicalTag: c };
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/** Nomor dari JID WhatsApp @s.whatsapp.net */
|
|
function phoneFromPnJid(jid) {
|
|
if (!jid || typeof jid !== 'string') return null;
|
|
const [user, host] = jid.split('@');
|
|
if (!user || host !== 's.whatsapp.net') return null;
|
|
return user.replace(/\D/g, '') || null;
|
|
}
|
|
|
|
function mapDeviceData(device, tag, isPppoeActive = false) {
|
|
if (!device) return null;
|
|
|
|
const ssid = getParameterWithPaths(device, parameterPaths.ssid);
|
|
const ssidDisplay = ssid === 'N/A' ? '-' : ssid;
|
|
|
|
const lastInformRaw =
|
|
device?._lastInform ||
|
|
device?.Events?.Inform ||
|
|
device?.InternetGatewayDevice?.DeviceInfo?.['1']?.LastInform?._value ||
|
|
'';
|
|
const lastInform = formatDeviceTimestamp(lastInformRaw);
|
|
const lastSyncRaw = device?._updatedAt || lastInformRaw || '';
|
|
const lastSync = formatDeviceTimestamp(lastSyncRaw);
|
|
const syncState = getBuiltinSyncState(device?._id);
|
|
|
|
let status = 'Unknown';
|
|
if (device?._lastInform) {
|
|
const diffMs = Date.now() - new Date(device._lastInform).getTime();
|
|
status = diffMs < 15 * 60 * 1000 ? 'Online' : 'Offline';
|
|
} else if (device?.Events?.Inform) {
|
|
const diffMs = Date.now() - new Date(device.Events.Inform).getTime();
|
|
status = diffMs < 15 * 60 * 1000 ? 'Online' : 'Offline';
|
|
}
|
|
|
|
if (status !== 'Online' && isPppoeActive) {
|
|
status = 'Online';
|
|
}
|
|
|
|
let connectedUsers = [];
|
|
try {
|
|
const hosts = device?.InternetGatewayDevice?.LANDevice?.['1']?.Hosts?.Host || device?.Device?.Hosts?.Host;
|
|
if (hosts && typeof hosts === 'object') {
|
|
for (const key in hosts) {
|
|
if (!isNaN(key)) {
|
|
const entry = hosts[key];
|
|
connectedUsers.push({
|
|
hostname: typeof entry?.HostName === 'object' ? entry?.HostName?._value || '-' : entry?.HostName || '-',
|
|
ip: typeof entry?.IPAddress === 'object' ? entry?.IPAddress?._value || '-' : entry?.IPAddress || '-',
|
|
mac: typeof entry?.MACAddress === 'object' ? entry?.MACAddress?._value || '-' : entry?.MACAddress || '-',
|
|
iface: typeof entry?.InterfaceType === 'object' ? entry?.InterfaceType?._value || '-' : entry?.InterfaceType || entry?.Interface || '-',
|
|
status: (
|
|
entry?.Active?._value === 'true' ||
|
|
entry?.Active?._value === '1' ||
|
|
entry?.Active?._value === 1 ||
|
|
entry?.Active === true ||
|
|
entry?.Active === '1' ||
|
|
entry?.Active === 1 ||
|
|
String(entry?.Active || '').toLowerCase() === 'online'
|
|
) ? 'Online' : 'Offline'
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
const assocCandidates = [
|
|
device?.InternetGatewayDevice?.LANDevice?.['1']?.WLANConfiguration?.['1']?.AssociatedDevice,
|
|
device?.InternetGatewayDevice?.LANDevice?.['1']?.WLANConfiguration?.['5']?.AssociatedDevice,
|
|
device?.Device?.WiFi?.AccessPoint?.['1']?.AssociatedDevice,
|
|
device?.Device?.WiFi?.AccessPoint?.['2']?.AssociatedDevice
|
|
];
|
|
for (const assoc of assocCandidates) {
|
|
if (!assoc || typeof assoc !== 'object') continue;
|
|
for (const key in assoc) {
|
|
if (key === '_value' || key === '_type' || key === '_timestamp') continue;
|
|
if (isNaN(key)) continue;
|
|
const entry = assoc[key];
|
|
const mac =
|
|
(typeof entry?.MACAddress === 'object' ? entry?.MACAddress?._value : entry?.MACAddress) ||
|
|
(typeof entry?.AssociatedDeviceMACAddress === 'object' ? entry?.AssociatedDeviceMACAddress?._value : entry?.AssociatedDeviceMACAddress) ||
|
|
'-';
|
|
const ip =
|
|
(typeof entry?.IPAddress === 'object' ? entry?.IPAddress?._value : entry?.IPAddress) ||
|
|
(typeof entry?.IP === 'object' ? entry?.IP?._value : entry?.IP) ||
|
|
'-';
|
|
const hostname =
|
|
(typeof entry?.HostName === 'object' ? entry?.HostName?._value : entry?.HostName) ||
|
|
(typeof entry?.DeviceName === 'object' ? entry?.DeviceName?._value : entry?.DeviceName) ||
|
|
'-';
|
|
const ssidName =
|
|
(typeof entry?.SSID === 'object' ? entry?.SSID?._value : entry?.SSID) ||
|
|
(typeof entry?.WLANConfiguration === 'object' ? entry?.WLANConfiguration?._value : entry?.WLANConfiguration) ||
|
|
'';
|
|
if (!mac || mac === '-') continue;
|
|
connectedUsers.push({
|
|
hostname: hostname || '-',
|
|
ip: ip || '-',
|
|
mac: mac || '-',
|
|
iface: ssidName ? `WiFi ${ssidName}` : 'WiFi',
|
|
status: 'Online'
|
|
});
|
|
}
|
|
}
|
|
} catch (e) {}
|
|
|
|
try {
|
|
const seen = new Set();
|
|
connectedUsers = connectedUsers.filter((u) => {
|
|
const mac = String(u?.mac || '').toUpperCase();
|
|
const ip = String(u?.ip || '');
|
|
const key = mac ? `${mac}|${ip}` : ip;
|
|
if (!key) return false;
|
|
if (seen.has(key)) return false;
|
|
seen.add(key);
|
|
return true;
|
|
});
|
|
} catch {}
|
|
|
|
let rxPower = getParameterWithPaths(device, parameterPaths.rxPower);
|
|
if (rxPower !== 'N/A' && rxPower !== '-' && rxPower !== '') {
|
|
const num = parseFloat(rxPower);
|
|
if (!isNaN(num) && num > 0) {
|
|
const dbVal = 30 + (Math.log10(num * Math.pow(10, -7)) * 10);
|
|
rxPower = (Math.ceil(dbVal * 100) / 100).toFixed(2);
|
|
}
|
|
}
|
|
const pppoeIP = extractPppoeIp(device);
|
|
const pppoeUsername = extractPppoeUser(device);
|
|
const uptimeRaw = getParameterWithPaths(device, parameterPaths.uptime);
|
|
let totalAssociations = getParameterWithPaths(device, parameterPaths.userConnected);
|
|
|
|
// Fallback: If N/A or 0, count from connectedUsers list (LAN + WLAN)
|
|
if ((totalAssociations === 'N/A' || totalAssociations === 0 || totalAssociations === '0') && connectedUsers.length > 0) {
|
|
totalAssociations = connectedUsers.filter(u => u.status === 'Online').length;
|
|
}
|
|
if (totalAssociations === 'N/A' || totalAssociations === '-' || totalAssociations === '' || totalAssociations === null || totalAssociations === undefined) {
|
|
totalAssociations = 0;
|
|
}
|
|
if (typeof totalAssociations === 'string' && /^\d+$/.test(totalAssociations)) {
|
|
totalAssociations = parseInt(totalAssociations, 10);
|
|
}
|
|
|
|
function formatUptime(seconds) {
|
|
if (!seconds || seconds === 'N/A' || seconds === '-') return seconds || 'N/A';
|
|
if (typeof seconds === 'string' && (seconds.includes('d') || seconds.includes(':')) && isNaN(seconds)) {
|
|
return seconds;
|
|
}
|
|
const totalSecs = parseInt(seconds, 10);
|
|
if (isNaN(totalSecs)) return seconds || 'N/A';
|
|
const days = Math.floor(totalSecs / 86400);
|
|
const rem = totalSecs % 86400;
|
|
|
|
let hrs = Math.floor(rem / 3600);
|
|
if (hrs < 10) hrs = "0" + hrs;
|
|
|
|
const rem2 = rem % 3600;
|
|
let mins = Math.floor(rem2 / 60);
|
|
if (mins < 10) mins = "0" + mins;
|
|
|
|
let secs = rem2 % 60;
|
|
if (secs < 10) secs = "0" + secs;
|
|
|
|
return days + "d " + hrs + ":" + mins + ":" + secs;
|
|
}
|
|
const uptime = formatUptime(uptimeRaw);
|
|
const pppoeUptime = extractPppoeUptime(device);
|
|
|
|
const serialNumber = getParameterWithPaths(device, parameterPaths.serialNumber);
|
|
const productClass = getParameterWithPaths(device, parameterPaths.model);
|
|
const softwareVersion = getParameterWithPaths(device, parameterPaths.softwareVersion);
|
|
const model = productClass;
|
|
|
|
let lokasi = device?._tags || '-';
|
|
if (Array.isArray(lokasi)) lokasi = lokasi.join(', ');
|
|
|
|
// Suhu ONT (dari parameter ACS bila tersedia)
|
|
let temperature = '-';
|
|
try {
|
|
const _db = require('../config/database');
|
|
const _trow = _db.prepare('SELECT params FROM acs_devices WHERE id = ?').get(device._id);
|
|
const _tflat = _trow && _trow.params ? JSON.parse(_trow.params) : null;
|
|
if (_tflat) {
|
|
const _tkey = Object.keys(_tflat).find(k => /temp/i.test(k));
|
|
if (_tkey != null) {
|
|
let _tv = _tflat[_tkey];
|
|
if (_tv && typeof _tv === 'object') _tv = (_tv._value != null ? _tv._value : _tv.value);
|
|
const _tn = Number(_tv);
|
|
if (Number.isFinite(_tn) && _tn > 0) temperature = (_tn > 200 ? (_tn / 100).toFixed(1) : String(_tn)) + '°C';
|
|
}
|
|
}
|
|
} catch (e) {}
|
|
|
|
return {
|
|
phone: tag,
|
|
ssid: ssidDisplay,
|
|
temperature,
|
|
status,
|
|
lastInform,
|
|
lastInformRaw: lastInformRaw || '',
|
|
lastInformAgo: formatRelativeTime(lastInformRaw),
|
|
lastSync,
|
|
lastSyncRaw: lastSyncRaw || '',
|
|
lastSyncAgo: formatRelativeTime(lastSyncRaw),
|
|
syncInProgress: !!syncState.syncInProgress,
|
|
syncPendingCount: Number(syncState.syncPendingCount || 0),
|
|
syncLastQueueAt: syncState.syncLastQueueAt || '',
|
|
syncLastQueueLabel: syncState.syncLastQueueLabel || '-',
|
|
syncStatusLabel: syncState.syncStatusLabel || 'Idle',
|
|
connectedUsers,
|
|
rxPower: rxPower === 'N/A' ? '-' : rxPower,
|
|
pppoeIP: pppoeIP === 'N/A' ? '-' : pppoeIP,
|
|
pppoeUsername: pppoeUsername === 'N/A' ? '-' : pppoeUsername,
|
|
pppoeUptime: pppoeUptime === 'N/A' ? '-' : pppoeUptime,
|
|
serialNumber: serialNumber === 'N/A' ? '-' : serialNumber,
|
|
productClass: productClass === 'N/A' ? '-' : productClass,
|
|
lokasi,
|
|
softwareVersion: softwareVersion === 'N/A' ? '-' : softwareVersion,
|
|
model: model === 'N/A' ? '-' : model,
|
|
uptime: uptime === 'N/A' ? '-' : uptime,
|
|
totalAssociations
|
|
};
|
|
}
|
|
|
|
async function getCustomerDeviceData(tag) {
|
|
const base = await resolveDeviceToken(tag);
|
|
if (!base || !base._id) return null;
|
|
const device = await fetchFullDevice(base._id);
|
|
|
|
let isPppoeActive = false;
|
|
try {
|
|
const pppoeUser = extractPppoeUser(device);
|
|
if (pppoeUser && pppoeUser !== 'N/A' && pppoeUser !== '-') {
|
|
const activeSessionsMap = await mikrotikService.getActivePppoeSessionsMap().catch(() => new Map());
|
|
if (activeSessionsMap.has(pppoeUser.toLowerCase())) {
|
|
isPppoeActive = true;
|
|
}
|
|
}
|
|
} catch (e) {}
|
|
|
|
return mapDeviceData(device, tag, isPppoeActive);
|
|
}
|
|
|
|
function fallbackCustomer(tag) {
|
|
return {
|
|
phone: tag,
|
|
ssid: '-',
|
|
status: 'Tidak ditemukan',
|
|
lastInform: '-',
|
|
lastInformAgo: '-',
|
|
lastSync: '-',
|
|
lastSyncAgo: '-',
|
|
syncInProgress: false,
|
|
syncPendingCount: 0,
|
|
syncLastQueueLabel: '-',
|
|
syncStatusLabel: 'Idle',
|
|
connectedUsers: [],
|
|
rxPower: '-',
|
|
pppoeIP: '-',
|
|
pppoeUsername: '-',
|
|
pppoeUptime: '-',
|
|
serialNumber: '-',
|
|
productClass: '-',
|
|
lokasi: '-',
|
|
softwareVersion: '-',
|
|
model: '-',
|
|
uptime: '-',
|
|
totalAssociations: '-'
|
|
};
|
|
}
|
|
|
|
async function updateSSID(tag, newSSID, actor = null) {
|
|
try {
|
|
const ssidClean = String(newSSID ?? '').replace(/[\r\n\t]+/g, '').trim();
|
|
if (!ssidClean) { logger.warn('[updateSSID] SSID kosong, dibatalkan.'); return false; }
|
|
const device = await resolveDeviceToken(tag);
|
|
if (!device) return false;
|
|
const deviceId = encodeURIComponent(device._id);
|
|
|
|
// Gunakan server yang sesuai
|
|
const server = device._acs_server_id ? genieacsApi.getACSServer(device._acs_server_id) : genieacsApi.getACSServer('legacy');
|
|
if (!server) return false;
|
|
|
|
const instance = genieacsApi.createAxiosInstance(server);
|
|
const tasksUrl = `/devices/${deviceId}/tasks`;
|
|
|
|
const parameterValues = [];
|
|
|
|
// Check supported paths in DB
|
|
const db = require('../config/database');
|
|
const row = db.prepare('SELECT params FROM acs_devices WHERE id = ?').get(device._id);
|
|
const flatParams = row && row.params ? JSON.parse(row.params) : null;
|
|
|
|
if (flatParams) {
|
|
// SSID 2.4G paths
|
|
const paths24G = [
|
|
'InternetGatewayDevice.LANDevice.1.WLANConfiguration.1.SSID',
|
|
'Device.WiFi.SSID.1.SSID'
|
|
];
|
|
paths24G.forEach(p => {
|
|
if (flatParams[p] !== undefined) {
|
|
parameterValues.push([p, ssidClean, 'xsd:string']);
|
|
}
|
|
});
|
|
|
|
// SSID 5G paths
|
|
const paths5G = [
|
|
'Device.WiFi.SSID.2.SSID'
|
|
];
|
|
for (const idx of [5, 6, 7, 8]) {
|
|
paths5G.push(`InternetGatewayDevice.LANDevice.1.WLANConfiguration.${idx}.SSID`);
|
|
}
|
|
paths5G.forEach(p => {
|
|
if (flatParams[p] !== undefined) {
|
|
parameterValues.push([p, `${ssidClean}-5G`, 'xsd:string']);
|
|
}
|
|
});
|
|
}
|
|
|
|
// Fallback if no parameters match or device not bootstrapped yet
|
|
if (parameterValues.length === 0) {
|
|
parameterValues.push(
|
|
['InternetGatewayDevice.LANDevice.1.WLANConfiguration.1.SSID', ssidClean, 'xsd:string'],
|
|
['Device.WiFi.SSID.1.SSID', ssidClean, 'xsd:string']
|
|
);
|
|
}
|
|
|
|
let ok = false;
|
|
try {
|
|
await instance.post(tasksUrl, {
|
|
name: 'setParameterValues',
|
|
parameterValues: parameterValues
|
|
}, { timeout: 20000 });
|
|
ok = true;
|
|
} catch (e) {
|
|
logger.error(`[updateSSID] Failed to set SSID: ${e.message}`);
|
|
}
|
|
|
|
// Refresh + inform dijalankan di latar belakang agar respons cepat (tidak menahan UI)
|
|
if (ok) {
|
|
instance.post(tasksUrl, { name: 'refreshObject', objectName: 'InternetGatewayDevice.LANDevice.1.WLANConfiguration' }, { timeout: 12000 }).catch(() => {});
|
|
instance.post(tasksUrl, { name: 'inform' }, { timeout: 12000 }).catch(() => {});
|
|
logger.info(`[updateSSID] Set SSID dikirim untuk ${tag}; refresh & inform berjalan di latar belakang`);
|
|
}
|
|
|
|
// Catat audit trail jika berhasil
|
|
if (ok && actor) {
|
|
auditTrail.logAuditTrail({
|
|
action: 'UPDATE_SSID',
|
|
entity_type: 'device',
|
|
entity_id: tag,
|
|
actor_type: actor.type || 'unknown',
|
|
actor_id: actor.id || null,
|
|
actor_name: actor.name || null,
|
|
details: {
|
|
oldSSID: device._id || 'unknown',
|
|
newSSID: newSSID
|
|
},
|
|
ip_address: actor.ip || null,
|
|
user_agent: actor.userAgent || null
|
|
});
|
|
}
|
|
|
|
return ok;
|
|
} catch (e) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
async function updatePassword(tag, newPassword, actor = null) {
|
|
try {
|
|
const pwRaw = String(newPassword ?? '');
|
|
const pw = pwRaw.replace(/[\r\n\t]+/g, '').trim();
|
|
if (pw.length < 8) {
|
|
logger.warn(`[updatePassword] Password too short for tag ${tag}`);
|
|
return false;
|
|
}
|
|
const device = await resolveDeviceToken(tag);
|
|
if (!device) {
|
|
logger.warn(`[updatePassword] Device not found for tag ${tag}`);
|
|
return false;
|
|
}
|
|
const deviceId = encodeURIComponent(device._id);
|
|
|
|
// Gunakan server yang sesuai
|
|
const server = device._acs_server_id ? genieacsApi.getACSServer(device._acs_server_id) : genieacsApi.getACSServer('legacy');
|
|
if (!server) return false;
|
|
|
|
const instance = genieacsApi.createAxiosInstance(server);
|
|
const tasksUrl = `/devices/${deviceId}/tasks`;
|
|
|
|
logger.info(`[updatePassword] Setting password for device ${deviceId}, tag ${tag}`);
|
|
|
|
const parameterValues = [];
|
|
|
|
// Check supported paths in DB
|
|
const db = require('../config/database');
|
|
const row = db.prepare('SELECT params FROM acs_devices WHERE id = ?').get(device._id);
|
|
const flatParams = row && row.params ? JSON.parse(row.params) : null;
|
|
|
|
if (flatParams) {
|
|
// 2.4G password paths
|
|
const paths24G = [
|
|
'InternetGatewayDevice.LANDevice.1.WLANConfiguration.1.KeyPassphrase',
|
|
'InternetGatewayDevice.LANDevice.1.WLANConfiguration.1.PreSharedKey.1.KeyPassphrase',
|
|
'InternetGatewayDevice.LANDevice.1.WLANConfiguration.1.PreSharedKey.1.PreSharedKey',
|
|
'Device.WiFi.AccessPoint.1.Security.KeyPassphrase',
|
|
'Device.WiFi.AccessPoint.1.Security.PreSharedKey'
|
|
];
|
|
paths24G.forEach(p => {
|
|
if (flatParams[p] !== undefined) {
|
|
parameterValues.push([p, pw, 'xsd:string']);
|
|
}
|
|
});
|
|
|
|
// 5G password paths
|
|
const paths5G = [
|
|
'Device.WiFi.AccessPoint.2.Security.KeyPassphrase',
|
|
'Device.WiFi.AccessPoint.2.Security.PreSharedKey'
|
|
];
|
|
for (const idx of [5, 6, 7, 8]) {
|
|
paths5G.push(
|
|
`InternetGatewayDevice.LANDevice.1.WLANConfiguration.${idx}.KeyPassphrase`,
|
|
`InternetGatewayDevice.LANDevice.1.WLANConfiguration.${idx}.PreSharedKey.1.KeyPassphrase`,
|
|
`InternetGatewayDevice.LANDevice.1.WLANConfiguration.${idx}.PreSharedKey.1.PreSharedKey`
|
|
);
|
|
}
|
|
paths5G.forEach(p => {
|
|
if (flatParams[p] !== undefined) {
|
|
parameterValues.push([p, pw, 'xsd:string']);
|
|
}
|
|
});
|
|
}
|
|
|
|
// Fallback if no parameters match or device not bootstrapped yet
|
|
if (parameterValues.length === 0) {
|
|
parameterValues.push(
|
|
['InternetGatewayDevice.LANDevice.1.WLANConfiguration.1.KeyPassphrase', pw, 'xsd:string'],
|
|
['InternetGatewayDevice.LANDevice.1.WLANConfiguration.1.PreSharedKey.1.KeyPassphrase', pw, 'xsd:string'],
|
|
['InternetGatewayDevice.LANDevice.1.WLANConfiguration.1.PreSharedKey.1.PreSharedKey', pw, 'xsd:string'],
|
|
['Device.WiFi.AccessPoint.1.Security.KeyPassphrase', pw, 'xsd:string']
|
|
);
|
|
}
|
|
|
|
let ok = false;
|
|
try {
|
|
await instance.post(tasksUrl, {
|
|
name: 'setParameterValues',
|
|
parameterValues: parameterValues
|
|
}, { timeout: 20000 });
|
|
ok = true;
|
|
} catch (e) {
|
|
logger.error(`[updatePassword] Failed to set password: ${e.message}`);
|
|
}
|
|
|
|
// Refresh di latar belakang agar respons cepat
|
|
instance.post(tasksUrl, { name: 'refreshObject', objectName: 'InternetGatewayDevice.LANDevice.1.WLANConfiguration' }, { timeout: 12000 }).catch(() => {});
|
|
// Skip Device.WiFi.AccessPoint refresh karena tidak semua ONU support (CIOT tidak support)
|
|
|
|
// Catat audit trail jika berhasil
|
|
if (ok && actor) {
|
|
auditTrail.logAuditTrail({
|
|
action: 'UPDATE_PASSWORD',
|
|
entity_type: 'device',
|
|
entity_id: tag,
|
|
actor_type: actor.type || 'unknown',
|
|
actor_id: actor.id || null,
|
|
actor_name: actor.name || null,
|
|
details: {
|
|
device_id: deviceId
|
|
},
|
|
ip_address: actor.ip || null,
|
|
user_agent: actor.userAgent || null
|
|
});
|
|
}
|
|
|
|
return ok;
|
|
} catch (e) {
|
|
logger.error(`[updatePassword] Error: ${e.message}`, e.response?.data || '');
|
|
return false;
|
|
}
|
|
}
|
|
|
|
async function requestRefresh(tag, actor = null) {
|
|
try {
|
|
const device = await resolveDeviceToken(tag);
|
|
if (!device || !device._id) {
|
|
return { ok: false, message: 'Perangkat tidak ditemukan.' };
|
|
}
|
|
|
|
const server = device._acs_server_id
|
|
? genieacsApi.getACSServer(device._acs_server_id)
|
|
: genieacsApi.getACSServer('legacy');
|
|
if (!server) {
|
|
return { ok: false, message: 'Server ACS tidak ditemukan.' };
|
|
}
|
|
|
|
if (server.id === 'builtin') {
|
|
try {
|
|
const pending = db.prepare(
|
|
`SELECT COUNT(*) AS c
|
|
FROM acs_tasks
|
|
WHERE device_id = ?
|
|
AND name IN ('refreshObject', 'getParameterValues', 'getParameterNames')
|
|
AND status IN ('pending', 'in_progress')`
|
|
).get(device._id);
|
|
if (pending && pending.c > 0) {
|
|
return { ok: true, message: 'Sinkronisasi ONU masih berjalan. Mohon tunggu sebentar.' };
|
|
}
|
|
} catch (e) {
|
|
logger.debug(`[CustomerDevice] Unable to check pending ACS tasks for ${device._id}: ${e.message}`);
|
|
}
|
|
}
|
|
|
|
const instance = genieacsApi.createAxiosInstance(server);
|
|
const tasksUrl = `/devices/${encodeURIComponent(device._id)}/tasks`;
|
|
const refreshObjects = collectRefreshObjects(device);
|
|
|
|
for (const objectName of refreshObjects) {
|
|
await instance.post(tasksUrl, { name: 'refreshObject', objectName }, { timeout: 15000 });
|
|
}
|
|
|
|
if (actor) {
|
|
auditTrail.logAuditTrail({
|
|
action: 'REFRESH_DEVICE',
|
|
entity_type: 'device',
|
|
entity_id: tag,
|
|
actor_type: actor.type || 'unknown',
|
|
actor_id: actor.id || null,
|
|
actor_name: actor.name || null,
|
|
details: {
|
|
device_id: device._id,
|
|
refreshObjects
|
|
},
|
|
ip_address: actor.ip || null,
|
|
user_agent: actor.userAgent || null
|
|
});
|
|
}
|
|
|
|
return {
|
|
ok: true,
|
|
message: `Sinkronisasi ONU dimulai. ${refreshObjects.length} jalur TR-069 dipoll.`,
|
|
deviceId: device._id,
|
|
acsServerId: server.id
|
|
};
|
|
} catch (e) {
|
|
logger.error(`[CustomerDevice] Error requesting refresh for ${tag}: ${e.message}`);
|
|
return { ok: false, message: 'Gagal memulai sinkronisasi ONU.' };
|
|
}
|
|
}
|
|
|
|
async function requestReboot(tag, actor = null) {
|
|
const device = await resolveDeviceToken(tag);
|
|
if (!device || !device._id) return { ok: false, message: 'Perangkat tidak ditemukan.' };
|
|
|
|
const server = device._acs_server_id ? genieacsApi.getACSServer(device._acs_server_id) : genieacsApi.getACSServer('legacy');
|
|
if (!server) return { ok: false, message: 'Server ACS tidak ditemukan.' };
|
|
|
|
const instance = genieacsApi.createAxiosInstance(server);
|
|
|
|
try {
|
|
await instance.post(
|
|
`/devices/${encodeURIComponent(device._id)}/tasks`,
|
|
{ name: 'reboot', timestamp: new Date().toISOString() }
|
|
);
|
|
|
|
// Catat audit trail jika berhasil
|
|
if (actor) {
|
|
auditTrail.logAuditTrail({
|
|
action: 'REBOOT_DEVICE',
|
|
entity_type: 'device',
|
|
entity_id: tag,
|
|
actor_type: actor.type || 'unknown',
|
|
actor_id: actor.id || null,
|
|
actor_name: actor.name || null,
|
|
details: {
|
|
device_id: device._id
|
|
},
|
|
ip_address: actor.ip || null,
|
|
user_agent: actor.userAgent || null
|
|
});
|
|
}
|
|
|
|
return { ok: true, message: 'Perintah reboot terkirim. Tunggu beberapa menit hingga ONU online.' };
|
|
} catch (e) {
|
|
return { ok: false, message: 'Gagal mengirim reboot ke GenieACS.' };
|
|
}
|
|
}
|
|
|
|
/** Daftar perangkat yang punya minimal satu tag (untuk admin WA). */
|
|
async function listDevicesWithTags(limit = 250) {
|
|
const servers = genieacsApi.getAllACSServers();
|
|
const queries = [
|
|
{ _tags: { $exists: true, $ne: [] } },
|
|
{ _tags: { $exists: true, $not: { $size: 0 } } },
|
|
{ '_tags.0': { $exists: true } }
|
|
];
|
|
const projection = [
|
|
'_id',
|
|
'_tags',
|
|
'_lastInform',
|
|
'DeviceID.SerialNumber',
|
|
'VirtualParameters.pppoeUsername',
|
|
'VirtualParameters.pppUsername',
|
|
'InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.WANPPPConnection.1.Username',
|
|
'InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.WANPPPConnection.1.Username._value'
|
|
].join(',');
|
|
|
|
let allDevices = [];
|
|
const maxLimit = Math.max(1, Math.min(parseInt(limit, 10) || 250, 500));
|
|
|
|
for (const server of servers) {
|
|
let found = false;
|
|
for (const query of queries) {
|
|
try {
|
|
const instance = genieacsApi.createAxiosInstance(server);
|
|
let response;
|
|
try {
|
|
response = await instance.get(`/devices`, {
|
|
params: {
|
|
query: JSON.stringify(query),
|
|
limit: maxLimit,
|
|
projection
|
|
},
|
|
timeout: 45000
|
|
});
|
|
} catch (e) {
|
|
response = await instance.get(`/api/devices`, {
|
|
params: {
|
|
query: JSON.stringify(query),
|
|
limit: maxLimit,
|
|
projection
|
|
},
|
|
timeout: 45000
|
|
});
|
|
}
|
|
const rows = Array.isArray(response.data) ? response.data : [];
|
|
if (rows.length > 0) {
|
|
rows.forEach(d => {
|
|
d._acs_server_id = server.id;
|
|
d._acs_server_name = server.name;
|
|
});
|
|
allDevices.push(...rows);
|
|
found = true;
|
|
break;
|
|
}
|
|
} catch (e) {
|
|
/* coba query alternatif */
|
|
}
|
|
}
|
|
}
|
|
|
|
if (allDevices.length > 0) {
|
|
return { ok: true, devices: allDevices.slice(0, limit) };
|
|
}
|
|
|
|
return { ok: false, devices: [], message: 'Gagal mengambil daftar dari GenieACS.' };
|
|
}
|
|
|
|
/** Mengambil semua perangkat tanpa melihat tag. */
|
|
async function listAllDevices(limit = 999999, acsId = null) {
|
|
let servers = genieacsApi.getAllACSServers();
|
|
if (acsId && acsId !== 'all') {
|
|
servers = servers.filter(s => String(s.id) === String(acsId));
|
|
}
|
|
|
|
let allDevices = [];
|
|
let lastError = null;
|
|
|
|
// Query servers in parallel using Promise.allSettled
|
|
const promises = servers.map(async (server) => {
|
|
try {
|
|
const instance = genieacsApi.createAxiosInstance(server);
|
|
const params = {
|
|
limit,
|
|
projection: '_id,_tags,_lastInform,DeviceID.SerialNumber,VirtualParameters,InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.WANPPPConnection.1.Username,InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.WANPPPConnection.2.Username,InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.WANPPPConnection.1.ExternalIPAddress,Device.PPP.Interface.1.Username,Device.PPP.Interface.1.ExternalIPAddress,InternetGatewayDevice.DeviceInfo.ModelName,InternetGatewayDevice.DeviceInfo.SoftwareVersion,InternetGatewayDevice.LANDevice.1.WLANConfiguration.1.SSID,InternetGatewayDevice.LANDevice.1.WLANConfiguration.1.TotalAssociations,InternetGatewayDevice.LANDevice.1.WLANConfiguration.5.TotalAssociations,InternetGatewayDevice.LANDevice.1.Hosts.HostNumberOfEntries,Device.WiFi.AccessPoint.1.AssociatedDeviceNumberOfEntries,Device.Hosts.HostNumberOfEntries,InternetGatewayDevice.LANDevice.1.Hosts.Host,Device.Hosts.Host'
|
|
};
|
|
let response;
|
|
try {
|
|
response = await instance.get(`/devices`, { params, timeout: 8000 });
|
|
} catch (e) {
|
|
response = await instance.get(`/api/devices`, { params, timeout: 8000 });
|
|
}
|
|
const rows = Array.isArray(response.data) ? response.data : [];
|
|
rows.forEach(d => {
|
|
d._acs_server_id = server.id;
|
|
d._acs_server_name = server.name;
|
|
});
|
|
return rows;
|
|
} catch (e) {
|
|
logger.error(`[CustomerDevice] Error listing devices on ${server.name}: ${e.message}`);
|
|
throw e;
|
|
}
|
|
});
|
|
|
|
const results = await Promise.allSettled(promises);
|
|
results.forEach((r) => {
|
|
if (r.status === 'fulfilled') {
|
|
allDevices.push(...r.value);
|
|
} else {
|
|
lastError = r.reason;
|
|
}
|
|
});
|
|
|
|
if (allDevices.length > 0 || !lastError) {
|
|
return { ok: true, devices: allDevices.slice(0, limit) };
|
|
}
|
|
|
|
return { ok: false, devices: [], message: 'Gagal mengambil daftar dari GenieACS: ' + (lastError ? lastError.message : 'Unknown error') };
|
|
}
|
|
|
|
async function updateCustomerTag(oldTag, newTag) {
|
|
const device = await findDeviceByTag(oldTag);
|
|
if (!device || !device._id) return { ok: false, message: 'Perangkat tidak ditemukan.' };
|
|
|
|
const server = device._acs_server_id ? genieacsApi.getACSServer(device._acs_server_id) : genieacsApi.getACSServer('legacy');
|
|
if (!server) return { ok: false, message: 'Server ACS tidak ditemukan.' };
|
|
|
|
const instance = genieacsApi.createAxiosInstance(server);
|
|
|
|
try {
|
|
const tags = Array.isArray(device._tags) ? device._tags.filter((t) => t !== oldTag) : [];
|
|
tags.push(newTag);
|
|
await instance.put(
|
|
`/devices/${encodeURIComponent(device._id)}`,
|
|
{ _id: device._id, _tags: tags }
|
|
);
|
|
return { ok: true };
|
|
} catch (e) {
|
|
return { ok: false, message: 'Gagal mengubah tag.' };
|
|
}
|
|
}
|
|
|
|
module.exports = {
|
|
findDeviceByTag,
|
|
findDeviceByPppoe,
|
|
fetchFullDevice,
|
|
resolveDeviceToken,
|
|
mapDeviceData,
|
|
extractPppoeUser,
|
|
getCustomerDeviceData,
|
|
fallbackCustomer,
|
|
requestRefresh,
|
|
updateSSID,
|
|
updatePassword,
|
|
requestReboot,
|
|
updateCustomerTag,
|
|
listDevicesWithTags,
|
|
listAllDevices,
|
|
expandTagCandidates,
|
|
findDeviceWithTagVariants,
|
|
phoneFromPnJid
|
|
};
|