feat(prod): deploy web dashboard, migrate mongodb configuration, resolve server components render error and fix logo static asset paths
This commit is contained in:
1 parent
4882108068
commit
b2ea883601
119 files changed
+7123
-2011
No files matched your search
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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 },
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user