// backend/routes/dashboard/agents.js // ───────────────────────────────────────────────────────────────────────────── // Agent Management and Telemetry API Router // ───────────────────────────────────────────────────────────────────────────── const express = require('express'); const router = express.Router(); const mongoose = require('mongoose'); const { Summary } = require('../../models/Schemas'); const { getTimeFilter, isKnownSite } = require('./helpers'); // GET /api/dashboard/agents/uptime router.get('/agents/uptime', async (req, res) => { try { const range = req.query.timeRange || '1d'; const cyclesMap = { '5m': 1, '30m': 6, '1h': 12, '1d': 288, '7d': 2016, '30d': 8640, }; const ideal = cyclesMap[range] ?? 12; let timeFilter = getTimeFilter(req); if (!timeFilter) { const now = new Date(); timeFilter = { $gte: new Date(now.getTime() - 30 * 24 * 3600000) }; } const query = { timestamp: timeFilter }; const requestedSiteUuid = req.headers['x-backone-site-uuid']; const isGlobalUser = req.user?.role === 'SUPER_ADMIN' || req.user?.role === 'EXECUTIVE' || ((!req.user?.site_uuid || req.user?.site_uuid === 'default') && ['SOC_ANALYST', 'ENGINEER'].includes(req.user?.role)); if (isGlobalUser && requestedSiteUuid && requestedSiteUuid !== 'all' && isKnownSite(requestedSiteUuid)) { query.site_uuid = { $in: [requestedSiteUuid, 'global'] }; } else if (isGlobalUser && (process.env.BACKONE_SITE_UUIDS || process.env.BACKONE_SITE_UUID)) { const envSites = (process.env.BACKONE_SITE_UUIDS || process.env.BACKONE_SITE_UUID).split(',').map(s => s.trim()).filter(Boolean); if (envSites.length > 0) query.site_uuid = { $in: [...envSites, 'global'] }; } else if (!isGlobalUser) { const envSites = ((process.env.BACKONE_SITE_UUIDS || process.env.BACKONE_SITE_UUID) || '').split(',').map(s => s.trim()).filter(Boolean); if (envSites.length > 0) { query.site_uuid = { $in: [...envSites, req.user?.site_uuid, 'global'].filter(Boolean) }; } else if (req.user?.site_uuid) { query.site_uuid = { $in: [req.user.site_uuid, 'global'] }; } } const stats = await Summary.aggregate([ { $match: query }, { $group: { _id: "$agent_uuid", count: { $sum: 1 } } } ]); const uptimeMap = {}; stats.forEach(s => { if (s._id) { const pct = Math.min(100, Math.round((s.count / ideal) * 1000) / 10); uptimeMap[s._id] = pct; } }); res.json({ ok: true, uptime: uptimeMap }); } catch (err) { res.status(500).json({ ok: false, error: err.message }); } }); // GET /api/dashboard/agents router.get('/agents', async (req, res) => { try { const isAuthorized = req.user?.role === 'SUPER_ADMIN' || req.user?.role === 'EXECUTIVE' || req.user?.role === 'TENANT_ADMIN' || req.user?._originalRole === 'SUPER_ADMIN' || req.user?._originalRole === 'TENANT_ADMIN'; if (!isAuthorized) { return res.status(403).json({ ok: false, error: 'Forbidden: Admin access only' }); } // Always filter out null/empty agent_uuid entries const query = { agent_uuid: { $nin: [null, '', undefined] } }; const effectiveRole = req.user?._originalRole || req.user?.role; const isGlobalUser = effectiveRole === 'SUPER_ADMIN' || effectiveRole === 'EXECUTIVE'; const requestedSiteUuid = req.headers['x-backone-site-uuid']; if (isGlobalUser && requestedSiteUuid && requestedSiteUuid !== 'all' && isKnownSite(requestedSiteUuid)) { query.site_uuid = { $in: [requestedSiteUuid, 'global'] }; } else if (isGlobalUser && (process.env.BACKONE_SITE_UUIDS || process.env.BACKONE_SITE_UUID)) { const envSites = (process.env.BACKONE_SITE_UUIDS || process.env.BACKONE_SITE_UUID).split(',').map(s => s.trim()).filter(Boolean); if (envSites.length > 0) query.site_uuid = { $in: [...envSites, 'global'] }; } else if (effectiveRole === 'TENANT_ADMIN') { const envSites = ((process.env.BACKONE_SITE_UUIDS || process.env.BACKONE_SITE_UUID) || '').split(',').map(s => s.trim()).filter(Boolean); if (envSites.length > 0) { query.site_uuid = { $in: [...envSites, req.user.site_uuid, 'global'].filter(Boolean) }; } else { query.site_uuid = { $in: [req.user.site_uuid, 'global'] }; } } const agents = await Summary.distinct('agent_uuid', query); // Extra safety: filter any remaining null values from result const cleanAgents = agents.filter(a => a != null && a !== ''); res.json({ ok: true, count: cleanAgents.length, agents: cleanAgents }); } catch (err) { res.status(500).json({ ok: false, error: err.message }); } }); // GET /api/dashboard/agents/storage // Returns per-agent total data size from in-memory cache (capacityTracker). // Cache is computed once at startup and refreshed every 5-minute collection cycle. // Values represent total MongoDB storage footprint per agent (across 7-day retention window). router.get('/agents/storage', async (req, res) => { try { const isAuthorized = req.user?.role === 'SUPER_ADMIN' || req.user?.role === 'TENANT_ADMIN' || req.user?._originalRole === 'SUPER_ADMIN' || req.user?._originalRole === 'TENANT_ADMIN'; if (!isAuthorized) { return res.status(403).json({ ok: false, error: 'Forbidden: Admin access only' }); } const requestedSiteUuid = req.headers['x-backone-site-uuid']; const isGlobalUser = req.user?.role === 'SUPER_ADMIN' || req.user?.role === 'EXECUTIVE' || ((!req.user?.site_uuid || req.user?.site_uuid === 'default') && ['SOC_ANALYST', 'ENGINEER'].includes(req.user?.role)); let siteUuid = null; if (isGlobalUser && requestedSiteUuid && requestedSiteUuid !== 'all' && isKnownSite(requestedSiteUuid)) { siteUuid = requestedSiteUuid; } else if (!isGlobalUser && req.user?.site_uuid) { siteUuid = req.user.site_uuid; } const { agentSizesCache, lastCacheUpdate } = require('../../db/capacityTracker'); const allStorage = agentSizesCache(); const cachedAt = lastCacheUpdate(); let storage = allStorage; if (siteUuid) { const registryAgents = await mongoose.connection.db.collection('agent_registry') .find({ site_uuid: { $in: [siteUuid, 'global'] } }) .toArray(); const siteAgentUuids = new Set(registryAgents.map(a => a.uuid)); const summaryAgents = await Summary.distinct('agent_uuid', { site_uuid: { $in: [siteUuid, 'global'] } }); summaryAgents.forEach(uuid => { if (uuid) siteAgentUuids.add(uuid); }); storage = {}; Object.keys(allStorage).forEach(uuid => { if (siteAgentUuids.has(uuid)) { storage[uuid] = allStorage[uuid]; } }); } res.json({ ok: true, storage, cached_at: cachedAt }); } catch (err) { res.status(500).json({ ok: false, error: err.message }); } }); // ─── GET /api/dashboard/agents/list ────────────────────────────────────────── // Lightweight endpoint: kembalikan list agents (uuid + label) untuk user saat ini // Digunakan frontend untuk lookup label agent pada View-As banner router.get('/agents/list', async (req, res) => { try { const db = mongoose.connection.db; const user = req.user; const companyRoles = ['COMPANY_ADMIN', 'COMPANY_OPERATOR', 'COMPANY_VIEWER']; const isCompanyRole = companyRoles.includes(user?.role); let filter = {}; if (isCompanyRole) { // Company roles: hanya kembalikan agent yang di-assign ke user const agentUuids = user?.agent_uuids || []; if (agentUuids.length === 0) { return res.json({ ok: true, data: [] }); } filter.uuid = { $in: agentUuids }; } else { // Admin/SUPER_ADMIN: filter berdasarkan site UUID dari header const requestedSiteUuid = req.headers['x-backone-site-uuid']; const effectiveRole = user?._originalRole || user?.role; const isGlobalUser = effectiveRole === 'SUPER_ADMIN' || effectiveRole === 'EXECUTIVE'; if (isGlobalUser && requestedSiteUuid && requestedSiteUuid !== 'all' && isKnownSite(requestedSiteUuid)) { filter.site_uuid = { $in: [requestedSiteUuid, 'global'] }; } else if (isGlobalUser && (process.env.BACKONE_SITE_UUIDS || process.env.BACKONE_SITE_UUID)) { const envSites = (process.env.BACKONE_SITE_UUIDS || process.env.BACKONE_SITE_UUID).split(',').map(s => s.trim()).filter(Boolean); if (envSites.length > 0) filter.site_uuid = { $in: [...envSites, 'global'] }; } else if (!isGlobalUser) { const envSites = ((process.env.BACKONE_SITE_UUIDS || process.env.BACKONE_SITE_UUID) || '').split(',').map(s => s.trim()).filter(Boolean); if (envSites.length > 0) { filter.site_uuid = { $in: [...envSites, user?.site_uuid, 'global'].filter(Boolean) }; } else if (user?.site_uuid) { filter.site_uuid = { $in: [user.site_uuid, 'global'] }; } } } const agents = await db.collection('agent_registry') .find(filter) .project({ uuid: 1, label: 1, protected_label: 1, _id: 0 }) .sort({ uuid: 1 }) .toArray(); // Use protected_label if available to prevent proxy overwrite bug const mappedAgents = agents.map(a => ({ uuid: a.uuid, label: a.protected_label || a.label || a.uuid })); res.json({ ok: true, data: mappedAgents }); } catch (err) { res.status(500).json({ ok: false, error: err.message }); } }); // ─── GET /api/dashboard/agents/:uuid/subnets ───────────────────────────────── // Kembalikan konfigurasi subnet yang diizinkan untuk agent tertentu router.get('/agents/:uuid/subnets', async (req, res) => { try { const db = mongoose.connection.db; const doc = await db.collection('agent_registry').findOne({ uuid: req.params.uuid }); res.json({ ok: true, data: { allowed_subnets: doc?.allowed_subnets || [] } }); } catch (err) { res.status(500).json({ ok: false, error: err.message }); } }); // ─── PUT /api/dashboard/agents/:uuid/subnets ───────────────────────────────── // Simpan konfigurasi subnet yang diizinkan untuk agent tertentu // Body: { allowed_subnets: ["192.168.1", "10.21"] } function ipToLong(ip) { return ip.split('.').reduce((acc, octet) => (acc << 8) + parseInt(octet, 10), 0) >>> 0; } function ipMatchesSubnets(ip, subnets) { if (!subnets || subnets.length === 0) return true; if (!ip) return false; return subnets.some(subnet => { if (subnet.includes('/')) { try { const [range, bitsStr] = subnet.split('/'); const bits = parseInt(bitsStr, 10); if (isNaN(bits) || bits < 0 || bits > 32) return false; const mask = bits === 0 ? 0 : (~0 << (32 - bits)) >>> 0; return (ipToLong(ip) & mask) === (ipToLong(range) & mask); } catch (e) { return false; } } else { return ip === subnet; } }); } // PUT /api/dashboard/agents/:uuid/subnets router.put('/agents/:uuid/subnets', async (req, res) => { try { const allowedRoles = ['SUPER_ADMIN', 'TENANT_ADMIN', 'COMPANY_ADMIN']; if (!allowedRoles.includes(req.user?.role)) { return res.status(403).json({ ok: false, error: 'Forbidden' }); } const db = mongoose.connection.db; const subnets = (req.body.allowed_subnets || []).map(s => s.trim()).filter(Boolean); await db.collection('agent_registry').updateOne( { uuid: req.params.uuid }, { $set: { allowed_subnets: subnets, subnets_updated_at: new Date() } } ); // Auto-cleanup background task if (subnets.length > 0) { setTimeout(async () => { try { const ips = await db.collection('devicestats').distinct('ip_address', { agent_uuid: req.params.uuid }); const invalidIps = ips.filter(ip => !ipMatchesSubnets(ip, subnets)); if (invalidIps.length > 0) { await db.collection('devicestats').deleteMany({ agent_uuid: req.params.uuid, ip_address: { $in: invalidIps } }); await db.collection('flows').deleteMany({ agent_uuid: req.params.uuid, src_ip: { $in: invalidIps } }); } } catch (e) { console.error('Auto-cleanup error:', e); } }, 100); } res.json({ ok: true, data: { allowed_subnets: subnets } }); } catch (err) { res.status(500).json({ ok: false, error: err.message }); } }); module.exports = router;