feat: complete deployment codebase for BackOne DPI dashboard with dynamic telemetry, rebranding, database size tracking, and modular design
This commit is contained in:
1 parent
6cf3c6c7e5
commit
c316f3171b
226 files changed
+21152
-8397
No files matched your search
@@ -0,0 +1,284 @@
|
||||
// backend/routes/appDetailsHandler.js
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// App Detail Handler — reads from MongoDB first (DeviceAppStat + AppStat + Flow)
|
||||
// Falls back to live DPI API only if MongoDB has zero data for this app+agent
|
||||
//
|
||||
// Menggunakan DeviceAppStat sebagai sumber utama untuk top_ips agar sinkron
|
||||
// dengan data aplikasi di detail perangkat (DeviceDetailModal).
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
const axios = require('axios');
|
||||
const { AppStat, DeviceAppStat, Flow } = require('../models/Schemas');
|
||||
|
||||
// ─── Shared in-memory caches (for DPI API fallback only) ─────────────────────
|
||||
let appLookupCache = null;
|
||||
let agentMapCache = null;
|
||||
let agentCachePopulating = false;
|
||||
|
||||
function timeRangeToMinutes(timeRange) {
|
||||
const mapping = {
|
||||
'5m': 5, '10m': 10, '30m': 30, '1h': 60,
|
||||
'1d': 1440, '7d': 10080, '30d': 43200, 'all': 43200
|
||||
};
|
||||
return mapping[timeRange] ?? 60;
|
||||
}
|
||||
|
||||
// Resolve agent UUID → DPI numeric agent ID (for filter_agents param)
|
||||
async function populateAgentCache(BASE_URL, token, siteUuid) {
|
||||
if (agentMapCache !== null || agentCachePopulating) return;
|
||||
agentCachePopulating = true;
|
||||
try {
|
||||
const headers = { 'x-api-key': token, 'Accept': 'application/json' };
|
||||
if (siteUuid) headers['x-net-site'] = siteUuid;
|
||||
const res = await axios.get(`${BASE_URL}/data/stats/top/agent/download`, {
|
||||
headers, params: { filter_interval: 43200, settings_limit: 100 }, timeout: 4000
|
||||
});
|
||||
agentMapCache = {};
|
||||
if (res.data && Array.isArray(res.data.data)) {
|
||||
res.data.data.forEach(r => {
|
||||
if (r.agent?.uuid && r.agent?.id) agentMapCache[r.agent.uuid] = r.agent.id;
|
||||
});
|
||||
}
|
||||
console.log(`[AppDetailsHandler] Agent cache: ${Object.keys(agentMapCache).length} agents`);
|
||||
} catch (e) {
|
||||
agentMapCache = {};
|
||||
console.warn('[AppDetailsHandler] Agent cache failed:', e.message);
|
||||
} finally {
|
||||
agentCachePopulating = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve app label → DPI application ID
|
||||
async function populateAppCache(BASE_URL, token, siteUuid) {
|
||||
if (appLookupCache !== null) return;
|
||||
try {
|
||||
const headers = { 'x-api-key': token, 'Accept': 'application/json' };
|
||||
if (siteUuid) headers['x-net-site'] = siteUuid;
|
||||
const res = await axios.get(`${BASE_URL}/lookup/applications`, {
|
||||
headers, params: { settings_limit: 2000 }, timeout: 8000
|
||||
});
|
||||
appLookupCache = {};
|
||||
if (res.data && Array.isArray(res.data.data)) {
|
||||
res.data.data.forEach(a => {
|
||||
if (!a.label || !a.id) return;
|
||||
let domain = null;
|
||||
if (a.home_page?.url) {
|
||||
domain = a.home_page.url.replace(/^https?:\/\/(www\.)?/, '').split('/')[0];
|
||||
} else if (a.domain_list?.length > 0) {
|
||||
domain = a.domain_list[0].label;
|
||||
} else {
|
||||
domain = a.label.toLowerCase();
|
||||
}
|
||||
appLookupCache[a.label.toLowerCase()] = { id: a.id, label: a.label, domain };
|
||||
});
|
||||
}
|
||||
console.log(`[AppDetailsHandler] App cache: ${Object.keys(appLookupCache).length} apps`);
|
||||
} catch (e) {
|
||||
appLookupCache = {};
|
||||
console.warn('[AppDetailsHandler] App cache failed:', e.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Core DPI fetch for app-details — only used when MongoDB has no data
|
||||
async function fetchFromDpiApi(label, agentUuid, timeRange, token, siteUuid) {
|
||||
const BASE_URL = process.env.NETIFY_INFORMATICS_BASE_URL || 'https://informatics.netify.ai/api/v1';
|
||||
const headers = { 'x-api-key': token, 'Accept': 'application/json', 'x-net-site': siteUuid };
|
||||
|
||||
const TIMEOUT_MS = 12000;
|
||||
const deadline = new Promise((_, reject) =>
|
||||
setTimeout(() => reject(new Error(`AppDetailsHandler: ${TIMEOUT_MS}ms timeout`)), TIMEOUT_MS)
|
||||
);
|
||||
|
||||
async function doFetch() {
|
||||
await Promise.all([
|
||||
populateAgentCache(BASE_URL, token, siteUuid),
|
||||
populateAppCache(BASE_URL, token, siteUuid)
|
||||
]);
|
||||
|
||||
const appInfo = appLookupCache?.[label.toLowerCase()];
|
||||
if (!appInfo) {
|
||||
console.warn(`[AppDetailsHandler] App "${label}" not found in lookup cache`);
|
||||
return null;
|
||||
}
|
||||
|
||||
const params = {
|
||||
filter_interval: timeRangeToMinutes(timeRange),
|
||||
filter_applications: `["${appInfo.id}"]`,
|
||||
settings_limit: 10000
|
||||
};
|
||||
|
||||
if (agentUuid && agentMapCache?.[agentUuid]) {
|
||||
params.filter_agents = `[${agentMapCache[agentUuid]}]`;
|
||||
}
|
||||
|
||||
const [dlRes, ulRes] = await Promise.all([
|
||||
axios.get(`${BASE_URL}/data/stats/top/local_ip/download`, { headers, params, timeout: 10000 }),
|
||||
axios.get(`${BASE_URL}/data/stats/top/local_ip/upload`, { headers, params, timeout: 10000 }),
|
||||
]);
|
||||
|
||||
const ipsMap = {};
|
||||
(dlRes.data?.data || []).forEach(item => {
|
||||
const ip = item.local_ip?.address;
|
||||
if (!ip) return;
|
||||
if (!ipsMap[ip]) {
|
||||
ipsMap[ip] = {
|
||||
ip_address: ip,
|
||||
download: item.download || 0,
|
||||
upload: 0,
|
||||
first_seen: item.last_seen_at?.date || new Date().toISOString(),
|
||||
last_seen: item.last_seen_at?.date || new Date().toISOString(),
|
||||
domain: appInfo.domain,
|
||||
protocol: 'HTTPS / TLS'
|
||||
};
|
||||
} else {
|
||||
ipsMap[ip].download = item.download || 0;
|
||||
}
|
||||
});
|
||||
|
||||
(ulRes.data?.data || []).forEach(item => {
|
||||
const ip = item.local_ip?.address;
|
||||
if (!ip) return;
|
||||
if (!ipsMap[ip]) {
|
||||
ipsMap[ip] = {
|
||||
ip_address: ip,
|
||||
download: 0,
|
||||
upload: item.upload || 0,
|
||||
first_seen: item.last_seen_at?.date || new Date().toISOString(),
|
||||
last_seen: item.last_seen_at?.date || new Date().toISOString(),
|
||||
domain: appInfo.domain,
|
||||
protocol: 'HTTPS / TLS'
|
||||
};
|
||||
} else {
|
||||
ipsMap[ip].upload = item.upload || 0;
|
||||
if (item.last_seen_at?.date) {
|
||||
const d = new Date(item.last_seen_at.date);
|
||||
if (d > new Date(ipsMap[ip].last_seen)) ipsMap[ip].last_seen = item.last_seen_at.date;
|
||||
if (d < new Date(ipsMap[ip].first_seen)) ipsMap[ip].first_seen = item.last_seen_at.date;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const top_ips = Object.values(ipsMap).sort((a, b) => (b.download + b.upload) - (a.download + a.upload));
|
||||
const totalDl = top_ips.reduce((s, x) => s + x.download, 0);
|
||||
const totalUl = top_ips.reduce((s, x) => s + x.upload, 0);
|
||||
|
||||
console.log(`[AppDetailsHandler] DPI API: label=${label} agent=${agentUuid} top_ips=${top_ips.length} dl=${(totalDl/1e9).toFixed(2)}GB`);
|
||||
return { top_ips, totalDl, totalUl };
|
||||
}
|
||||
|
||||
try {
|
||||
return await Promise.race([doFetch(), deadline]);
|
||||
} catch (err) {
|
||||
console.warn('[AppDetailsHandler] DPI API timeout/error:', err.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Main Handler ─────────────────────────────────────────────────────────────
|
||||
module.exports = async function appDetailsHandler(req, res, helpers) {
|
||||
const t0 = Date.now();
|
||||
try {
|
||||
const { getTimeFilter, getBaseFilter } = helpers;
|
||||
const label = String(req.query.label ?? '');
|
||||
if (!label) return res.status(400).json({ ok: false, message: 'label required' });
|
||||
|
||||
const token = process.env.NETIFY_API_KEY || process.env.NETIFY_TOKEN;
|
||||
const SITE_UUID = process.env.NETIFY_SITE_UUID;
|
||||
|
||||
// Respect timeRange from request
|
||||
const timeFilter = getTimeFilter(req);
|
||||
const baseFilter = getBaseFilter(req, timeFilter);
|
||||
|
||||
const agentUuid = String(req.query.agent_uuid ?? '') || req.user?.agent_uuid || null;
|
||||
if (agentUuid) baseFilter.agent_uuid = agentUuid;
|
||||
|
||||
// ── Step 1: Query DeviceAppStat (Primary source for per-device bandwidth per-app) ──
|
||||
const queryFilter = { ...baseFilter, app_label: label };
|
||||
const deviceApps = await DeviceAppStat.find(queryFilter).sort({ timestamp: -1 }).lean();
|
||||
|
||||
if (deviceApps.length > 0) {
|
||||
// Pre-load application lookup to resolve default domains
|
||||
const BASE_URL = process.env.NETIFY_INFORMATICS_BASE_URL || 'https://informatics.netify.ai/api/v1';
|
||||
if (token && SITE_UUID) {
|
||||
await populateAppCache(BASE_URL, token, SITE_UUID).catch(e => console.warn('[AppDetails] Cache error:', e.message));
|
||||
}
|
||||
const appMeta = appLookupCache?.[label.toLowerCase()];
|
||||
|
||||
const ipsMap = {};
|
||||
deviceApps.forEach(da => {
|
||||
const ip = da.ip_address;
|
||||
if (!ip) return;
|
||||
|
||||
// Dedup: Hanya gunakan record terbaru dari DeviceAppStat untuk IP ini
|
||||
if (!ipsMap[ip] || new Date(da.timestamp) > new Date(ipsMap[ip].timestamp)) {
|
||||
const tStr = da.timestamp ? new Date(da.timestamp).toISOString() : new Date().toISOString();
|
||||
ipsMap[ip] = {
|
||||
ip_address: ip,
|
||||
download: da.download || 0,
|
||||
upload: da.upload || 0,
|
||||
first_seen: da.created_at || tStr,
|
||||
last_seen: da.updated_at || tStr,
|
||||
timestamp: da.timestamp,
|
||||
domain: appMeta?.domain || null,
|
||||
protocol: 'HTTPS / TLS'
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// Enrich domain & protocol info from Flow if available
|
||||
const flows = await Flow.find({
|
||||
...baseFilter,
|
||||
$or: [
|
||||
{ app_label: label },
|
||||
{ domain: { $regex: label.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), $options: 'i' } }
|
||||
]
|
||||
}).sort({ timestamp: -1 }).lean();
|
||||
|
||||
flows.forEach(f => {
|
||||
const ip = f.src_ip;
|
||||
if (ip && ipsMap[ip]) {
|
||||
if (f.domain) ipsMap[ip].domain = f.domain;
|
||||
if (f.protocol) ipsMap[ip].protocol = f.protocol;
|
||||
}
|
||||
});
|
||||
|
||||
const top_ips = Object.values(ipsMap)
|
||||
.sort((a, b) => (b.download + b.upload) - (a.download + a.upload))
|
||||
.map(({ timestamp, ...rest }) => rest); // remove temp timestamp field
|
||||
|
||||
// Ambl total download/upload dari latest AppStat (cumulative global)
|
||||
const appStats = await AppStat.find({ ...baseFilter, app_label: label }).sort({ timestamp: -1 }).limit(1).lean();
|
||||
const topIpsDl = top_ips.reduce((s, x) => s + x.download, 0);
|
||||
const topIpsUl = top_ips.reduce((s, x) => s + x.upload, 0);
|
||||
const totalDl = Math.max(appStats[0]?.download || 0, topIpsDl);
|
||||
const totalUl = Math.max(appStats[0]?.upload || 0, topIpsUl);
|
||||
|
||||
console.log(`[AppDetails] DeviceAppStat: label=${label} deviceCount=${top_ips.length} dl=${(totalDl/1e9).toFixed(2)}GB time=${Date.now()-t0}ms`);
|
||||
return res.json({ ok: true, data: { label, total_download: totalDl, total_upload: totalUl, top_ips } });
|
||||
}
|
||||
|
||||
// ── Step 2: Fall back to DPI API only if MongoDB has ZERO data ────────────
|
||||
if (token && SITE_UUID) {
|
||||
const dpiResult = await fetchFromDpiApi(label, agentUuid, req.query.timeRange, token, SITE_UUID);
|
||||
if (dpiResult) {
|
||||
console.log(`[AppDetails] DPI fallback: label=${label} time=${Date.now()-t0}ms`);
|
||||
return res.json({
|
||||
ok: true,
|
||||
data: { label, total_download: dpiResult.totalDl, total_upload: dpiResult.totalUl, top_ips: dpiResult.top_ips }
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ── Step 3: AppStat only fallback (aggregate only, no IP list) ─────────────
|
||||
const appStats = await AppStat.find({ ...baseFilter, app_label: label }).sort({ timestamp: -1 }).limit(1).lean();
|
||||
const statsDl = appStats[0]?.download || 0;
|
||||
const statsUl = appStats[0]?.upload || 0;
|
||||
|
||||
console.log(`[AppDetails] AppStat fallback: label=${label} dl=${(statsDl/1e9).toFixed(2)}GB time=${Date.now()-t0}ms`);
|
||||
return res.json({ ok: true, data: { label, total_download: statsDl, total_upload: statsUl, top_ips: [] } });
|
||||
|
||||
} catch (err) {
|
||||
console.error('[AppDetailsHandler] Error:', err);
|
||||
return res.status(500).json({ ok: false, message: err.message });
|
||||
}
|
||||
};
|
||||
Reference in new issue
Block a user