303 lines
13 KiB
JavaScript
303 lines
13 KiB
JavaScript
// 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;
|