feat(source2): lock dark mode, sans-serif typography, and all recent updates
This commit is contained in:
1 parent
dd4c8f6876
commit
9f24b56e97
259 files changed
+15596
-8966
No files matched your search
+109
-212
@@ -1,40 +1,121 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const { Flow } = require('../../models/Schemas');
|
||||
const { getTimeFilter, getBaseFilter, topFlowField } = require('./helpers');
|
||||
const { Flow, DeviceStat } = require('../../models/Schemas');
|
||||
const { getTimeFilter, getBaseFilter, topFlowField, getCustomLabelsMap } = require('./helpers');
|
||||
|
||||
// GET /api/dashboard/flows-options
|
||||
router.get('/flows-options', async (req, res) => {
|
||||
try {
|
||||
const timeFilter = getTimeFilter(req);
|
||||
const query = getBaseFilter(req, timeFilter);
|
||||
|
||||
// Parallel distinct queries on indexed keys
|
||||
const [protocols, srcIps, dstIps, dstPorts, apps, domains] = await Promise.all([
|
||||
Flow.distinct('protocol', query),
|
||||
Flow.distinct('src_ip', query),
|
||||
Flow.distinct('dst_ip', query),
|
||||
Flow.distinct('dst_port', query),
|
||||
Flow.distinct('app_label', query),
|
||||
Flow.distinct('domain', query)
|
||||
]);
|
||||
|
||||
res.json({
|
||||
ok: true,
|
||||
data: {
|
||||
protocols: protocols.filter(Boolean).sort(),
|
||||
srcIps: srcIps.filter(Boolean).sort(),
|
||||
dstIps: dstIps.filter(Boolean).sort(),
|
||||
dstPorts: dstPorts.filter(Boolean).sort().map(String),
|
||||
apps: apps.filter(Boolean).sort(),
|
||||
domains: domains.filter(Boolean).sort()
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
res.status(500).json({ ok: false, error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/dashboard/flows
|
||||
router.get('/flows', async (req, res) => {
|
||||
try {
|
||||
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 limit = rawLimit <= 0 ? 0 : Math.min(rawLimit, 1000000);
|
||||
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 });
|
||||
// Apply query filters on MongoDB
|
||||
if (req.query.protocol && req.query.protocol !== 'All') {
|
||||
query.protocol = req.query.protocol;
|
||||
}
|
||||
if (req.query.src_ip && req.query.src_ip !== 'All') {
|
||||
query.src_ip = req.query.src_ip;
|
||||
}
|
||||
if (req.query.dst_ip && req.query.dst_ip !== 'All') {
|
||||
query.dst_ip = req.query.dst_ip;
|
||||
}
|
||||
if (req.query.dst_port && req.query.dst_port !== 'All') {
|
||||
query.dst_port = parseInt(req.query.dst_port);
|
||||
}
|
||||
if (req.query.app && req.query.app !== 'All') {
|
||||
query.app_label = req.query.app;
|
||||
}
|
||||
if (req.query.domain && req.query.domain !== 'All') {
|
||||
query.domain = req.query.domain;
|
||||
}
|
||||
|
||||
// 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;
|
||||
if (req.query.search) {
|
||||
const q = req.query.search.trim();
|
||||
if (q) {
|
||||
query.$or = [
|
||||
{ src_ip: { $regex: q, $options: 'i' } },
|
||||
{ dst_ip: { $regex: q, $options: 'i' } }
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
const raw = await Flow
|
||||
.find(query)
|
||||
.sort({ timestamp: sortOrder })
|
||||
.skip(skip)
|
||||
.limit(limit)
|
||||
.lean();
|
||||
if (limit === 0) {
|
||||
const total = await Flow.countDocuments(query);
|
||||
console.log('[BACKEND /flows] countOnly total:', total);
|
||||
return res.json({ ok: true, data: { flows: [], total } });
|
||||
}
|
||||
|
||||
// Apply sorting
|
||||
let sortObj = { timestamp: -1 };
|
||||
if (req.query.sort_download === 'Descending') {
|
||||
sortObj = { download: -1 };
|
||||
} else if (req.query.sort_download === 'Ascending') {
|
||||
sortObj = { download: 1 };
|
||||
} else if (req.query.sort_upload === 'Descending') {
|
||||
sortObj = { upload: -1 };
|
||||
} else if (req.query.sort_upload === 'Ascending') {
|
||||
sortObj = { upload: 1 };
|
||||
} else {
|
||||
const hasExplicitDateRange = !!(req.query.date_from || req.query.date_to);
|
||||
sortObj = { timestamp: hasExplicitDateRange ? 1 : -1 };
|
||||
}
|
||||
|
||||
console.log('[BACKEND /flows] Constructed MongoDB query:', JSON.stringify(query));
|
||||
|
||||
const deviceFilter = {};
|
||||
if (query.site_uuid) deviceFilter.site_uuid = query.site_uuid;
|
||||
|
||||
const [raw, customLabelsMap, devicesList] = await Promise.all([
|
||||
Flow.find(query).sort(sortObj).skip(skip).limit(limit).lean(),
|
||||
getCustomLabelsMap(),
|
||||
DeviceStat.find(deviceFilter, { ip_address: 1, mac_address: 1 }).lean()
|
||||
]);
|
||||
|
||||
const total = await Flow.countDocuments(query);
|
||||
console.log(`[BACKEND /flows] Found total: ${total}, returning slice length: ${raw.length}`);
|
||||
|
||||
// Build IP to MAC map for real client resolution
|
||||
const ipToMacMap = {};
|
||||
devicesList.forEach(d => {
|
||||
if (d.ip_address && d.mac_address && d.mac_address !== '-') {
|
||||
ipToMacMap[d.ip_address] = d.mac_address.toLowerCase();
|
||||
}
|
||||
});
|
||||
|
||||
const data = raw.map(f => {
|
||||
const port = f.dst_port ?? 0;
|
||||
@@ -55,12 +136,18 @@ router.get('/flows', async (req, res) => {
|
||||
}
|
||||
}
|
||||
|
||||
// Try resolving MAC from IP-to-MAC map first, fallback to flow src_mac
|
||||
const flowMac = (f.src_mac || '').toLowerCase();
|
||||
const realMac = ipToMacMap[f.src_ip] || flowMac;
|
||||
const srcLabel = customLabelsMap[realMac] || customLabelsMap[flowMac] || null;
|
||||
|
||||
return {
|
||||
id: f._id?.toString(),
|
||||
fetched_at: f.timestamp,
|
||||
flow_id: f.flow_id,
|
||||
src_ip: f.src_ip,
|
||||
src_mac: f.src_mac,
|
||||
src_label: srcLabel,
|
||||
dst_ip: f.dst_ip,
|
||||
dst_port: port,
|
||||
protocol: proto,
|
||||
@@ -76,197 +163,7 @@ router.get('/flows', async (req, res) => {
|
||||
};
|
||||
});
|
||||
|
||||
res.json({ ok: true, data });
|
||||
} catch (err) {
|
||||
res.status(500).json({ ok: false, error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/dashboard/vlans
|
||||
router.get('/vlans', async (req, res) => {
|
||||
try {
|
||||
const limit = parseInt(req.query.limit ?? 20);
|
||||
const raw = await topFlowField('src_ip', req, limit);
|
||||
const map = {};
|
||||
for (const r of raw) {
|
||||
const ip = r.label;
|
||||
let vlan_id = 1;
|
||||
let vlan_label = 'VLAN-1-Default';
|
||||
|
||||
if (ip.startsWith('10.6.10.')) {
|
||||
vlan_id = 10;
|
||||
vlan_label = 'VLAN-10-Office';
|
||||
} else if (ip.startsWith('10.6.11.')) {
|
||||
vlan_id = 11;
|
||||
vlan_label = 'VLAN-11-HRD';
|
||||
} else if (ip.startsWith('10.6.12.')) {
|
||||
vlan_id = 12;
|
||||
vlan_label = 'VLAN-12-Finance';
|
||||
} else if (ip.startsWith('10.6.30.')) {
|
||||
vlan_id = 30;
|
||||
vlan_label = 'VLAN-30-Servers';
|
||||
} else if (ip.startsWith('10.250.0.')) {
|
||||
vlan_id = 250;
|
||||
vlan_label = 'VLAN-250-Core-Net';
|
||||
} else if (ip.startsWith('192.168.')) {
|
||||
vlan_id = 100;
|
||||
vlan_label = 'VLAN-100-WiFi-Guest';
|
||||
}
|
||||
|
||||
const key = String(vlan_id);
|
||||
if (!map[key]) {
|
||||
map[key] = {
|
||||
vlan_id,
|
||||
vlan_label,
|
||||
download: 0,
|
||||
upload: 0,
|
||||
total: 0
|
||||
};
|
||||
}
|
||||
map[key].download += r.download;
|
||||
map[key].upload += r.upload;
|
||||
map[key].total += (r.download + r.upload);
|
||||
}
|
||||
const data = Object.values(map).sort((a, b) => b.total - a.total).slice(0, limit);
|
||||
res.json({ ok: true, data });
|
||||
} catch (err) {
|
||||
res.status(500).json({ ok: false, error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/dashboard/interfaces
|
||||
router.get('/interfaces', async (req, res) => {
|
||||
try {
|
||||
const limit = parseInt(req.query.limit ?? 20);
|
||||
const raw = await topFlowField('src_mac', req, limit);
|
||||
const map = {};
|
||||
for (const r of raw) {
|
||||
const mac = r.label;
|
||||
let hash = 0;
|
||||
for (let i = 0; i < mac.length; i++) {
|
||||
hash = (hash << 5) - hash + mac.charCodeAt(i);
|
||||
hash = hash & hash;
|
||||
}
|
||||
const index = Math.abs(hash);
|
||||
const interfaces = [
|
||||
{ name: 'eth0 - WAN', role: 'WAN/Internet' },
|
||||
{ name: 'eth1 - LAN', role: 'LAN/Local' },
|
||||
{ name: 'eth2 - DMZ', role: 'DMZ/Protected' },
|
||||
{ name: 'wlan0', role: 'Wireless/AccessPoint' }
|
||||
];
|
||||
const selected = interfaces[index % interfaces.length];
|
||||
const key = selected.name;
|
||||
if (!map[key]) {
|
||||
map[key] = {
|
||||
iface_name: selected.name,
|
||||
iface_role: selected.role,
|
||||
agent_id: req.user?.agent_uuid || 'Global',
|
||||
download: 0,
|
||||
upload: 0,
|
||||
total: 0
|
||||
};
|
||||
}
|
||||
map[key].download += r.download;
|
||||
map[key].upload += r.upload;
|
||||
map[key].total += (r.download + r.upload);
|
||||
}
|
||||
const data = Object.values(map).sort((a, b) => b.total - a.total).slice(0, limit);
|
||||
res.json({ ok: true, data });
|
||||
} catch (err) {
|
||||
res.status(500).json({ ok: false, error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/dashboard/flow-types
|
||||
router.get('/flow-types', async (req, res) => {
|
||||
try {
|
||||
const limit = parseInt(req.query.limit ?? 10);
|
||||
const raw = await topFlowField('protocol', req, limit);
|
||||
const data = raw.map(r => {
|
||||
const proto = r.label;
|
||||
const typeLabel = proto === 'TCP' ? 'IPv4 TCP Flow' : (proto === 'UDP' ? 'IPv4 UDP Flow' : `${proto} Protocol Flow`);
|
||||
return {
|
||||
flow_type_label: typeLabel,
|
||||
download: r.download,
|
||||
upload: r.upload,
|
||||
total: r.download + r.upload
|
||||
};
|
||||
});
|
||||
res.json({ ok: true, data });
|
||||
} catch (err) {
|
||||
res.status(500).json({ ok: false, error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/dashboard/flow-origins
|
||||
router.get('/flow-origins', async (req, res) => {
|
||||
try {
|
||||
const limit = parseInt(req.query.limit ?? 10);
|
||||
const raw = await topFlowField('src_ip', req, limit);
|
||||
const map = {};
|
||||
for (const r of raw) {
|
||||
const ip = r.label;
|
||||
let origin = 'Internet Inbound';
|
||||
if (ip.startsWith('10.') || ip.startsWith('192.168.') || ip.startsWith('172.')) {
|
||||
origin = 'Local Client';
|
||||
}
|
||||
if (!map[origin]) {
|
||||
map[origin] = {
|
||||
flow_origin_label: origin,
|
||||
download: 0,
|
||||
upload: 0,
|
||||
total: 0
|
||||
};
|
||||
}
|
||||
map[origin].download += r.download;
|
||||
map[origin].upload += r.upload;
|
||||
map[origin].total += (r.download + r.upload);
|
||||
}
|
||||
const data = Object.values(map).sort((a, b) => b.total - a.total).slice(0, limit);
|
||||
res.json({ ok: true, data });
|
||||
} catch (err) {
|
||||
res.status(500).json({ ok: false, error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/dashboard/ip-versions
|
||||
router.get('/ip-versions', async (req, res) => {
|
||||
try {
|
||||
const timeFilter = getTimeFilter(req);
|
||||
const matchBase = getBaseFilter(req, timeFilter);
|
||||
|
||||
const flows = await Flow.find({ ...matchBase, dst_ip: { $ne: null } }, { dst_ip: 1, download: 1, upload: 1 }).limit(20000).lean();
|
||||
let ipv4Total = 0, ipv6Total = 0;
|
||||
for (const f of flows) {
|
||||
const size = (f.download || 0) + (f.upload || 0);
|
||||
if (f.dst_ip && f.dst_ip.includes(':')) {
|
||||
ipv6Total += size;
|
||||
} else {
|
||||
ipv4Total += size;
|
||||
}
|
||||
}
|
||||
res.json({ ok: true, data: [
|
||||
{ ip_version_label: 'IPv4', total: ipv4Total },
|
||||
{ ip_version_label: 'IPv6', total: ipv6Total },
|
||||
]});
|
||||
} catch (err) {
|
||||
res.status(500).json({ ok: false, error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/dashboard/remote-ips
|
||||
router.get('/remote-ips', async (req, res) => {
|
||||
try {
|
||||
const limit = parseInt(req.query.limit ?? 20);
|
||||
const raw = await topFlowField('dst_ip', req, limit);
|
||||
const data = raw.map(r => ({
|
||||
remote_ip: r.label,
|
||||
ip_version: r.label.includes(':') ? 6 : 4,
|
||||
download: r.download,
|
||||
upload: r.upload,
|
||||
total: r.download + r.upload
|
||||
}));
|
||||
res.json({ ok: true, data });
|
||||
res.json({ ok: true, data: { flows: data, total } });
|
||||
} catch (err) {
|
||||
res.status(500).json({ ok: false, error: err.message });
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user