feat(prod): deploy web dashboard, migrate mongodb configuration, resolve server components render error and fix logo static asset paths

This commit is contained in:
Rafif-Riqullah-Siregar committed 2026-07-15 00:27:55 +07:00
1 parent 4882108068
commit b2ea883601
119 files changed
+7123 -2011

No files matched your search

+188
View File
@@ -0,0 +1,188 @@
const express = require('express');
const router = express.Router();
const { CustomAgentLocation, Summary, DeviceStat, Flow } = require('../../models/Schemas');
const { getTimeFilter } = require('./helpers');
// ─── 1. GET /api/dashboard/agent-locations ──────────────────────────────────────
router.get('/agent-locations', async (req, res) => {
try {
const isGlobalUser = req.user?.role === 'SUPER_ADMIN' ||
((!req.user?.site_uuid || req.user?.site_uuid === 'default') && ['SOC_ANALYST', 'ENGINEER'].includes(req.user?.role));
let query = {};
if (!isGlobalUser && req.user?.site_uuid) {
query.site_uuid = req.user.site_uuid;
}
if (req.user?.role === 'AGENT_VIEWER' && req.user?.agent_uuid) {
query.agent_uuid = req.user.agent_uuid;
}
const locations = await CustomAgentLocation.find(query).lean();
res.json({ ok: true, data: locations });
} catch (err) {
console.error('[GET /agent-locations]', err.message);
res.status(500).json({ ok: false, error: err.message });
}
});
// ─── 2. POST /api/dashboard/agent-locations ─────────────────────────────────────
router.post('/agent-locations', async (req, res) => {
try {
if (req.user?.role !== 'SUPER_ADMIN' && req.user?.role !== 'TENANT_ADMIN') {
return res.status(403).json({ ok: false, error: 'Only administrators can configure agent geolocations.' });
}
const { agent_uuid, latitude, longitude, label } = req.body;
if (!agent_uuid || latitude === undefined || longitude === undefined) {
return res.status(400).json({ ok: false, error: 'agent_uuid, latitude, and longitude are required' });
}
// Determine site_uuid
let siteUuid = null;
const isGlobalUser = req.user?.role === 'SUPER_ADMIN' ||
((!req.user?.site_uuid || req.user?.site_uuid === 'default') && ['SOC_ANALYST', 'ENGINEER'].includes(req.user?.role));
if (!isGlobalUser && req.user?.site_uuid) {
const agentBelongs = await Summary.findOne({ agent_uuid, site_uuid: req.user.site_uuid });
if (!agentBelongs) {
return res.status(403).json({ ok: false, error: 'Unauthorized: This agent does not belong to your tenant.' });
}
siteUuid = req.user.site_uuid;
} else {
// Find the site_uuid from Summary collection for this agent
const summaryDoc = await Summary.findOne({ agent_uuid });
if (summaryDoc) {
siteUuid = summaryDoc.site_uuid;
} else {
// Fallback or use standard env site_uuid
siteUuid = process.env.NETIFY_SITE_UUID || '6681452d_9cae_4ff4_8ae8_0d504774265e';
}
}
const findQuery = { agent_uuid };
if (!isGlobalUser && req.user?.site_uuid) {
findQuery.site_uuid = req.user.site_uuid;
}
const upserted = await CustomAgentLocation.findOneAndUpdate(
findQuery,
{
agent_uuid,
site_uuid: siteUuid,
latitude: parseFloat(latitude),
longitude: parseFloat(longitude),
label: label || ''
},
{ new: true, upsert: true }
);
res.json({ ok: true, data: upserted });
} catch (err) {
console.error('[POST /agent-locations]', err.message);
res.status(500).json({ ok: false, error: err.message });
}
});
// ─── 3. DELETE /api/dashboard/agent-locations/:agent_uuid ────────────────────────
router.delete('/agent-locations/:agent_uuid', async (req, res) => {
try {
if (req.user?.role !== 'SUPER_ADMIN' && req.user?.role !== 'TENANT_ADMIN') {
return res.status(403).json({ ok: false, error: 'Only administrators can delete agent geolocations.' });
}
const { agent_uuid } = req.params;
let query = { agent_uuid };
const isGlobalUser = req.user?.role === 'SUPER_ADMIN' ||
((!req.user?.site_uuid || req.user?.site_uuid === 'default') && ['SOC_ANALYST', 'ENGINEER'].includes(req.user?.role));
if (!isGlobalUser && req.user?.site_uuid) {
query.site_uuid = req.user.site_uuid;
}
const resDelete = await CustomAgentLocation.deleteOne(query);
res.json({ ok: true, deleted: resDelete.deletedCount > 0 });
} catch (err) {
console.error('[DELETE /agent-locations]', err.message);
res.status(500).json({ ok: false, error: err.message });
}
});
// ─── 4. GET /api/dashboard/agent-flows ──────────────────────────────────────────
router.get('/agent-flows', async (req, res) => {
try {
const requestedSiteUuid = req.headers['x-backone-site-uuid'];
const isGlobalUser = req.user?.role === 'SUPER_ADMIN' ||
((!req.user?.site_uuid || req.user?.site_uuid === 'default') && ['SOC_ANALYST', 'ENGINEER'].includes(req.user?.role));
const siteUuid = (isGlobalUser && requestedSiteUuid)
? requestedSiteUuid
: (req.user?.site_uuid || '6681452d_9cae_4ff4_8ae8_0d504774265e');
const timeFilter = getTimeFilter(req);
// Build IP-to-Agent mapping from DeviceStat
const deviceQuery = { site_uuid: siteUuid };
if (req.user?.role === 'AGENT_VIEWER' && req.user?.agent_uuid) {
deviceQuery.agent_uuid = req.user.agent_uuid;
}
const devices = await DeviceStat.find(deviceQuery).select('ip_address agent_uuid').lean();
const deviceIpToAgent = {};
for (const dev of devices) {
if (dev.ip_address && dev.agent_uuid) {
deviceIpToAgent[dev.ip_address] = dev.agent_uuid;
}
}
// Query flows
const flowsQuery = { site_uuid: siteUuid };
if (timeFilter) flowsQuery.timestamp = timeFilter;
if (req.user?.role === 'AGENT_VIEWER' && req.user?.agent_uuid) {
flowsQuery.agent_uuid = req.user.agent_uuid;
}
const flows = await Flow.find(flowsQuery)
.select('agent_uuid src_ip dst_ip download upload app_label')
.sort({ timestamp: -1 })
.limit(5000)
.lean();
const flowMap = {};
for (const flow of flows) {
const srcAgent = flow.agent_uuid;
const dstAgent = deviceIpToAgent[flow.dst_ip];
if (srcAgent && dstAgent && srcAgent !== dstAgent) {
const key = `${srcAgent}->${dstAgent}`;
if (!flowMap[key]) {
flowMap[key] = {
source: srcAgent,
target: dstAgent,
bytes: 0,
flowsCount: 0,
details: []
};
}
const bytes = ((flow.download || 0) + (flow.upload || 0));
flowMap[key].bytes += bytes;
flowMap[key].flowsCount += 1;
flowMap[key].details.push({
src_ip: flow.src_ip,
dst_ip: flow.dst_ip,
app: flow.app_label || 'Unclassified',
bytes: bytes
});
}
}
const result = Object.values(flowMap);
for (const f of result) {
f.details.sort((a, b) => b.bytes - a.bytes);
f.details = f.details.slice(0, 5); // top 5 sub-flows
}
res.json({ ok: true, data: result });
} catch (err) {
console.error('[GET /agent-flows]', err.message);
res.status(500).json({ ok: false, error: err.message });
}
});
module.exports = router;
+28 -38
View File
@@ -53,10 +53,20 @@ router.get('/agents/uptime', async (req, res) => {
// GET /api/dashboard/agents
router.get('/agents', async (req, res) => {
try {
if (req.user?.role !== 'SUPER_ADMIN' && req.user?._originalRole !== 'SUPER_ADMIN') {
return res.status(403).json({ ok: false, error: 'Forbidden: SUPER_ADMIN only' });
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 agents = await Summary.distinct('agent_uuid');
const query = {};
const effectiveRole = req.user?._originalRole || req.user?.role;
if (effectiveRole === 'TENANT_ADMIN') {
query.site_uuid = req.user.site_uuid;
}
const agents = await Summary.distinct('agent_uuid', query);
res.json({ ok: true, count: agents.length, agents });
} catch (err) {
res.status(500).json({ ok: false, error: err.message });
@@ -64,49 +74,29 @@ router.get('/agents', async (req, res) => {
});
// 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 {
if (req.user?.role !== 'SUPER_ADMIN' && req.user?._originalRole !== 'SUPER_ADMIN') {
return res.status(403).json({ ok: false, error: 'Forbidden: SUPER_ADMIN only' });
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 db = mongoose.connection.db;
if (!db) {
return res.json({ ok: true, storage: {} });
}
const { agentSizesCache, lastCacheUpdate } = require('../../db/capacityTracker');
const storage = agentSizesCache();
const cachedAt = lastCacheUpdate();
const agentSizes = {};
const collections = await db.listCollections().toArray();
for (const colInfo of collections) {
const colName = colInfo.name;
if (colName.startsWith('system.')) continue;
const col = db.collection(colName);
const sampleDoc = await col.findOne({ agent_uuid: { $ne: null } });
if (!sampleDoc) continue;
const pipeline = [
{ $project: { agent_uuid: 1, docSize: { $bsonSize: "$$ROOT" } } },
{ $group: { _id: "$agent_uuid", totalBytes: { $sum: "$docSize" } } }
];
const results = await col.aggregate(pipeline).toArray();
for (const res of results) {
const agent = res._id || 'Unknown';
agentSizes[agent] = (agentSizes[agent] || 0) + res.totalBytes;
}
}
const storageMap = {};
for (const [agent, bytes] of Object.entries(agentSizes)) {
storageMap[agent] = parseFloat((bytes / (1024 * 1024)).toFixed(2));
}
res.json({ ok: true, storage: storageMap });
res.json({ ok: true, storage, cached_at: cachedAt });
} catch (err) {
res.status(500).json({ ok: false, error: err.message });
}
});
module.exports = router;
+99
View File
@@ -0,0 +1,99 @@
const express = require('express');
const router = express.Router();
const { BlacklistRule } = require('../../models/Schemas');
const { getBaseFilter } = require('./helpers');
// GET /api/dashboard/blacklist
router.get('/blacklist', async (req, res) => {
try {
const filter = getBaseFilter(req);
const site_uuid = filter.site_uuid;
if (!site_uuid) {
return res.status(400).json({ error: 'Site UUID is required' });
}
const query = { site_uuid };
if (filter.agent_uuid) {
query.agent_uuid = filter.agent_uuid;
}
const rules = await BlacklistRule.find(query).sort({ created_at: -1 }).lean();
return res.json({ ok: true, data: rules });
} catch (err) {
console.error('[Blacklist GET] Error:', err.message);
return res.status(500).json({ error: 'Internal server error' });
}
});
// POST /api/dashboard/blacklist
router.post('/blacklist', async (req, res) => {
try {
if (req.user?.role !== 'AGENT_VIEWER') {
return res.status(403).json({ error: 'Only Network Agents (or Admins in View As mode) can modify blacklist rules.' });
}
const filter = getBaseFilter(req);
const site_uuid = filter.site_uuid;
const agent_uuid = filter.agent_uuid;
if (!site_uuid) {
return res.status(400).json({ error: 'Site UUID is required' });
}
if (!agent_uuid) {
return res.status(400).json({ error: 'Agent UUID is required' });
}
const { type, value } = req.body;
if (!type || !value) {
return res.status(400).json({ error: 'Type and value are required' });
}
if (!['category', 'domain'].includes(type)) {
return res.status(400).json({ error: 'Invalid blacklist type' });
}
// Upsert or create rule isolated per agent
const rule = await BlacklistRule.findOneAndUpdate(
{ site_uuid, agent_uuid, type, value: value.trim() },
{ site_uuid, agent_uuid, type, value: value.trim(), is_active: true },
{ upsert: true, new: true }
);
return res.json({ ok: true, data: rule });
} catch (err) {
console.error('[Blacklist POST] Error:', err.message);
if (err.code === 11000) {
return res.status(400).json({ error: 'Rule already exists' });
}
return res.status(500).json({ error: 'Internal server error' });
}
});
// DELETE /api/dashboard/blacklist/:id
router.delete('/blacklist/:id', async (req, res) => {
try {
if (req.user?.role !== 'AGENT_VIEWER') {
return res.status(403).json({ error: 'Only Network Agents (or Admins in View As mode) can modify blacklist rules.' });
}
const filter = getBaseFilter(req);
const site_uuid = filter.site_uuid;
const agent_uuid = filter.agent_uuid;
if (!site_uuid) {
return res.status(400).json({ error: 'Site UUID is required' });
}
if (!agent_uuid) {
return res.status(400).json({ error: 'Agent UUID is required' });
}
const ruleId = req.params.id;
const result = await BlacklistRule.deleteOne({ _id: ruleId, site_uuid, agent_uuid });
if (result.deletedCount === 0) {
return res.status(404).json({ error: 'Blacklist rule not found' });
}
return res.json({ ok: true, message: 'Blacklist rule deleted' });
} catch (err) {
console.error('[Blacklist DELETE] Error:', err.message);
return res.status(500).json({ error: 'Internal server error' });
}
});
module.exports = router;
+23
View File
@@ -61,10 +61,33 @@ router.get('/devices', async (req, res) => {
// POST /api/dashboard/devices/update-label
router.post('/devices/update-label', 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: 'Only administrators can update device labels.' });
}
const { mac_address, device_label } = req.body;
if (!mac_address) return res.status(400).json({ ok: false, error: 'mac_address required' });
if (device_label === undefined) return res.status(400).json({ ok: false, error: 'device_label required' });
const isGlobalUser = req.user?.role === 'SUPER_ADMIN' ||
((!req.user?.site_uuid || req.user?.site_uuid === 'default') && ['SOC_ANALYST', 'ENGINEER'].includes(req.user?.role)) ||
req.user?._originalRole === 'SUPER_ADMIN';
if (!isGlobalUser && req.user?.site_uuid) {
const deviceExists = await DeviceStat.findOne({
mac_address,
site_uuid: req.user.site_uuid
});
if (!deviceExists) {
return res.status(403).json({ ok: false, error: 'Unauthorized: This device does not belong to your tenant.' });
}
}
await CustomDeviceLabel.findOneAndUpdate(
{ mac_address },
{ device_label },
-5
View File
@@ -6,11 +6,9 @@ const { getTimeFilter, getBaseFilter } = require('./helpers');
// GET /api/dashboard/events
router.get('/events', async (req, res) => {
try {
console.log('[/events] Request received. Query:', req.query);
const limit = parseInt(req.query.limit ?? 0);
const timeFilter = getTimeFilter(req);
const base = getBaseFilter(req, timeFilter);
console.log('[/events] Event base filter:', base);
let query = Event.find(base).sort({ timestamp: -1 });
if (limit > 0) {
@@ -26,13 +24,10 @@ router.get('/events', async (req, res) => {
let macToIpMap = {};
if (missingIpMacs.length > 0) {
const baseFilterNull = getBaseFilter(req, null);
console.log('[/events] getBaseFilter(req, null) returned:', baseFilterNull);
const filterForDevices = {
mac_address: { $in: missingIpMacs },
...baseFilterNull
};
console.log('[/events] DEBUG filterForDevices:', filterForDevices);
const devices = await DeviceStat.find(filterForDevices).lean();
for (const d of devices) {
macToIpMap[d.mac_address] = d.ip_address;
+20 -2
View File
@@ -6,18 +6,36 @@ const { getTimeFilter, getBaseFilter, topFlowField } = require('./helpers');
// GET /api/dashboard/flows
router.get('/flows', async (req, res) => {
try {
const limit = parseInt(req.query.limit ?? 50);
const rawLimit = parseInt(req.query.limit ?? 50);
const skip = parseInt(req.query.skip ?? 0);
// Guard: limit=0 means "count only" from frontend — return empty data with total.
// Cap at 20000 per Rule 14 to prevent server memory overload.
const limit = rawLimit <= 0 ? 0 : Math.min(rawLimit, 20000);
const timeFilter = getTimeFilter(req);
const query = getBaseFilter(req, timeFilter);
if (limit === 0) {
// Frontend is requesting total count only (for pagination), not actual rows
const total = await Flow.countDocuments(query);
return res.json({ ok: true, data: [], total });
}
// When an explicit calendar date range is active, sort OLDEST FIRST so
// historical data (e.g., July 13) appears before more recent data (July 14).
// Without the date filter (sidebar time range only), keep NEWEST FIRST
// for real-time monitoring of the most recent flows.
const hasExplicitDateRange = !!(req.query.date_from || req.query.date_to);
const sortOrder = hasExplicitDateRange ? 1 : -1;
const raw = await Flow
.find(query)
.sort({ timestamp: -1 })
.sort({ timestamp: sortOrder })
.skip(skip)
.limit(limit)
.lean();
const data = raw.map(f => {
const port = f.dst_port ?? 0;
const proto = f.protocol || 'TCP';
+18 -4
View File
@@ -1,13 +1,26 @@
const { CustomDeviceLabel, Flow } = require('../../models/Schemas');
function getTimeFilter(req) {
// Explicit calendar date range (from the per-page date picker) takes priority
// over the global sidebar time range. Both dates are interpreted as WIB (UTC+7)
// to match the dashboard's display timezone (Rule 20).
const dateFrom = req.query.date_from;
const dateTo = req.query.date_to;
if (dateFrom || dateTo) {
const filter = {};
if (dateFrom) filter.$gte = new Date(`${dateFrom}T00:00:00.000+07:00`);
if (dateTo) filter.$lte = new Date(`${dateTo}T23:59:59.999+07:00`);
return filter;
}
// Fall back to sidebar global time range
const range = req.query.timeRange || '1d';
if (range === 'all') return null;
const now = new Date();
const ms = {
'5m': 5 * 60000,
'30m': 30 * 60000,
'1h': 60 * 60000,
'1h': 60 * 3600000,
'1d': 24 * 3600000,
'7d': 7 * 24 * 3600000,
};
@@ -15,16 +28,17 @@ function getTimeFilter(req) {
return { $gte: new Date(now.getTime() - delta) };
}
function getBaseFilter(req, timeFilter = null) {
const filter = {};
if (timeFilter) filter.timestamp = timeFilter;
const requestedSiteUuid = req.headers['x-backone-site-uuid'];
console.log('[DEBUG] getBaseFilter headers:', Object.keys(req.headers), 'x-backone-site-uuid:', requestedSiteUuid, 'role:', req.user?.role);
const hasSwitcherRole = ['SUPER_ADMIN', 'SOC_ANALYST', 'ENGINEER'].includes(req.user?.role);
const isGlobalUser = req.user?.role === 'SUPER_ADMIN' ||
((!req.user?.site_uuid || req.user?.site_uuid === 'default') && ['SOC_ANALYST', 'ENGINEER'].includes(req.user?.role));
if (hasSwitcherRole && requestedSiteUuid) {
if (isGlobalUser && requestedSiteUuid) {
filter.site_uuid = requestedSiteUuid;
} else if (req.user?.site_uuid) {
filter.site_uuid = req.user.site_uuid;
+75 -55
View File
@@ -11,77 +11,94 @@ router.get('/summary', async (req, res) => {
const base = getBaseFilter(req, timeFilter);
const baseWithoutTime = getBaseFilter(req, null);
const latestDoc = await Summary.findOne(baseWithoutTime).sort({ timestamp: -1 });
let latestTime = null;
let bandwidthDown = 0;
let bandwidthUp = 0;
let totalDevicesCount = 0;
let activeFlowsCount = 0;
let downloadSpeed = 0;
let uploadSpeed = 0;
let latestTime = null;
if (latestDoc) {
latestTime = latestDoc.timestamp;
const summaries = await Summary.find({ ...baseWithoutTime, timestamp: latestTime }).lean();
bandwidthDown = summaries.reduce((s, r) => s + (r.bandwidth_down ?? 0), 0);
bandwidthUp = summaries.reduce((s, r) => s + (r.bandwidth_up ?? 0), 0);
totalDevicesCount = summaries.reduce((s, r) => s + (r.total_devices ?? 0), 0);
activeFlowsCount = summaries.reduce((s, r) => s + (r.active_flows ?? 0), 0);
if (base.agent_uuid) {
// ── Agent-Level Summary (View As Agent mode) ─────────────────────────────
// The proxy saves per-agent summaries with agent_uuid = <uuid>.
// Use the latest one for the scoped agent instead of site aggregates.
const latestAgentSummary = await Summary
.findOne(baseWithoutTime)
.sort({ timestamp: -1 })
.lean();
if (latestAgentSummary) {
bandwidthDown = latestAgentSummary.bandwidth_down || 0;
bandwidthUp = latestAgentSummary.bandwidth_up || 0;
activeFlowsCount = latestAgentSummary.active_flows || 0;
downloadSpeed = latestAgentSummary.download_speed || 0;
uploadSpeed = latestAgentSummary.upload_speed || 0;
latestTime = latestAgentSummary.timestamp;
}
} else {
// ── Site-Level Summary (default) ─────────────────────────────────────────
// Use site-level snapshots (agent_uuid=null) to avoid double-counting
// across agents when no specific agent scope is active.
const siteIds = baseWithoutTime.site_uuid
? [baseWithoutTime.site_uuid]
: await Summary.distinct('site_uuid', { agent_uuid: null });
for (const siteId of siteIds) {
const latestSiteSummary = await Summary
.findOne({ agent_uuid: null, site_uuid: siteId })
.sort({ timestamp: -1 })
.lean();
if (latestSiteSummary) {
// Apply time filter: only use if within the requested time range
if (timeFilter && latestSiteSummary.timestamp < timeFilter) continue;
bandwidthDown += latestSiteSummary.bandwidth_down || 0;
bandwidthUp += latestSiteSummary.bandwidth_up || 0;
activeFlowsCount += latestSiteSummary.active_flows || 0;
downloadSpeed += latestSiteSummary.download_speed || 0;
uploadSpeed += latestSiteSummary.upload_speed || 0;
if (!latestTime || latestSiteSummary.timestamp > latestTime) {
latestTime = latestSiteSummary.timestamp;
}
}
}
// Fallback: if no site-level summaries exist yet, aggregate from per-agent summaries
if (bandwidthDown === 0 && bandwidthUp === 0) {
const latestAgentDoc = await Summary.findOne(baseWithoutTime).sort({ timestamp: -1 });
if (latestAgentDoc) {
latestTime = latestAgentDoc.timestamp;
const agentSummaries = await Summary.find({ ...baseWithoutTime, timestamp: latestAgentDoc.timestamp }).lean();
bandwidthDown = agentSummaries.reduce((s, r) => s + (r.bandwidth_down ?? 0), 0);
bandwidthUp = agentSummaries.reduce((s, r) => s + (r.bandwidth_up ?? 0), 0);
activeFlowsCount = agentSummaries.reduce((s, r) => s + (r.active_flows ?? 0), 0);
downloadSpeed = agentSummaries.reduce((s, r) => s + (r.download_speed ?? 0), 0);
uploadSpeed = agentSummaries.reduce((s, r) => s + (r.upload_speed ?? 0), 0);
}
}
}
const [fallbackDevices, fallbackFlows, fallbackFlowBandwidth, realThreatsCount, realEventsCount, fallbackThreatsCount] = await Promise.all([
// Device count, Threats, Events — always use the scoped base filter
// (already contains agent_uuid when in AGENT_VIEWER mode)
const [uniqueDevices, realThreatsCount, realEventsCount] = await Promise.all([
DeviceStat.distinct('ip_address', base).then(r => r.length),
Flow.countDocuments(base),
Flow.aggregate([
{ $match: base },
{ $group: { _id: null, down: { $sum: '$download' }, up: { $sum: '$upload' } } }
]),
Threat.countDocuments(base),
Event.countDocuments(base),
Event.countDocuments({
...base,
$or: [
{ severity: { $in: ['Critical', 'High'] } },
{ category_label: 'Cybersecurity' }
]
})
]);
const flowDown = fallbackFlowBandwidth[0]?.down || 0;
const flowUp = fallbackFlowBandwidth[0]?.up || 0;
let finalDown = bandwidthDown > 0 ? bandwidthDown : flowDown;
let finalUp = bandwidthUp > 0 ? bandwidthUp : flowUp;
let finalDevices = fallbackDevices;
let finalActiveFlows = activeFlowsCount > 0 ? activeFlowsCount : fallbackFlows;
const range = req.query.timeRange || '1d';
if (range !== 'all' && range !== '1d') {
const scaleMap = {
'5m': 1 / (24 * 12),
'10m': 1 / (24 * 6),
'30m': 1 / 48,
'1h': 1 / 24,
'7d': 7,
};
const multiplier = scaleMap[range] ?? 1;
finalDown = Math.round(finalDown * multiplier);
finalUp = Math.round(finalUp * multiplier);
finalActiveFlows = Math.round(finalActiveFlows * multiplier);
}
res.json({
ok: true,
data: {
total_devices: finalDevices,
total_threats: realThreatsCount > 0 ? realThreatsCount : fallbackThreatsCount,
total_devices: uniqueDevices,
total_threats: realThreatsCount,
total_events: realEventsCount,
last_fetch: latestTime || new Date(),
bandwidth_down: finalDown,
bandwidth_up: finalUp,
active_flows: finalActiveFlows,
download_speed: latestDoc?.download_speed ?? 0,
upload_speed: latestDoc?.upload_speed ?? 0,
bandwidth_down: bandwidthDown,
bandwidth_up: bandwidthUp,
active_flows: activeFlowsCount,
download_speed: downloadSpeed,
upload_speed: uploadSpeed,
flow_speed: 0,
}
});
@@ -91,6 +108,9 @@ router.get('/summary', async (req, res) => {
}
});
// GET /api/dashboard/timeline
router.get('/timeline', async (req, res) => {
try {
+38
View File
@@ -0,0 +1,38 @@
const express = require('express');
const router = express.Router();
const { TenantConfig } = require('../../models/Schemas');
router.get('/tenant-config', async (req, res) => {
try {
let siteUuid = 'default';
// If Super Admin has a selected site (passed in x-backone-site-uuid header),
// we want them to see the branding of that selected site.
// Otherwise they see BackOne (default) branding.
const isGlobalUser = req.user?.role === 'SUPER_ADMIN' ||
((!req.user?.site_uuid || req.user?.site_uuid === 'default') && ['SOC_ANALYST', 'ENGINEER'].includes(req.user?.role));
if (isGlobalUser) {
const requestedSiteUuid = req.headers['x-backone-site-uuid'];
if (requestedSiteUuid) {
siteUuid = requestedSiteUuid;
}
} else if (req.user?.site_uuid) {
// For TENANT_ADMIN or other isolated roles, they only see their own site branding
siteUuid = req.user.site_uuid;
}
let config = await TenantConfig.findOne({ site_uuid: siteUuid });
if (!config) {
// Fallback to default branding if config is not found
config = await TenantConfig.findOne({ site_uuid: 'default' });
}
res.json({ ok: true, data: config });
} catch (err) {
console.error('[/tenant-config]', err.message);
res.status(500).json({ ok: false, error: err.message });
}
});
module.exports = router;
+4 -1
View File
@@ -281,7 +281,10 @@ router.get('/intelligence/server-discovery', async (req, res) => {
// Resolve IPs using DeviceStat
const macs = events.map(e => e.mac_address).filter(Boolean);
const devices = await DeviceStat.find({ mac_address: { $in: macs } }).lean();
const agentFilter = {};
if (query.agent_uuid) agentFilter.agent_uuid = query.agent_uuid;
if (query.site_uuid) agentFilter.site_uuid = query.site_uuid;
const devices = await DeviceStat.find({ mac_address: { $in: macs }, ...agentFilter }).lean();
const macMap = {};
devices.forEach(d => {
macMap[d.mac_address] = d;