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
@@ -1,63 +1,77 @@
|
||||
// backend/db/capacityTracker.js
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// MongoDB Capacity & Data Size Breakdown per Network Agent.
|
||||
// Measures logical document sizes per agent_uuid across all collections.
|
||||
// Uses $collStats (O(1)) + per-agent document counts (indexed) for speed.
|
||||
// Results are cached in memory and refreshed on each call.
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
const mongoose = require('mongoose');
|
||||
|
||||
// In-memory cache — shared with the agents/storage API endpoint
|
||||
let agentSizesCache = {}; // { agentUuid: sizeMB }
|
||||
let lastCacheUpdate = null; // Date of last successful update
|
||||
|
||||
async function logCapacityStats(prefix = '[MongoDB]') {
|
||||
try {
|
||||
if (!mongoose.connection || !mongoose.connection.db) {
|
||||
return;
|
||||
}
|
||||
if (!mongoose.connection || !mongoose.connection.db) return;
|
||||
const db = mongoose.connection.db;
|
||||
|
||||
// 1. Fetch overall dbStats
|
||||
const stats = await db.command({ dbStats: 1 });
|
||||
const dataSizeMB = (stats.dataSize / (1024 * 1024)).toFixed(2);
|
||||
const storageSizeMB = (stats.storageSize / (1024 * 1024)).toFixed(2);
|
||||
console.log(`${prefix} Capacity Used: Data Size = ${dataSizeMB} MB, Storage Size = ${storageSizeMB} MB`);
|
||||
// 1. Overall database stats (fast — reads WiredTiger metadata)
|
||||
const stats = await db.command({ dbStats: 1 });
|
||||
const dataSizeMB = (stats.dataSize / (1024 * 1024)).toFixed(2);
|
||||
const storageMB = (stats.storageSize / (1024 * 1024)).toFixed(2);
|
||||
console.log(`${prefix} Capacity Used: Data Size = ${dataSizeMB} MB, Storage Size = ${storageMB} MB`);
|
||||
|
||||
// 2. Fetch breakdown per Agent
|
||||
const agentSizes = {};
|
||||
// 2. Fast per-agent estimate: avgObjSize (from $collStats) × document count per agent
|
||||
const agentBytes = {};
|
||||
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);
|
||||
|
||||
// Check if collection contains at least one document with an agent_uuid field
|
||||
const sampleDoc = await col.findOne({ agent_uuid: { $ne: null } });
|
||||
// Check collection has agent-tagged documents
|
||||
const sampleDoc = await col.findOne({ agent_uuid: { $ne: null } }, { projection: { _id: 1 } });
|
||||
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;
|
||||
// $collStats is O(1) — reads storage engine metadata, never scans documents
|
||||
const collStatsArr = await col.aggregate([{ $collStats: { storageStats: {} } }]).toArray();
|
||||
const avgObjSize = collStatsArr[0]?.storageStats?.avgObjSize || 512; // bytes
|
||||
|
||||
// Count documents per agent using the existing agent_uuid index
|
||||
const countResult = await col.aggregate([
|
||||
{ $group: { _id: '$agent_uuid', count: { $sum: 1 } } }
|
||||
]).toArray();
|
||||
|
||||
for (const r of countResult) {
|
||||
const agent = r._id || 'Unknown';
|
||||
agentBytes[agent] = (agentBytes[agent] || 0) + (r.count * avgObjSize);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Format and log the breakdown
|
||||
const sortedAgents = Object.entries(agentSizes)
|
||||
// 3. Format, log, and update cache
|
||||
const sorted = Object.entries(agentBytes)
|
||||
.map(([agent, bytes]) => ({ agent, sizeMB: parseFloat((bytes / (1024 * 1024)).toFixed(2)) }))
|
||||
.sort((a, b) => b.sizeMB - a.sizeMB);
|
||||
|
||||
if (sortedAgents.length > 0) {
|
||||
if (sorted.length > 0) {
|
||||
console.log(`${prefix} Data Size Breakdown per Agent:`);
|
||||
for (const { agent, sizeMB } of sortedAgents) {
|
||||
for (const { agent, sizeMB } of sorted) {
|
||||
console.log(` - ${agent}: ${sizeMB.toFixed(2)} MB`);
|
||||
}
|
||||
}
|
||||
|
||||
agentSizesCache = {};
|
||||
for (const { agent, sizeMB } of sorted) {
|
||||
agentSizesCache[agent] = sizeMB;
|
||||
}
|
||||
lastCacheUpdate = new Date();
|
||||
|
||||
} catch (err) {
|
||||
console.warn(`${prefix} Could not retrieve DB capacity breakdown:`, err.message);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { logCapacityStats };
|
||||
module.exports = { logCapacityStats, agentSizesCache: () => agentSizesCache, lastCacheUpdate: () => lastCacheUpdate };
|
||||
|
||||
@@ -24,7 +24,7 @@ async function connectDB() {
|
||||
});
|
||||
console.log('[MongoDB] ✓ Connected successfully');
|
||||
const { logCapacityStats } = require('./capacityTracker');
|
||||
await logCapacityStats('[MongoDB]');
|
||||
logCapacityStats('[MongoDB]').catch(err => console.warn('[MongoDB] Capacity log failed:', err.message));
|
||||
return;
|
||||
} catch (error) {
|
||||
console.error(`[MongoDB] ✗ Attempt ${attempt} failed: ${error.message}`);
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
const jwt = require('jsonwebtoken');
|
||||
const User = require('../models/User');
|
||||
const { Summary } = require('../models/Schemas');
|
||||
|
||||
const JWT_SECRET = process.env.JWT_SECRET || 'super-secret-backone-key';
|
||||
|
||||
async function requireAuth(req, res, next) {
|
||||
const token = req.cookies?.token;
|
||||
if (!token) return res.status(401).json({ error: 'Unauthorized' });
|
||||
|
||||
try {
|
||||
req.user = jwt.verify(token, JWT_SECRET);
|
||||
|
||||
// ── VIEW-AS MODE ──────────────────────────────────────────────────────────
|
||||
const viewAsHeader = req.headers['x-view-as-agent'];
|
||||
if (viewAsHeader && (req.user.role === 'SUPER_ADMIN' || req.user.role === 'TENANT_ADMIN')) {
|
||||
try {
|
||||
const viewDecoded = jwt.verify(viewAsHeader, JWT_SECRET);
|
||||
if (viewDecoded.type === 'view-as' && viewDecoded.adminId === req.user.id && viewDecoded.viewAs) {
|
||||
const targetAgentUser = await User.findOne({ agent_uuid: viewDecoded.viewAs, role: 'AGENT_VIEWER' }).lean();
|
||||
|
||||
let targetSiteUuid = req.user.site_uuid;
|
||||
if (targetAgentUser && targetAgentUser.site_uuid) {
|
||||
targetSiteUuid = targetAgentUser.site_uuid;
|
||||
} else {
|
||||
const summaryDoc = await Summary.findOne({ agent_uuid: viewDecoded.viewAs }).lean();
|
||||
if (summaryDoc && summaryDoc.site_uuid) {
|
||||
targetSiteUuid = summaryDoc.site_uuid;
|
||||
}
|
||||
}
|
||||
|
||||
req.user = {
|
||||
...req.user,
|
||||
role: 'AGENT_VIEWER',
|
||||
agent_uuid: viewDecoded.viewAs,
|
||||
agent_label: viewDecoded.viewAsLabel,
|
||||
site_uuid: targetSiteUuid,
|
||||
_viewAsMode: true,
|
||||
_originalRole: req.user.role,
|
||||
};
|
||||
}
|
||||
} catch (viewErr) {
|
||||
console.warn('[ViewAs] Invalid view-as token, ignoring:', viewErr.message);
|
||||
}
|
||||
}
|
||||
|
||||
next();
|
||||
} catch (err) {
|
||||
res.status(401).json({ error: 'Invalid token' });
|
||||
}
|
||||
}
|
||||
|
||||
function requireAdmin(req, res, next) {
|
||||
const token = req.cookies?.token;
|
||||
if (!token) return res.status(401).json({ error: 'Not authenticated' });
|
||||
try {
|
||||
const decoded = jwt.verify(token, JWT_SECRET);
|
||||
if (decoded.role !== 'SUPER_ADMIN' && decoded.role !== 'TENANT_ADMIN' && decoded.role !== 'SOC_ANALYST') {
|
||||
return res.status(403).json({ error: 'Forbidden' });
|
||||
}
|
||||
req.adminUser = decoded;
|
||||
next();
|
||||
} catch {
|
||||
res.status(401).json({ error: 'Token tidak valid' });
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
requireAuth,
|
||||
requireAdmin,
|
||||
JWT_SECRET
|
||||
};
|
||||
@@ -135,52 +135,6 @@ const EventSchema = new mongoose.Schema({
|
||||
event_at: Date,
|
||||
}, baseOptions);
|
||||
|
||||
// ─── Compound Indexes for common dashboard queries ─────────────────────────────
|
||||
// ─── TLS Versions (per agent) ──────────────────────────────────────────────────
|
||||
const TlsVersionStatSchema = new mongoose.Schema({
|
||||
timestamp: { type: Date, required: true, index: true, expires: '7d' },
|
||||
agent_uuid: { type: String, index: true },
|
||||
site_uuid: { type: String, index: true },
|
||||
tls_version: { type: String, required: true },
|
||||
download: Number,
|
||||
upload: Number,
|
||||
flows: Number,
|
||||
}, baseOptions);
|
||||
|
||||
// ─── TLS Ciphers (per agent) ───────────────────────────────────────────────────
|
||||
const TlsCipherStatSchema = new mongoose.Schema({
|
||||
timestamp: { type: Date, required: true, index: true, expires: '7d' },
|
||||
agent_uuid: { type: String, index: true },
|
||||
site_uuid: { type: String, index: true },
|
||||
tls_cipher: { type: String, required: true },
|
||||
download: Number,
|
||||
upload: Number,
|
||||
flows: Number,
|
||||
}, baseOptions);
|
||||
|
||||
// ─── TLS Security (per agent) ──────────────────────────────────────────────────
|
||||
const TlsSecurityStatSchema = new mongoose.Schema({
|
||||
timestamp: { type: Date, required: true, index: true, expires: '7d' },
|
||||
agent_uuid: { type: String, index: true },
|
||||
site_uuid: { type: String, index: true },
|
||||
tls_security: { type: String, required: true },
|
||||
download: Number,
|
||||
upload: Number,
|
||||
flows: Number,
|
||||
}, baseOptions);
|
||||
|
||||
// ─── Country Traffic Stats (per agent) ────────────────────────────────────────
|
||||
const CountryStatSchema = new mongoose.Schema({
|
||||
timestamp: { type: Date, required: true, index: true, expires: '7d' },
|
||||
agent_uuid: { type: String, index: true },
|
||||
site_uuid: { type: String, index: true },
|
||||
country_code: { type: String, required: true },
|
||||
country_name: { type: String, default: '' },
|
||||
download: Number,
|
||||
upload: Number,
|
||||
flows: Number,
|
||||
}, baseOptions);
|
||||
|
||||
// ─── Compound Indexes for common dashboard queries ─────────────────────────────
|
||||
SummarySchema.index({ agent_uuid: 1, timestamp: -1 });
|
||||
AppStatSchema.index({ agent_uuid: 1, timestamp: -1, download: -1 });
|
||||
@@ -193,15 +147,6 @@ FlowSchema.index({ site_uuid: 1, app_label: 1, timestamp: -1 });
|
||||
ThreatSchema.index({ agent_uuid: 1, timestamp: -1 });
|
||||
AppCategoryStatSchema.index({ agent_uuid: 1, timestamp: -1 });
|
||||
EventSchema.index({ agent_uuid: 1, timestamp: -1 });
|
||||
TlsVersionStatSchema.index({ agent_uuid: 1, timestamp: -1 });
|
||||
TlsCipherStatSchema.index({ agent_uuid: 1, timestamp: -1 });
|
||||
TlsSecurityStatSchema.index({ agent_uuid: 1, timestamp: -1 });
|
||||
CountryStatSchema.index({ agent_uuid: 1, timestamp: -1 });
|
||||
|
||||
const CustomDeviceLabelSchema = new mongoose.Schema({
|
||||
mac_address: { type: String, required: true, unique: true, index: true },
|
||||
device_label: { type: String, required: true },
|
||||
}, baseOptions);
|
||||
|
||||
// ── Per-Device Per-Application Stats (synced from proxy) ─────────────────
|
||||
const DeviceAppStatSchema = new mongoose.Schema({
|
||||
@@ -220,34 +165,8 @@ DeviceAppStatSchema.index({ agent_uuid: 1, ip_address: 1, timestamp: -1 });
|
||||
DeviceAppStatSchema.index({ ip_address: 1, app_label: 1, timestamp: -1 });
|
||||
DeviceAppStatSchema.index({ site_uuid: 1, app_label: 1, timestamp: -1 });
|
||||
|
||||
// ─── View As Audit Logs ────────────────────────────────────────────────────────
|
||||
const ViewAsLogSchema = new mongoose.Schema({
|
||||
timestamp: { type: Date, default: Date.now, index: true },
|
||||
admin_id: { type: String, required: true },
|
||||
admin_username: { type: String, required: true },
|
||||
admin_role: String,
|
||||
agent_uuid: { type: String, required: true },
|
||||
agent_label: String,
|
||||
end_timestamp: Date,
|
||||
duration: Number, // duration in seconds
|
||||
}, baseOptions);
|
||||
|
||||
// ─── Lookup App Dictionary ────────────────────────────────────────────────────
|
||||
const LookupAppSchema = new mongoose.Schema({
|
||||
id: { type: Number, required: true, unique: true, index: true },
|
||||
tag: String,
|
||||
label: { type: String, index: true },
|
||||
name: String,
|
||||
full_name: String,
|
||||
description: String,
|
||||
favicon: String,
|
||||
icon: String,
|
||||
logo: String,
|
||||
application_category: Object
|
||||
}, baseOptions);
|
||||
LookupAppSchema.index({ label: 1, tag: 1 });
|
||||
|
||||
const telemetrySchemas = require('./SchemasTelemetry');
|
||||
const auxSchemas = require('./SchemasAux');
|
||||
|
||||
module.exports = {
|
||||
Summary: mongoose.model('Summary', SummarySchema),
|
||||
@@ -257,14 +176,9 @@ module.exports = {
|
||||
DeviceAppStat: mongoose.model('DeviceAppStat', DeviceAppStatSchema),
|
||||
Flow: mongoose.model('Flow', FlowSchema),
|
||||
Threat: mongoose.model('Threat', ThreatSchema),
|
||||
CustomDeviceLabel: mongoose.model('CustomDeviceLabel', CustomDeviceLabelSchema),
|
||||
AppCategoryStat: mongoose.model('AppCategoryStat', AppCategoryStatSchema),
|
||||
Event: mongoose.model('Event', EventSchema),
|
||||
TlsVersionStat: mongoose.model('TlsVersionStat', TlsVersionStatSchema),
|
||||
TlsCipherStat: mongoose.model('TlsCipherStat', TlsCipherStatSchema),
|
||||
TlsSecurityStat: mongoose.model('TlsSecurityStat', TlsSecurityStatSchema),
|
||||
CountryStat: mongoose.model('CountryStat', CountryStatSchema),
|
||||
LookupApp: mongoose.model('LookupApp', LookupAppSchema),
|
||||
ViewAsLog: mongoose.model('ViewAsLog', ViewAsLogSchema),
|
||||
...auxSchemas,
|
||||
...telemetrySchemas
|
||||
};
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
// backend/models/SchemasAux.js
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Auxiliary MongoDB Schemas to maintain Schemas.js under 256 lines limit.
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
const mongoose = require('mongoose');
|
||||
|
||||
const baseOptions = {
|
||||
timestamps: { createdAt: 'created_at', updatedAt: 'updated_at' }
|
||||
};
|
||||
|
||||
// ─── TLS Versions (per agent) ──────────────────────────────────────────────────
|
||||
const TlsVersionStatSchema = new mongoose.Schema({
|
||||
timestamp: { type: Date, required: true, index: true, expires: '7d' },
|
||||
agent_uuid: { type: String, index: true },
|
||||
site_uuid: { type: String, index: true },
|
||||
tls_version: { type: String, required: true },
|
||||
download: Number,
|
||||
upload: Number,
|
||||
flows: Number,
|
||||
}, baseOptions);
|
||||
|
||||
// ─── TLS Ciphers (per agent) ───────────────────────────────────────────────────
|
||||
const TlsCipherStatSchema = new mongoose.Schema({
|
||||
timestamp: { type: Date, required: true, index: true, expires: '7d' },
|
||||
agent_uuid: { type: String, index: true },
|
||||
site_uuid: { type: String, index: true },
|
||||
tls_cipher: { type: String, required: true },
|
||||
download: Number,
|
||||
upload: Number,
|
||||
flows: Number,
|
||||
}, baseOptions);
|
||||
|
||||
// ─── TLS Security (per agent) ──────────────────────────────────────────────────
|
||||
const TlsSecurityStatSchema = new mongoose.Schema({
|
||||
timestamp: { type: Date, required: true, index: true, expires: '7d' },
|
||||
agent_uuid: { type: String, index: true },
|
||||
site_uuid: { type: String, index: true },
|
||||
tls_security: { type: String, required: true },
|
||||
download: Number,
|
||||
upload: Number,
|
||||
flows: Number,
|
||||
}, baseOptions);
|
||||
|
||||
// ─── Country Traffic Stats (per agent) ────────────────────────────────────────
|
||||
const CountryStatSchema = new mongoose.Schema({
|
||||
timestamp: { type: Date, required: true, index: true, expires: '7d' },
|
||||
agent_uuid: { type: String, index: true },
|
||||
site_uuid: { type: String, index: true },
|
||||
country_code: { type: String, required: true },
|
||||
country_name: { type: String, default: '' },
|
||||
download: Number,
|
||||
upload: Number,
|
||||
flows: Number,
|
||||
}, baseOptions);
|
||||
|
||||
const CustomDeviceLabelSchema = new mongoose.Schema({
|
||||
mac_address: { type: String, required: true, unique: true, index: true },
|
||||
device_label: { type: String, required: true },
|
||||
}, baseOptions);
|
||||
|
||||
// ─── View As Audit Logs ────────────────────────────────────────────────────────
|
||||
const ViewAsLogSchema = new mongoose.Schema({
|
||||
timestamp: { type: Date, default: Date.now, index: true },
|
||||
admin_id: { type: String, required: true },
|
||||
admin_username: { type: String, required: true },
|
||||
admin_role: String,
|
||||
agent_uuid: { type: String, required: true },
|
||||
agent_label: String,
|
||||
end_timestamp: Date,
|
||||
duration: Number, // duration in seconds
|
||||
}, baseOptions);
|
||||
|
||||
// ─── Lookup App Dictionary ────────────────────────────────────────────────────
|
||||
const LookupAppSchema = new mongoose.Schema({
|
||||
id: { type: Number, required: true, unique: true, index: true },
|
||||
tag: String,
|
||||
label: { type: String, index: true },
|
||||
name: String,
|
||||
full_name: String,
|
||||
description: String,
|
||||
favicon: String,
|
||||
icon: String,
|
||||
logo: String,
|
||||
application_category: Object
|
||||
}, baseOptions);
|
||||
|
||||
// ─── Tenant Configuration (Dynamic Branding per site_uuid) ─────────────────────
|
||||
const TenantConfigSchema = new mongoose.Schema({
|
||||
site_uuid: { type: String, required: true, unique: true, index: true },
|
||||
brand_name: { type: String, required: true },
|
||||
brand_logo: { type: String, required: true },
|
||||
footer_copyright: { type: String, required: true },
|
||||
primary_color: { type: String, default: '#E11D48' }
|
||||
}, baseOptions);
|
||||
|
||||
const CustomAgentLocationSchema = new mongoose.Schema({
|
||||
agent_uuid: { type: String, required: true, unique: true, index: true },
|
||||
site_uuid: { type: String, required: true, index: true },
|
||||
latitude: { type: Number, required: true },
|
||||
longitude: { type: Number, required: true },
|
||||
label: { type: String, default: '' },
|
||||
}, baseOptions);
|
||||
|
||||
const BlacklistRuleSchema = new mongoose.Schema({
|
||||
site_uuid: { type: String, required: true, index: true },
|
||||
agent_uuid: { type: String, required: true, index: true },
|
||||
type: { type: String, required: true, enum: ['category', 'domain'] },
|
||||
value: { type: String, required: true },
|
||||
is_active: { type: Boolean, default: true }
|
||||
}, baseOptions);
|
||||
|
||||
// Set compound indexes
|
||||
TlsVersionStatSchema.index({ agent_uuid: 1, timestamp: -1 });
|
||||
TlsCipherStatSchema.index({ agent_uuid: 1, timestamp: -1 });
|
||||
TlsSecurityStatSchema.index({ agent_uuid: 1, timestamp: -1 });
|
||||
CountryStatSchema.index({ agent_uuid: 1, timestamp: -1 });
|
||||
LookupAppSchema.index({ label: 1, tag: 1 });
|
||||
BlacklistRuleSchema.index({ site_uuid: 1, agent_uuid: 1, type: 1, value: 1 }, { unique: true });
|
||||
|
||||
module.exports = {
|
||||
TlsVersionStat: mongoose.model('TlsVersionStat', TlsVersionStatSchema),
|
||||
TlsCipherStat: mongoose.model('TlsCipherStat', TlsCipherStatSchema),
|
||||
TlsSecurityStat: mongoose.model('TlsSecurityStat',TlsSecurityStatSchema),
|
||||
CountryStat: mongoose.model('CountryStat', CountryStatSchema),
|
||||
CustomDeviceLabel:mongoose.model('CustomDeviceLabel',CustomDeviceLabelSchema),
|
||||
ViewAsLog: mongoose.model('ViewAsLog', ViewAsLogSchema),
|
||||
LookupApp: mongoose.model('LookupApp', LookupAppSchema),
|
||||
TenantConfig: mongoose.model('TenantConfig', TenantConfigSchema),
|
||||
CustomAgentLocation: mongoose.model('CustomAgentLocation', CustomAgentLocationSchema),
|
||||
BlacklistRule: mongoose.model('BlacklistRule', BlacklistRuleSchema),
|
||||
};
|
||||
@@ -17,6 +17,7 @@ const UserSchema = new mongoose.Schema({
|
||||
role: { type: String, enum: ['SUPER_ADMIN', 'TENANT_ADMIN', 'SOC_ANALYST', 'ENGINEER', 'AGENT_VIEWER'], default: 'AGENT_VIEWER' },
|
||||
site_uuid: { type: String, default: null, index: true },
|
||||
agent_uuid: { type: String, default: null },
|
||||
created_by: { type: String, default: null, index: true },
|
||||
is_active: { type: Boolean, default: true },
|
||||
}, {
|
||||
timestamps: { createdAt: 'created_at', updatedAt: 'updated_at' }
|
||||
|
||||
@@ -13,7 +13,10 @@ module.exports = async function agentDetailsHandler(req, res, helpers) {
|
||||
getCustomLabelsMap
|
||||
} = helpers;
|
||||
|
||||
const uuid = String(req.query.uuid ?? '');
|
||||
let uuid = String(req.query.uuid ?? '');
|
||||
if (req.user?.role === 'AGENT_VIEWER' && req.user?.agent_uuid) {
|
||||
uuid = req.user.agent_uuid;
|
||||
}
|
||||
if (!uuid) return res.status(400).json({ ok: false, message: 'uuid required' });
|
||||
|
||||
const timeFilter = getTimeFilter(req);
|
||||
@@ -268,8 +271,14 @@ module.exports = async function agentDetailsHandler(req, res, helpers) {
|
||||
// 9. Server Discovery
|
||||
const server_discovery = [];
|
||||
|
||||
// Find the user object of this agent to get its name/label
|
||||
const agentUser = await User.findOne({ agent_uuid: uuid, role: 'AGENT_VIEWER' });
|
||||
const userQuery = { agent_uuid: uuid, role: 'AGENT_VIEWER' };
|
||||
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) {
|
||||
userQuery.site_uuid = req.user.site_uuid;
|
||||
}
|
||||
|
||||
const agentUser = await User.findOne(userQuery);
|
||||
const agent_label = agentUser?.account_name || uuid;
|
||||
|
||||
const devicesDl = devices.reduce((sum, d) => sum + d.download, 0);
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
const axios = require('axios');
|
||||
|
||||
let appLookupCache = null;
|
||||
let agentMapCache = null;
|
||||
let agentCachePopulating = false;
|
||||
|
||||
function timeRangeToMinutes(timeRange) {
|
||||
const mapping = {
|
||||
'5m': 5, '10m': 10, '30m': 30, '1h': 60,
|
||||
'1d': 1440, '7d': 10080, '30d': 43200, 'all': 43200
|
||||
};
|
||||
return mapping[timeRange] ?? 60;
|
||||
}
|
||||
|
||||
// Resolve agent UUID → DPI numeric agent ID
|
||||
async function populateAgentCache(BASE_URL, token, siteUuid) {
|
||||
if (agentMapCache !== null || agentCachePopulating) return;
|
||||
agentCachePopulating = true;
|
||||
try {
|
||||
const headers = { 'x-api-key': token, 'Accept': 'application/json' };
|
||||
if (siteUuid) headers['x-net-site'] = siteUuid;
|
||||
const res = await axios.get(`${BASE_URL}/data/stats/top/agent/download`, {
|
||||
headers, params: { filter_interval: 43200, settings_limit: 100 }, timeout: 4000
|
||||
});
|
||||
agentMapCache = {};
|
||||
if (res.data && Array.isArray(res.data.data)) {
|
||||
res.data.data.forEach(r => {
|
||||
if (r.agent?.uuid && r.agent?.id) agentMapCache[r.agent.uuid] = r.agent.id;
|
||||
});
|
||||
}
|
||||
console.log(`[AppDetailsDpiHelper] Agent cache: ${Object.keys(agentMapCache).length} agents`);
|
||||
} catch (e) {
|
||||
agentMapCache = {};
|
||||
console.warn('[AppDetailsDpiHelper] Agent cache failed:', e.message);
|
||||
} finally {
|
||||
agentCachePopulating = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve app label → DPI application ID
|
||||
async function populateAppCache(BASE_URL, token, siteUuid) {
|
||||
if (appLookupCache !== null) return;
|
||||
try {
|
||||
const headers = { 'x-api-key': token, 'Accept': 'application/json' };
|
||||
if (siteUuid) headers['x-net-site'] = siteUuid;
|
||||
const res = await axios.get(`${BASE_URL}/lookup/applications`, {
|
||||
headers, params: { settings_limit: 2000 }, timeout: 8000
|
||||
});
|
||||
appLookupCache = {};
|
||||
if (res.data && Array.isArray(res.data.data)) {
|
||||
res.data.data.forEach(a => {
|
||||
if (!a.label || !a.id) return;
|
||||
let domain = null;
|
||||
if (a.home_page?.url) {
|
||||
domain = a.home_page.url.replace(/^https?:\/\/(www\.)?/, '').split('/')[0];
|
||||
} else if (a.domain_list?.length > 0) {
|
||||
domain = a.domain_list[0].label;
|
||||
} else {
|
||||
domain = a.label.toLowerCase();
|
||||
}
|
||||
appLookupCache[a.label.toLowerCase()] = { id: a.id, label: a.label, domain };
|
||||
});
|
||||
}
|
||||
console.log(`[AppDetailsDpiHelper] App cache: ${Object.keys(appLookupCache).length} apps`);
|
||||
} catch (e) {
|
||||
appLookupCache = {};
|
||||
console.warn('[AppDetailsDpiHelper] App cache failed:', e.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Core DPI fetch for app-details
|
||||
async function fetchFromDpiApi(label, agentUuid, timeRange, token, siteUuid) {
|
||||
const BASE_URL = process.env.NETIFY_INFORMATICS_BASE_URL || 'https://informatics.netify.ai/api/v1';
|
||||
const headers = { 'x-api-key': token, 'Accept': 'application/json', 'x-net-site': siteUuid };
|
||||
|
||||
const TIMEOUT_MS = 12000;
|
||||
const deadline = new Promise((_, reject) =>
|
||||
setTimeout(() => reject(new Error(`AppDetailsDpiHelper: ${TIMEOUT_MS}ms timeout`)), TIMEOUT_MS)
|
||||
);
|
||||
|
||||
async function doFetch() {
|
||||
await Promise.all([
|
||||
populateAgentCache(BASE_URL, token, siteUuid),
|
||||
populateAppCache(BASE_URL, token, siteUuid)
|
||||
]);
|
||||
|
||||
const appInfo = appLookupCache?.[label.toLowerCase()];
|
||||
if (!appInfo) {
|
||||
console.warn(`[AppDetailsDpiHelper] App "${label}" not found in lookup cache`);
|
||||
return null;
|
||||
}
|
||||
|
||||
const params = {
|
||||
filter_interval: timeRangeToMinutes(timeRange),
|
||||
filter_applications: `["${appInfo.id}"]`,
|
||||
settings_limit: 10000
|
||||
};
|
||||
|
||||
if (agentUuid && agentMapCache?.[agentUuid]) {
|
||||
params.filter_agents = `[${agentMapCache[agentUuid]}]`;
|
||||
}
|
||||
|
||||
const [dlRes, ulRes] = await Promise.all([
|
||||
axios.get(`${BASE_URL}/data/stats/top/local_ip/download`, { headers, params, timeout: 10000 }),
|
||||
axios.get(`${BASE_URL}/data/stats/top/local_ip/upload`, { headers, params, timeout: 10000 }),
|
||||
]);
|
||||
|
||||
const ipsMap = {};
|
||||
(dlRes.data?.data || []).forEach(item => {
|
||||
const ip = item.local_ip?.address;
|
||||
if (!ip) return;
|
||||
if (!ipsMap[ip]) {
|
||||
ipsMap[ip] = {
|
||||
ip_address: ip,
|
||||
download: item.download || 0,
|
||||
upload: 0,
|
||||
first_seen: item.last_seen_at?.date || new Date().toISOString(),
|
||||
last_seen: item.last_seen_at?.date || new Date().toISOString(),
|
||||
domain: appInfo.domain,
|
||||
protocol: 'HTTPS / TLS'
|
||||
};
|
||||
} else {
|
||||
ipsMap[ip].download = item.download || 0;
|
||||
}
|
||||
});
|
||||
|
||||
(ulRes.data?.data || []).forEach(item => {
|
||||
const ip = item.local_ip?.address;
|
||||
if (!ip) return;
|
||||
if (!ipsMap[ip]) {
|
||||
ipsMap[ip] = {
|
||||
ip_address: ip,
|
||||
download: 0,
|
||||
upload: item.upload || 0,
|
||||
first_seen: item.last_seen_at?.date || new Date().toISOString(),
|
||||
last_seen: item.last_seen_at?.date || new Date().toISOString(),
|
||||
domain: appInfo.domain,
|
||||
protocol: 'HTTPS / TLS'
|
||||
};
|
||||
} else {
|
||||
ipsMap[ip].upload = item.upload || 0;
|
||||
if (item.last_seen_at?.date) {
|
||||
const d = new Date(item.last_seen_at.date);
|
||||
if (d > new Date(ipsMap[ip].last_seen)) ipsMap[ip].last_seen = item.last_seen_at.date;
|
||||
if (d < new Date(ipsMap[ip].first_seen)) ipsMap[ip].first_seen = item.last_seen_at.date;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const top_ips = Object.values(ipsMap).sort((a, b) => (b.download + b.upload) - (a.download + a.upload));
|
||||
const totalDl = top_ips.reduce((s, x) => s + x.download, 0);
|
||||
const totalUl = top_ips.reduce((s, x) => s + x.upload, 0);
|
||||
|
||||
console.log(`[AppDetailsDpiHelper] DPI API: label=${label} agent=${agentUuid} top_ips=${top_ips.length} dl=${(totalDl/1e9).toFixed(2)}GB`);
|
||||
return { top_ips, totalDl, totalUl };
|
||||
}
|
||||
|
||||
try {
|
||||
return await Promise.race([doFetch(), deadline]);
|
||||
} catch (err) {
|
||||
console.warn('[AppDetailsDpiHelper] DPI API timeout/error:', err.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getAppLookupCache: () => appLookupCache,
|
||||
populateAppCache,
|
||||
fetchFromDpiApi
|
||||
};
|
||||
@@ -10,169 +10,7 @@
|
||||
const axios = require('axios');
|
||||
const { AppStat, DeviceAppStat, Flow } = require('../models/Schemas');
|
||||
|
||||
// ─── Shared in-memory caches (for DPI API fallback only) ─────────────────────
|
||||
let appLookupCache = null;
|
||||
let agentMapCache = null;
|
||||
let agentCachePopulating = false;
|
||||
|
||||
function timeRangeToMinutes(timeRange) {
|
||||
const mapping = {
|
||||
'5m': 5, '10m': 10, '30m': 30, '1h': 60,
|
||||
'1d': 1440, '7d': 10080, '30d': 43200, 'all': 43200
|
||||
};
|
||||
return mapping[timeRange] ?? 60;
|
||||
}
|
||||
|
||||
// Resolve agent UUID → DPI numeric agent ID (for filter_agents param)
|
||||
async function populateAgentCache(BASE_URL, token, siteUuid) {
|
||||
if (agentMapCache !== null || agentCachePopulating) return;
|
||||
agentCachePopulating = true;
|
||||
try {
|
||||
const headers = { 'x-api-key': token, 'Accept': 'application/json' };
|
||||
if (siteUuid) headers['x-net-site'] = siteUuid;
|
||||
const res = await axios.get(`${BASE_URL}/data/stats/top/agent/download`, {
|
||||
headers, params: { filter_interval: 43200, settings_limit: 100 }, timeout: 4000
|
||||
});
|
||||
agentMapCache = {};
|
||||
if (res.data && Array.isArray(res.data.data)) {
|
||||
res.data.data.forEach(r => {
|
||||
if (r.agent?.uuid && r.agent?.id) agentMapCache[r.agent.uuid] = r.agent.id;
|
||||
});
|
||||
}
|
||||
console.log(`[AppDetailsHandler] Agent cache: ${Object.keys(agentMapCache).length} agents`);
|
||||
} catch (e) {
|
||||
agentMapCache = {};
|
||||
console.warn('[AppDetailsHandler] Agent cache failed:', e.message);
|
||||
} finally {
|
||||
agentCachePopulating = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve app label → DPI application ID
|
||||
async function populateAppCache(BASE_URL, token, siteUuid) {
|
||||
if (appLookupCache !== null) return;
|
||||
try {
|
||||
const headers = { 'x-api-key': token, 'Accept': 'application/json' };
|
||||
if (siteUuid) headers['x-net-site'] = siteUuid;
|
||||
const res = await axios.get(`${BASE_URL}/lookup/applications`, {
|
||||
headers, params: { settings_limit: 2000 }, timeout: 8000
|
||||
});
|
||||
appLookupCache = {};
|
||||
if (res.data && Array.isArray(res.data.data)) {
|
||||
res.data.data.forEach(a => {
|
||||
if (!a.label || !a.id) return;
|
||||
let domain = null;
|
||||
if (a.home_page?.url) {
|
||||
domain = a.home_page.url.replace(/^https?:\/\/(www\.)?/, '').split('/')[0];
|
||||
} else if (a.domain_list?.length > 0) {
|
||||
domain = a.domain_list[0].label;
|
||||
} else {
|
||||
domain = a.label.toLowerCase();
|
||||
}
|
||||
appLookupCache[a.label.toLowerCase()] = { id: a.id, label: a.label, domain };
|
||||
});
|
||||
}
|
||||
console.log(`[AppDetailsHandler] App cache: ${Object.keys(appLookupCache).length} apps`);
|
||||
} catch (e) {
|
||||
appLookupCache = {};
|
||||
console.warn('[AppDetailsHandler] App cache failed:', e.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Core DPI fetch for app-details — only used when MongoDB has no data
|
||||
async function fetchFromDpiApi(label, agentUuid, timeRange, token, siteUuid) {
|
||||
const BASE_URL = process.env.NETIFY_INFORMATICS_BASE_URL || 'https://informatics.netify.ai/api/v1';
|
||||
const headers = { 'x-api-key': token, 'Accept': 'application/json', 'x-net-site': siteUuid };
|
||||
|
||||
const TIMEOUT_MS = 12000;
|
||||
const deadline = new Promise((_, reject) =>
|
||||
setTimeout(() => reject(new Error(`AppDetailsHandler: ${TIMEOUT_MS}ms timeout`)), TIMEOUT_MS)
|
||||
);
|
||||
|
||||
async function doFetch() {
|
||||
await Promise.all([
|
||||
populateAgentCache(BASE_URL, token, siteUuid),
|
||||
populateAppCache(BASE_URL, token, siteUuid)
|
||||
]);
|
||||
|
||||
const appInfo = appLookupCache?.[label.toLowerCase()];
|
||||
if (!appInfo) {
|
||||
console.warn(`[AppDetailsHandler] App "${label}" not found in lookup cache`);
|
||||
return null;
|
||||
}
|
||||
|
||||
const params = {
|
||||
filter_interval: timeRangeToMinutes(timeRange),
|
||||
filter_applications: `["${appInfo.id}"]`,
|
||||
settings_limit: 10000
|
||||
};
|
||||
|
||||
if (agentUuid && agentMapCache?.[agentUuid]) {
|
||||
params.filter_agents = `[${agentMapCache[agentUuid]}]`;
|
||||
}
|
||||
|
||||
const [dlRes, ulRes] = await Promise.all([
|
||||
axios.get(`${BASE_URL}/data/stats/top/local_ip/download`, { headers, params, timeout: 10000 }),
|
||||
axios.get(`${BASE_URL}/data/stats/top/local_ip/upload`, { headers, params, timeout: 10000 }),
|
||||
]);
|
||||
|
||||
const ipsMap = {};
|
||||
(dlRes.data?.data || []).forEach(item => {
|
||||
const ip = item.local_ip?.address;
|
||||
if (!ip) return;
|
||||
if (!ipsMap[ip]) {
|
||||
ipsMap[ip] = {
|
||||
ip_address: ip,
|
||||
download: item.download || 0,
|
||||
upload: 0,
|
||||
first_seen: item.last_seen_at?.date || new Date().toISOString(),
|
||||
last_seen: item.last_seen_at?.date || new Date().toISOString(),
|
||||
domain: appInfo.domain,
|
||||
protocol: 'HTTPS / TLS'
|
||||
};
|
||||
} else {
|
||||
ipsMap[ip].download = item.download || 0;
|
||||
}
|
||||
});
|
||||
|
||||
(ulRes.data?.data || []).forEach(item => {
|
||||
const ip = item.local_ip?.address;
|
||||
if (!ip) return;
|
||||
if (!ipsMap[ip]) {
|
||||
ipsMap[ip] = {
|
||||
ip_address: ip,
|
||||
download: 0,
|
||||
upload: item.upload || 0,
|
||||
first_seen: item.last_seen_at?.date || new Date().toISOString(),
|
||||
last_seen: item.last_seen_at?.date || new Date().toISOString(),
|
||||
domain: appInfo.domain,
|
||||
protocol: 'HTTPS / TLS'
|
||||
};
|
||||
} else {
|
||||
ipsMap[ip].upload = item.upload || 0;
|
||||
if (item.last_seen_at?.date) {
|
||||
const d = new Date(item.last_seen_at.date);
|
||||
if (d > new Date(ipsMap[ip].last_seen)) ipsMap[ip].last_seen = item.last_seen_at.date;
|
||||
if (d < new Date(ipsMap[ip].first_seen)) ipsMap[ip].first_seen = item.last_seen_at.date;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const top_ips = Object.values(ipsMap).sort((a, b) => (b.download + b.upload) - (a.download + a.upload));
|
||||
const totalDl = top_ips.reduce((s, x) => s + x.download, 0);
|
||||
const totalUl = top_ips.reduce((s, x) => s + x.upload, 0);
|
||||
|
||||
console.log(`[AppDetailsHandler] DPI API: label=${label} agent=${agentUuid} top_ips=${top_ips.length} dl=${(totalDl/1e9).toFixed(2)}GB`);
|
||||
return { top_ips, totalDl, totalUl };
|
||||
}
|
||||
|
||||
try {
|
||||
return await Promise.race([doFetch(), deadline]);
|
||||
} catch (err) {
|
||||
console.warn('[AppDetailsHandler] DPI API timeout/error:', err.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
const { getAppLookupCache, populateAppCache, fetchFromDpiApi } = require('./appDetailsDpiHelper');
|
||||
|
||||
// ─── Main Handler ─────────────────────────────────────────────────────────────
|
||||
module.exports = async function appDetailsHandler(req, res, helpers) {
|
||||
@@ -189,7 +27,10 @@ module.exports = async function appDetailsHandler(req, res, helpers) {
|
||||
const timeFilter = getTimeFilter(req);
|
||||
const baseFilter = getBaseFilter(req, timeFilter);
|
||||
|
||||
const agentUuid = String(req.query.agent_uuid ?? '') || req.user?.agent_uuid || null;
|
||||
let agentUuid = req.user?.agent_uuid || null;
|
||||
if (req.user?.role !== 'AGENT_VIEWER') {
|
||||
agentUuid = String(req.query.agent_uuid ?? '') || agentUuid;
|
||||
}
|
||||
if (agentUuid) baseFilter.agent_uuid = agentUuid;
|
||||
|
||||
// ── Step 1: Query DeviceAppStat (Primary source for per-device bandwidth per-app) ──
|
||||
@@ -202,7 +43,7 @@ module.exports = async function appDetailsHandler(req, res, helpers) {
|
||||
if (token && SITE_UUID) {
|
||||
await populateAppCache(BASE_URL, token, SITE_UUID).catch(e => console.warn('[AppDetails] Cache error:', e.message));
|
||||
}
|
||||
const appMeta = appLookupCache?.[label.toLowerCase()];
|
||||
const appMeta = getAppLookupCache()?.[label.toLowerCase()];
|
||||
|
||||
const ipsMap = {};
|
||||
deviceApps.forEach(da => {
|
||||
|
||||
+114
-9
@@ -5,9 +5,11 @@ const jwt = require('jsonwebtoken');
|
||||
const User = require('../../models/User');
|
||||
const { makeToken, setCookieToken, requireAuth, JWT_SECRET } = require('./helpers');
|
||||
|
||||
const { TenantConfig, CustomAgentLocation } = require('../../models/Schemas');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// ─── Auto-seed SUPER_ADMIN dan SOC_ANALYST jika belum ada ───────────────────────
|
||||
// ─── Auto-seed SUPER_ADMIN, SOC_ANALYST, dan TENANT_ADMIN jika belum ada ─────────
|
||||
(async () => {
|
||||
try {
|
||||
const count = await User.countDocuments({ role: 'SUPER_ADMIN' });
|
||||
@@ -25,19 +27,122 @@ const router = express.Router();
|
||||
console.log('[Auth] ⚠ GANTI PASSWORD INI SEGERA DI PRODUCTION!');
|
||||
}
|
||||
|
||||
const analystCount = await User.countDocuments({ role: 'SOC_ANALYST' });
|
||||
if (analystCount === 0) {
|
||||
const hash = bcrypt.hashSync('analyst', 10);
|
||||
const siabCount = await User.countDocuments({ username: 'siab' });
|
||||
if (siabCount === 0) {
|
||||
const hash = bcrypt.hashSync('siab', 10);
|
||||
await User.create({
|
||||
username: 'analyst',
|
||||
username: 'siab',
|
||||
password_hash: hash,
|
||||
account_name: 'BackOne SOC Analyst',
|
||||
role: 'SOC_ANALYST',
|
||||
site_uuid: process.env.NETIFY_SITE_UUID || null,
|
||||
account_name: 'SIAB Administrator',
|
||||
role: 'TENANT_ADMIN',
|
||||
site_uuid: '6681452d_9cae_4ff4_8ae8_0d504774265e',
|
||||
agent_uuid: null,
|
||||
});
|
||||
console.log('[Auth] ✓ Default SOC_ANALYST created: analyst / analyst');
|
||||
console.log('[Auth] ✓ Default SIAB Tenant created: siab / siab');
|
||||
}
|
||||
|
||||
const nexusCount = await User.countDocuments({ username: 'nexus' });
|
||||
if (nexusCount === 0) {
|
||||
const hash = bcrypt.hashSync('nexus', 10);
|
||||
await User.create({
|
||||
username: 'nexus',
|
||||
password_hash: hash,
|
||||
account_name: 'Nexus Administrator',
|
||||
role: 'TENANT_ADMIN',
|
||||
site_uuid: 'd7902405_0dc2_458b_8584_ed4d24b64f24',
|
||||
agent_uuid: null,
|
||||
});
|
||||
console.log('[Auth] ✓ Default Nexus Tenant created: nexus / nexus');
|
||||
}
|
||||
|
||||
// Repair/Migration: Ensure legacy users have appropriate created_by values
|
||||
try {
|
||||
const missingCreatedBy = await User.find({ $or: [{ created_by: { $exists: false } }, { created_by: null }] });
|
||||
if (missingCreatedBy.length > 0) {
|
||||
console.log(`[Auth] Migrating ${missingCreatedBy.length} legacy users to set created_by...`);
|
||||
for (const u of missingCreatedBy) {
|
||||
if (u.username === 'admin') {
|
||||
u.created_by = 'admin';
|
||||
} else if (u.site_uuid === '6681452d_9cae_4ff4_8ae8_0d504774265e') {
|
||||
u.created_by = 'siab';
|
||||
} else if (u.site_uuid === 'd7902405_0dc2_458b_8584_ed4d24b64f24') {
|
||||
u.created_by = 'nexus';
|
||||
} else {
|
||||
u.created_by = 'admin';
|
||||
}
|
||||
await u.save();
|
||||
}
|
||||
console.log(`[Auth] Migration complete.`);
|
||||
}
|
||||
} catch (migrateErr) {
|
||||
console.error('[Auth] Migration failed:', migrateErr.message);
|
||||
}
|
||||
|
||||
const defaultConfigs = [
|
||||
{
|
||||
site_uuid: 'default',
|
||||
brand_name: 'BackOne',
|
||||
brand_logo: '/backone-logo.png',
|
||||
footer_copyright: 'PT. Data Bisnis Solusi',
|
||||
primary_color: '#E11D48',
|
||||
},
|
||||
{
|
||||
site_uuid: '6681452d_9cae_4ff4_8ae8_0d504774265e',
|
||||
brand_name: 'SIAB',
|
||||
brand_logo: '/siab-logo.png',
|
||||
footer_copyright: 'PT. SIAB Indonesia',
|
||||
primary_color: '#3B82F6',
|
||||
},
|
||||
{
|
||||
site_uuid: 'd7902405_0dc2_458b_8584_ed4d24b64f24',
|
||||
brand_name: 'Nexus',
|
||||
brand_logo: '/nexus-logo.png',
|
||||
footer_copyright: 'PT. Nexus Solusi',
|
||||
primary_color: '#8B5CF6',
|
||||
}
|
||||
];
|
||||
|
||||
for (const config of defaultConfigs) {
|
||||
const existing = await TenantConfig.findOne({ site_uuid: config.site_uuid });
|
||||
if (!existing) {
|
||||
await TenantConfig.create(config);
|
||||
console.log(`[Auth] ✓ Seeded TenantConfig for: ${config.brand_name}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Seed default agent locations
|
||||
const defaultLocations = [
|
||||
{
|
||||
agent_uuid: 'F6-2V-DT-8A',
|
||||
site_uuid: '6681452d_9cae_4ff4_8ae8_0d504774265e',
|
||||
latitude: -6.2263304,
|
||||
longitude: 106.4247322,
|
||||
label: 'CPI Balaraja Agent Office'
|
||||
},
|
||||
{
|
||||
agent_uuid: '2F-TF-1D-GK',
|
||||
site_uuid: '6681452d_9cae_4ff4_8ae8_0d504774265e',
|
||||
latitude: -6.3763318,
|
||||
longitude: 106.8983017,
|
||||
label: 'JRP Cibubur Agent Office'
|
||||
},
|
||||
{
|
||||
agent_uuid: '8A-V3-PB-85',
|
||||
site_uuid: '6681452d_9cae_4ff4_8ae8_0d504774265e',
|
||||
latitude: -6.2253265,
|
||||
longitude: 106.8061484,
|
||||
label: 'IFG LT.18 Agent HQ'
|
||||
}
|
||||
];
|
||||
|
||||
for (const loc of defaultLocations) {
|
||||
const existing = await CustomAgentLocation.findOne({ agent_uuid: loc.agent_uuid });
|
||||
if (!existing) {
|
||||
await CustomAgentLocation.create(loc);
|
||||
console.log(`[Auth] ✓ Seeded CustomAgentLocation for: ${loc.agent_uuid}`);
|
||||
}
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
console.warn('[Auth] Seed skipped (MongoDB not ready yet):', err.message);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ const multer = require('multer');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
|
||||
const JWT_SECRET = process.env.JWT_SECRET || 'super-secret-backone-key';
|
||||
const { requireAuth, requireAdmin, JWT_SECRET } = require('../../middleware/auth');
|
||||
|
||||
function makeToken(user) {
|
||||
return jwt.sign(
|
||||
@@ -31,52 +31,6 @@ function setCookieToken(res, token) {
|
||||
});
|
||||
}
|
||||
|
||||
function requireAuth(req, res, next) {
|
||||
const token = req.cookies?.token;
|
||||
if (!token) return res.status(401).json({ error: 'Not authenticated' });
|
||||
try {
|
||||
req.user = jwt.verify(token, JWT_SECRET);
|
||||
|
||||
// ── VIEW-AS MODE ──────────────────────────────────────────────────────────
|
||||
const viewAsHeader = req.headers['x-view-as-agent'];
|
||||
if (viewAsHeader && req.user.role === 'SUPER_ADMIN') {
|
||||
try {
|
||||
const viewDecoded = jwt.verify(viewAsHeader, JWT_SECRET);
|
||||
if (viewDecoded.type === 'view-as' && viewDecoded.adminId === req.user.id && viewDecoded.viewAs) {
|
||||
req.user = {
|
||||
...req.user,
|
||||
role: 'AGENT_VIEWER',
|
||||
agent_uuid: viewDecoded.viewAs,
|
||||
agent_label: viewDecoded.viewAsLabel,
|
||||
_viewAsMode: true,
|
||||
_originalRole: 'SUPER_ADMIN',
|
||||
};
|
||||
}
|
||||
} catch (viewErr) {
|
||||
console.warn('[ViewAs] Invalid view-as token, ignoring:', viewErr.message);
|
||||
}
|
||||
}
|
||||
next();
|
||||
} catch {
|
||||
res.status(401).json({ error: 'Token tidak valid' });
|
||||
}
|
||||
}
|
||||
|
||||
function requireAdmin(req, res, next) {
|
||||
const token = req.cookies?.token;
|
||||
if (!token) return res.status(401).json({ error: 'Not authenticated' });
|
||||
try {
|
||||
const decoded = jwt.verify(token, JWT_SECRET);
|
||||
if (decoded.role !== 'SUPER_ADMIN' && decoded.role !== 'TENANT_ADMIN' && decoded.role !== 'SOC_ANALYST') {
|
||||
return res.status(403).json({ error: 'Role Anda tidak memiliki izin untuk melakukan aksi ini (Hanya Administrator / Analyst)' });
|
||||
}
|
||||
req.adminUser = decoded;
|
||||
next();
|
||||
} catch {
|
||||
res.status(401).json({ error: 'Token tidak valid' });
|
||||
}
|
||||
}
|
||||
|
||||
function getUploadsDir() {
|
||||
if (fs.existsSync('/home/adminbackend/web/demoplace.my.id/public_html')) {
|
||||
return '/home/adminbackend/web/demoplace.my.id/public_html/api/uploads';
|
||||
|
||||
@@ -17,7 +17,17 @@ function blockAnalyst(req, res, next) {
|
||||
// GET /api/auth/admin/users — daftar semua users (admin & analyst)
|
||||
router.get('/admin/users', requireAdmin, async (req, res) => {
|
||||
try {
|
||||
const users = await User.find({}, '-password_hash').sort({ created_at: 1 });
|
||||
let query = {};
|
||||
if (req.adminUser.role === 'TENANT_ADMIN') {
|
||||
query = {
|
||||
$or: [
|
||||
{ role: 'AGENT_VIEWER', site_uuid: req.adminUser.site_uuid },
|
||||
{ created_by: req.adminUser.username }
|
||||
]
|
||||
};
|
||||
}
|
||||
query.username = { $ne: req.adminUser.username };
|
||||
const users = await User.find(query, '-password_hash').sort({ created_at: 1 });
|
||||
const data = users.map(u => ({
|
||||
id: u._id.toString(),
|
||||
username: u.username,
|
||||
@@ -42,7 +52,21 @@ router.post('/admin/create-agent-user', requireAdmin, blockAnalyst, async (req,
|
||||
return res.status(400).json({ ok: false, error: 'Username dan password wajib diisi' });
|
||||
}
|
||||
const passwordHash = bcrypt.hashSync(password, 10);
|
||||
const siteUuid = process.env.BACKONE_SITE_UUID || process.env.NETIFY_SITE_UUID || null;
|
||||
|
||||
let siteUuid = null;
|
||||
if (agent_uuid) {
|
||||
const { Summary } = require('../../models/Schemas');
|
||||
const summaryDoc = await Summary.findOne({ agent_uuid: agent_uuid.trim() });
|
||||
if (summaryDoc) {
|
||||
siteUuid = summaryDoc.site_uuid;
|
||||
}
|
||||
}
|
||||
|
||||
if (!siteUuid) {
|
||||
siteUuid = req.adminUser.role === 'SUPER_ADMIN'
|
||||
? (req.body.site_uuid || process.env.BACKONE_SITE_UUID || process.env.NETIFY_SITE_UUID || null)
|
||||
: req.adminUser.site_uuid;
|
||||
}
|
||||
|
||||
const newUser = await User.create({
|
||||
username: username.trim(),
|
||||
@@ -51,6 +75,7 @@ router.post('/admin/create-agent-user', requireAdmin, blockAnalyst, async (req,
|
||||
agent_uuid: agent_uuid?.trim() || null,
|
||||
role: 'AGENT_VIEWER',
|
||||
site_uuid: siteUuid,
|
||||
created_by: req.adminUser.username,
|
||||
});
|
||||
|
||||
res.json({ ok: true, message: 'Akun Network Agent berhasil dibuat', userId: newUser._id.toString() });
|
||||
@@ -69,6 +94,10 @@ router.post('/admin/update-agent-user', requireAdmin, blockAnalyst, upload.singl
|
||||
const target = await User.findById(user_id).select('+password_hash');
|
||||
if (!target) return res.status(404).json({ ok: false, error: 'User tidak ditemukan' });
|
||||
if (target.role === 'SUPER_ADMIN') return res.status(403).json({ ok: false, error: 'Tidak bisa mengubah akun SUPER_ADMIN dari sini' });
|
||||
|
||||
if (req.adminUser.role !== 'SUPER_ADMIN' && target.site_uuid !== req.adminUser.site_uuid) {
|
||||
return res.status(403).json({ ok: false, error: 'Unauthorized: This account does not belong to your tenant.' });
|
||||
}
|
||||
|
||||
if (username?.trim()) {
|
||||
const existing = await User.findOne({ username: username.trim(), _id: { $ne: user_id } });
|
||||
@@ -77,7 +106,16 @@ router.post('/admin/update-agent-user', requireAdmin, blockAnalyst, upload.singl
|
||||
}
|
||||
if (password) target.password_hash = bcrypt.hashSync(password, 10);
|
||||
if (account_name != null) target.account_name = account_name?.trim() || null;
|
||||
if (agent_uuid != null) target.agent_uuid = agent_uuid?.trim() || null;
|
||||
if (agent_uuid != null) {
|
||||
target.agent_uuid = agent_uuid?.trim() || null;
|
||||
if (agent_uuid.trim()) {
|
||||
const { Summary } = require('../../models/Schemas');
|
||||
const summaryDoc = await Summary.findOne({ agent_uuid: agent_uuid.trim() });
|
||||
if (summaryDoc) {
|
||||
target.site_uuid = summaryDoc.site_uuid;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (req.file) target.profile_picture = req.file.filename;
|
||||
|
||||
await target.save();
|
||||
@@ -94,6 +132,10 @@ router.delete('/admin/delete-agent-user/:id', requireAdmin, blockAnalyst, async
|
||||
const target = await User.findById(req.params.id);
|
||||
if (!target) return res.status(404).json({ ok: false, error: 'User tidak ditemukan' });
|
||||
if (target.role === 'SUPER_ADMIN') return res.status(403).json({ ok: false, error: 'Tidak bisa menghapus SUPER_ADMIN' });
|
||||
|
||||
if (req.adminUser.role !== 'SUPER_ADMIN' && target.site_uuid !== req.adminUser.site_uuid) {
|
||||
return res.status(403).json({ ok: false, error: 'Unauthorized: This account does not belong to your tenant.' });
|
||||
}
|
||||
await User.findByIdAndDelete(req.params.id);
|
||||
res.json({ ok: true, message: 'Akun berhasil dihapus' });
|
||||
} catch (err) {
|
||||
@@ -107,6 +149,10 @@ router.post('/admin/upload-agent-picture/:id', requireAdmin, blockAnalyst, uploa
|
||||
if (!req.file) return res.status(400).json({ ok: false, error: 'File gambar wajib diupload' });
|
||||
const target = await User.findById(req.params.id);
|
||||
if (!target) return res.status(404).json({ ok: false, error: 'User tidak ditemukan' });
|
||||
|
||||
if (req.adminUser.role !== 'SUPER_ADMIN' && target.site_uuid !== req.adminUser.site_uuid) {
|
||||
return res.status(403).json({ ok: false, error: 'Unauthorized: This account does not belong to your tenant.' });
|
||||
}
|
||||
target.profile_picture = req.file.filename;
|
||||
await target.save();
|
||||
res.json({ ok: true, message: 'Foto profil berhasil diperbarui', filename: req.file.filename });
|
||||
@@ -135,7 +181,13 @@ router.post('/admin/create-external-user', requireAdmin, blockAnalyst, upload.si
|
||||
}
|
||||
|
||||
const passwordHash = bcrypt.hashSync(password, 10);
|
||||
const siteUuid = process.env.BACKONE_SITE_UUID || process.env.NETIFY_SITE_UUID || null;
|
||||
const siteUuid = req.adminUser.role === 'SUPER_ADMIN'
|
||||
? (req.body.site_uuid || process.env.BACKONE_SITE_UUID || process.env.NETIFY_SITE_UUID || null)
|
||||
: req.adminUser.site_uuid;
|
||||
|
||||
const createdBy = req.adminUser.role === 'SUPER_ADMIN'
|
||||
? (req.body.created_by || req.adminUser.username)
|
||||
: req.adminUser.username;
|
||||
|
||||
const newUser = await User.create({
|
||||
username: username.trim(),
|
||||
@@ -143,6 +195,7 @@ router.post('/admin/create-external-user', requireAdmin, blockAnalyst, upload.si
|
||||
account_name: account_name?.trim() || null,
|
||||
role: role,
|
||||
site_uuid: siteUuid,
|
||||
created_by: createdBy,
|
||||
profile_picture: req.file ? req.file.filename : null
|
||||
});
|
||||
|
||||
|
||||
@@ -10,20 +10,55 @@ const axios = require('axios');
|
||||
|
||||
const PROXY_URL = process.env.PROXY_URL || 'http://localhost:4000';
|
||||
|
||||
// ─── Rebranding Helper (Memory Safe & Fast) ──────────────────────────────────
|
||||
function rebrandString(str) {
|
||||
if (typeof str !== 'string') return str;
|
||||
return str
|
||||
.replace(/netify\.unclassified/gi, 'backone.unclassified')
|
||||
.replace(/netify\.(?!ai)/gi, 'backone.')
|
||||
.replace(/Netify's/g, "BackOne's")
|
||||
.replace(/netify's/g, "backone's")
|
||||
.replace(/Netify(?!(\.ai))/g, 'BackOne')
|
||||
.replace(/netify(?!(\.ai))/g, 'backone');
|
||||
}
|
||||
|
||||
function rebrandObj(obj) {
|
||||
if (obj === null || obj === undefined) return obj;
|
||||
|
||||
if (Array.isArray(obj)) {
|
||||
for (let i = 0; i < obj.length; i++) {
|
||||
obj[i] = rebrandObj(obj[i]);
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
if (typeof obj === 'object') {
|
||||
for (const key in obj) {
|
||||
if (Object.prototype.hasOwnProperty.call(obj, key)) {
|
||||
if (typeof obj[key] === 'string') {
|
||||
obj[key] = rebrandString(obj[key]);
|
||||
} else if (typeof obj[key] === 'object') {
|
||||
obj[key] = rebrandObj(obj[key]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
if (typeof obj === 'string') {
|
||||
return rebrandString(obj);
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
// ─── Rebranding Middleware ────────────────────────────────────────────────────
|
||||
router.use((req, res, next) => {
|
||||
const originalJson = res.json.bind(res);
|
||||
res.json = function (body) {
|
||||
if (body) {
|
||||
try {
|
||||
const sanitized = JSON.stringify(body)
|
||||
.replace(/netify\.unclassified/gi, 'backone.unclassified')
|
||||
.replace(/netify\.(?!ai)/gi, 'backone.')
|
||||
.replace(/Netify's/g, "BackOne's")
|
||||
.replace(/netify's/g, "backone's")
|
||||
.replace(/Netify(?!(\.ai))/g, 'BackOne')
|
||||
.replace(/netify(?!(\.ai))/g, 'backone');
|
||||
body = JSON.parse(sanitized);
|
||||
body = rebrandObj(body);
|
||||
} catch (err) {
|
||||
console.error('[Dashboard] Rebrand error:', err.message);
|
||||
}
|
||||
@@ -56,5 +91,8 @@ router.use(require('./dashboard/tls'));
|
||||
router.use(require('./dashboard/telemetry'));
|
||||
router.use(require('./dashboard/events'));
|
||||
router.use(require('./dashboard/sslSan'));
|
||||
router.use(require('./dashboard/tenantConfig'));
|
||||
router.use(require('./dashboard/agentLocations'));
|
||||
router.use(require('./dashboard/blacklist'));
|
||||
|
||||
module.exports = router;
|
||||
@@ -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;
|
||||
|
||||
@@ -48,30 +48,34 @@ module.exports = async function deviceDetailsHandler(req, res, helpers) {
|
||||
const t0 = Date.now();
|
||||
try {
|
||||
const {
|
||||
getTimeFilter, generateMacFromIp,
|
||||
getTimeFilter, getBaseFilter, generateMacFromIp,
|
||||
resolveDeviceTypeFromIp, resolveOSFromIp, resolveVendorFromIp, generateAutoLabel
|
||||
} = helpers;
|
||||
|
||||
const baseFilter = getBaseFilter(req);
|
||||
|
||||
let ip = String(req.query.ip ?? '');
|
||||
const mac = String(req.query.mac ?? '');
|
||||
|
||||
if (!ip && mac) {
|
||||
const dev = await DeviceStat.findOne({ mac_address: mac }).sort({ timestamp: -1 }).lean();
|
||||
const dev = await DeviceStat.findOne({ mac_address: mac, ...baseFilter }).sort({ timestamp: -1 }).lean();
|
||||
if (dev) {
|
||||
ip = dev.ip_address;
|
||||
} else {
|
||||
const flow = await Flow.findOne({ src_mac: mac }).sort({ timestamp: -1 }).lean();
|
||||
const flow = await Flow.findOne({ src_mac: mac, ...baseFilter }).sort({ timestamp: -1 }).lean();
|
||||
if (flow) ip = flow.src_ip;
|
||||
}
|
||||
}
|
||||
if (!ip) return res.status(400).json({ ok: false, message: 'ip or mac required' });
|
||||
if (!ip && !mac) return res.status(400).json({ ok: false, message: 'ip or mac required' });
|
||||
|
||||
const agentUuidParam = String(req.query.agent_uuid ?? '');
|
||||
const metaFilter = {};
|
||||
if (req.user?.site_uuid) metaFilter.site_uuid = req.user.site_uuid;
|
||||
// Find device stats by ip if set, else by mac
|
||||
const deviceQuery = ip ? { ip_address: ip } : { mac_address: mac };
|
||||
const device = await DeviceStat.findOne({ ...deviceQuery, ...baseFilter }).sort({ timestamp: -1 }).lean();
|
||||
if (!ip && device?.ip_address) {
|
||||
ip = device.ip_address;
|
||||
}
|
||||
|
||||
const device = await DeviceStat.findOne({ ip_address: ip, ...metaFilter }).sort({ timestamp: -1 }).lean();
|
||||
const agentUuid = agentUuidParam || device?.agent_uuid || req.user?.agent_uuid || null;
|
||||
const agentUuid = baseFilter.agent_uuid || device?.agent_uuid || null;
|
||||
|
||||
// ── PRIMARY bandwidth source ─────────────────────────────────────────────
|
||||
const totalDownload = device?.download || 0;
|
||||
@@ -89,15 +93,35 @@ module.exports = async function deviceDetailsHandler(req, res, helpers) {
|
||||
}
|
||||
|
||||
// ── Parallel queries ─────────────────────────────────────────────────────
|
||||
const flowQueryConditions = [];
|
||||
if (ip) {
|
||||
flowQueryConditions.push({ src_ip: ip }, { dst_ip: ip });
|
||||
}
|
||||
if (mac) {
|
||||
flowQueryConditions.push({ src_mac: mac }, { dst_mac: mac });
|
||||
}
|
||||
|
||||
const threatQuery = {
|
||||
...(agentUuid ? { agent_uuid: agentUuid } : {})
|
||||
};
|
||||
if (ip && mac) {
|
||||
threatQuery.$or = [{ ip_address: ip }, { mac_address: mac }, { src_mac: mac }];
|
||||
} else if (ip) {
|
||||
threatQuery.ip_address = ip;
|
||||
} else if (mac) {
|
||||
threatQuery.$or = [{ mac_address: mac }, { src_mac: mac }];
|
||||
}
|
||||
|
||||
const appFilter = agentUuid ? { agent_uuid: agentUuid, ip_address: ip } : { ip_address: ip };
|
||||
if (req.user?.site_uuid) appFilter.site_uuid = req.user.site_uuid;
|
||||
|
||||
const [deviceAppStats, flowsQuery, rawThreats] = await Promise.all([
|
||||
// PRIMARY: per-device per-app from DPI API (stored by proxy Step 3b)
|
||||
DeviceAppStat.find(appFilter).sort({ timestamp: -1 }).lean(),
|
||||
Flow.find({ ...flowFilter, $or: [{ src_ip: ip }, { dst_ip: ip }] }).sort({ timestamp: -1 }).limit(2000).lean(),
|
||||
Threat.find({ ...(agentUuid ? { agent_uuid: agentUuid } : {}), ip_address: ip })
|
||||
.sort({ detected_at: -1 }).lean(),
|
||||
// Only query DeviceAppStat if we have an IP
|
||||
ip ? DeviceAppStat.find(appFilter).sort({ timestamp: -1 }).lean() : [],
|
||||
flowQueryConditions.length > 0
|
||||
? Flow.find({ ...flowFilter, $or: flowQueryConditions }).sort({ timestamp: -1 }).limit(2000).lean()
|
||||
: [],
|
||||
Threat.find(threatQuery).sort({ detected_at: -1 }).lean(),
|
||||
]);
|
||||
|
||||
// ── Apps tab — use DeviceAppStat (real DPI per-IP per-app data) ──────────
|
||||
@@ -132,7 +156,8 @@ module.exports = async function deviceDetailsHandler(req, res, helpers) {
|
||||
};
|
||||
|
||||
for (const f of flowsQuery) {
|
||||
if (f.src_ip !== ip) continue; // outbound only
|
||||
const isOutbound = ip ? (f.src_ip === ip) : (mac ? (f.src_mac === mac) : false);
|
||||
if (!isOutbound) continue; // outbound only
|
||||
const down = f.download || 0;
|
||||
const up = f.upload || 0;
|
||||
const ls = f.last_seen || (f.timestamp ? new Date(f.timestamp).toISOString() : new Date().toISOString());
|
||||
@@ -178,7 +203,7 @@ module.exports = async function deviceDetailsHandler(req, res, helpers) {
|
||||
}));
|
||||
|
||||
const flows = flowsQuery
|
||||
.filter(f => f.src_ip === ip)
|
||||
.filter(f => ip ? (f.src_ip === ip) : (mac ? (f.src_mac === mac) : false))
|
||||
.map(f => ({
|
||||
flow_id: f.flow_id || f._id.toString(),
|
||||
src_ip: f.src_ip,
|
||||
|
||||
@@ -10,9 +10,11 @@ module.exports = async function remoteIpDetailsHandler(req, res, helpers) {
|
||||
const flowFilter = {};
|
||||
if (req.user?.site_uuid) flowFilter.site_uuid = req.user.site_uuid;
|
||||
|
||||
// Agent scope if viewer
|
||||
// Agent scope if viewer or query param
|
||||
if (req.user?.role === 'AGENT_VIEWER' && req.user?.agent_uuid) {
|
||||
flowFilter.agent_uuid = req.user.agent_uuid;
|
||||
} else if (req.query?.agent_uuid) {
|
||||
flowFilter.agent_uuid = req.query.agent_uuid;
|
||||
}
|
||||
|
||||
const rawTimeRange = String(req.query.timeRange ?? 'all');
|
||||
|
||||
+14
-148
@@ -55,43 +55,15 @@ app.use('/api/auth', authRoutes);
|
||||
app.use('/api/uploads', express.static(getUploadsDir()));
|
||||
|
||||
// ─── Auth Middleware ──────────────────────────────────────────────────────────
|
||||
const JWT_SECRET = process.env.JWT_SECRET || 'super-secret-backone-key';
|
||||
|
||||
function requireAuth(req, res, next) {
|
||||
const token = req.cookies?.token;
|
||||
if (!token) return res.status(401).json({ error: 'Unauthorized' });
|
||||
|
||||
try {
|
||||
req.user = jwt.verify(token, JWT_SECRET);
|
||||
|
||||
// ── VIEW-AS MODE ──────────────────────────────────────────────────────────
|
||||
// Jika SUPER_ADMIN sedang dalam mode "View As Agent", frontend mengirim
|
||||
// header X-View-As-Agent berisi JWT token yang berisi agent_uuid yang dipilih.
|
||||
const viewAsHeader = req.headers['x-view-as-agent'];
|
||||
if (viewAsHeader && req.user.role === 'SUPER_ADMIN') {
|
||||
try {
|
||||
const viewDecoded = jwt.verify(viewAsHeader, JWT_SECRET);
|
||||
if (viewDecoded.type === 'view-as' && viewDecoded.adminId === req.user.id && viewDecoded.viewAs) {
|
||||
req.user = {
|
||||
...req.user,
|
||||
role: 'AGENT_VIEWER',
|
||||
agent_uuid: viewDecoded.viewAs,
|
||||
agent_label: viewDecoded.viewAsLabel,
|
||||
_viewAsMode: true,
|
||||
_originalRole: 'SUPER_ADMIN',
|
||||
};
|
||||
}
|
||||
} catch (viewErr) {
|
||||
console.warn('[ViewAs] Invalid view-as token, ignoring:', viewErr.message);
|
||||
}
|
||||
}
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
next();
|
||||
} catch (err) {
|
||||
res.status(401).json({ error: 'Invalid token' });
|
||||
}
|
||||
}
|
||||
const { requireAuth } = require('./middleware/auth');
|
||||
const { getTimeFilter, getBaseFilter } = require('./routes/dashboard/helpers');
|
||||
const {
|
||||
generateMacFromIp,
|
||||
resolveVendorFromIp,
|
||||
resolveDeviceTypeFromIp,
|
||||
resolveOSFromIp,
|
||||
generateAutoLabel
|
||||
} = require('./deviceResolver');
|
||||
|
||||
// ─── Protected Dashboard Routes ───────────────────────────────────────────────
|
||||
const dashboardRoutes = require('./routes/dashboard');
|
||||
@@ -99,109 +71,16 @@ const dashboardRoutes = require('./routes/dashboard');
|
||||
// Override /api/dashboard/app-details to show real-time device mapping per application
|
||||
app.get('/api/dashboard/app-details', requireAuth, (req, res) => {
|
||||
require('./routes/appDetailsHandler')(req, res, {
|
||||
getTimeFilter: (req) => {
|
||||
const range = req.query.timeRange || 'all';
|
||||
if (range === 'all') return null;
|
||||
const now = new Date();
|
||||
const ms = {
|
||||
'5m': 5 * 60000,
|
||||
'10m': 10 * 60000,
|
||||
'30m': 30 * 60000,
|
||||
'1h': 60 * 60000,
|
||||
'1d': 24 * 3600000,
|
||||
'7d': 7 * 24 * 3600000,
|
||||
};
|
||||
const delta = ms[range] ?? ms['1h'];
|
||||
return { $gte: new Date(now.getTime() - delta) };
|
||||
},
|
||||
getBaseFilter: (req, timeFilter = null) => {
|
||||
const filter = {};
|
||||
if (timeFilter) filter.timestamp = timeFilter;
|
||||
if (req.user?.site_uuid) filter.site_uuid = req.user.site_uuid;
|
||||
|
||||
// Agent-based isolation (RBAC / Multi-Tenant)
|
||||
if (req.user?.role === 'AGENT_VIEWER' && req.user?.agent_uuid) {
|
||||
filter.agent_uuid = req.user.agent_uuid;
|
||||
}
|
||||
return filter;
|
||||
}
|
||||
getTimeFilter,
|
||||
getBaseFilter
|
||||
});
|
||||
});
|
||||
|
||||
// Override /api/dashboard/device-details to map real-time classifications (Facebook, YouTube, etc.)
|
||||
app.get('/api/dashboard/device-details', requireAuth, (req, res) => {
|
||||
const generateMacFromIp = (ip) => {
|
||||
if (!ip) return '00:16:3e:00:11:22';
|
||||
let hash = 0;
|
||||
for (let i = 0; i < ip.length; i++) {
|
||||
hash = (hash << 5) - hash + ip.charCodeAt(i);
|
||||
hash |= 0;
|
||||
}
|
||||
const hex = Math.abs(hash).toString(16).padEnd(8, 'a');
|
||||
return `00:16:3e:${hex.substring(0,2)}:${hex.substring(2,4)}:${hex.substring(4,6)}`;
|
||||
};
|
||||
|
||||
const resolveVendorFromIp = (ip) => {
|
||||
if (!ip) return 'Intel Corporation';
|
||||
if (ip.startsWith('10.6.30.') || ip.startsWith('10.250.')) return 'Supermicro / Dell Inc.';
|
||||
if (ip.startsWith('10.6.10.') || ip.startsWith('10.6.11.')) return 'Cisco Systems, Inc.';
|
||||
if (ip.startsWith('192.168.')) return 'TP-Link Corporation';
|
||||
let hash = 0;
|
||||
for (let i = 0; i < ip.length; i++) hash = (hash << 5) - hash + ip.charCodeAt(i);
|
||||
const vendors = ['Intel Corporation', 'Asustek Computer Inc.', 'Apple Inc.', 'Hewlett Packard', 'Samsung Electronics'];
|
||||
return vendors[Math.abs(hash) % vendors.length];
|
||||
};
|
||||
|
||||
const resolveDeviceTypeFromIp = (ip) => {
|
||||
if (!ip) return 'Workstation';
|
||||
if (ip.endsWith('.1') || ip.endsWith('.254')) return 'Gateway / Router';
|
||||
if (ip.startsWith('10.6.30.')) return 'Database Server';
|
||||
if (ip.startsWith('10.250.')) return 'Core Network Node';
|
||||
if (ip.startsWith('10.6.12.')) return 'Finance Workstation';
|
||||
return 'Workstation / Laptop';
|
||||
};
|
||||
|
||||
const resolveOSFromIp = (ip) => {
|
||||
if (!ip) return 'Windows 11';
|
||||
if (ip.startsWith('10.6.30.') || ip.startsWith('10.250.')) return 'Linux (Ubuntu Server 24.04)';
|
||||
if (ip.startsWith('10.6.12.')) return 'Windows 11 Enterprise';
|
||||
if (ip.startsWith('192.168.')) return 'iOS / Android';
|
||||
return 'Windows 11 Pro';
|
||||
};
|
||||
|
||||
const generateAutoLabel = (ip, mac, manufacturer, deviceType) => {
|
||||
const brand = manufacturer && manufacturer !== '-' && manufacturer !== 'Unknown' ? manufacturer.split(' ')[0] : '';
|
||||
const type = deviceType && deviceType !== '-' && deviceType !== 'Unknown' ? deviceType : 'Device';
|
||||
const suffix = ip ? ip.split('.').slice(-2).join('.') : (mac ? mac.split(':').slice(-2).join(':') : 'Node');
|
||||
return brand ? `${brand} ${type} (${suffix})` : `${type} (${suffix})`;
|
||||
};
|
||||
|
||||
require('./routes/deviceDetailsHandler')(req, res, {
|
||||
// Device detail: default timeRange is 'all' so ALL historical data shows
|
||||
// Only respect explicit time filters if user deliberately passes one
|
||||
getTimeFilter: (req) => {
|
||||
const range = req.query.timeRange || 'all';
|
||||
if (range === 'all') return null;
|
||||
const now = new Date();
|
||||
const ms = {
|
||||
'5m': 5 * 60000,
|
||||
'30m': 30 * 60000,
|
||||
'1h': 60 * 60000,
|
||||
'1d': 24 * 3600000,
|
||||
'7d': 7 * 24 * 3600000,
|
||||
};
|
||||
const delta = ms[range] ?? ms['1h'];
|
||||
return { $gte: new Date(now.getTime() - delta) };
|
||||
},
|
||||
getBaseFilter: (req, timeFilter = null) => {
|
||||
const filter = {};
|
||||
if (timeFilter) filter.timestamp = timeFilter;
|
||||
if (req.user?.site_uuid) filter.site_uuid = req.user.site_uuid;
|
||||
if (req.user?.role === 'AGENT_VIEWER' && req.user?.agent_uuid) {
|
||||
filter.agent_uuid = req.user.agent_uuid;
|
||||
}
|
||||
return filter;
|
||||
},
|
||||
getTimeFilter,
|
||||
getBaseFilter,
|
||||
generateMacFromIp,
|
||||
resolveDeviceTypeFromIp,
|
||||
resolveOSFromIp,
|
||||
@@ -212,20 +91,7 @@ app.get('/api/dashboard/device-details', requireAuth, (req, res) => {
|
||||
|
||||
app.get('/api/dashboard/remote-ip-details', requireAuth, async (req, res) => {
|
||||
require('./routes/remoteIpDetailsHandler')(req, res, {
|
||||
getTimeFilter: (req) => {
|
||||
const range = req.query.timeRange || 'all';
|
||||
if (range === 'all') return null;
|
||||
const now = new Date();
|
||||
const ms = {
|
||||
'5m': 5 * 60000,
|
||||
'30m': 30 * 60000,
|
||||
'1h': 60 * 60000,
|
||||
'1d': 24 * 3600000,
|
||||
'7d': 7 * 24 * 3600000,
|
||||
};
|
||||
const delta = ms[range] ?? ms['1h'];
|
||||
return { $gte: new Date(now.getTime() - delta) };
|
||||
}
|
||||
getTimeFilter
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in new issue
Block a user