Compare commits
27
Commits
996f685065
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0dcedf5d76 | ||
|
|
068435ffb9 | ||
|
|
f7ebd7a5c5 | ||
|
|
6a23577b08 | ||
|
|
f0d9a55a56 | ||
|
|
64f77187d8 | ||
|
|
8a73d266d8 | ||
|
|
bc30a8a0e4 | ||
|
|
6e891392a7 | ||
|
|
7bc28a6a37 | ||
|
|
a4dc705a3d | ||
|
|
1bb67e99dd | ||
|
|
f4ec563a34 | ||
|
|
ac48bf4585 | ||
|
|
3c2dc831a1 | ||
|
|
973ac7d264 | ||
|
|
e4a484df45 | ||
|
|
781826d934 | ||
|
|
19197ea9fa | ||
|
|
3524588e42 | ||
|
|
e7c9870e59 | ||
|
|
8dd8432c29 | ||
|
|
919e83331a | ||
|
|
7525375de9 | ||
|
|
56c208f7c9 | ||
|
|
997df2bc6b | ||
|
|
03c9ce5482 |
No files matched your search
@@ -2,10 +2,10 @@ FROM oven/bun:1-alpine
|
|||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
COPY backend/package*.json ./
|
COPY package*.json ./
|
||||||
RUN bun install --production
|
RUN bun install --production
|
||||||
|
|
||||||
COPY backend/ .
|
COPY . .
|
||||||
|
|
||||||
EXPOSE 3001
|
EXPOSE 3001
|
||||||
|
|
||||||
|
|||||||
@@ -239,6 +239,27 @@ router.get('/agents/:uuid/subnets', async (req, res) => {
|
|||||||
// ─── PUT /api/dashboard/agents/:uuid/subnets ─────────────────────────────────
|
// ─── PUT /api/dashboard/agents/:uuid/subnets ─────────────────────────────────
|
||||||
// Simpan konfigurasi subnet yang diizinkan untuk agent tertentu
|
// Simpan konfigurasi subnet yang diizinkan untuk agent tertentu
|
||||||
// Body: { allowed_subnets: ["192.168.1", "10.21"] }
|
// Body: { allowed_subnets: ["192.168.1", "10.21"] }
|
||||||
|
function ipToLong(ip) {
|
||||||
|
return ip.split('.').reduce((acc, octet) => (acc << 8) + parseInt(octet, 10), 0) >>> 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ipMatchesSubnets(ip, subnets) {
|
||||||
|
if (!subnets || subnets.length === 0) return true;
|
||||||
|
if (!ip) return false;
|
||||||
|
return subnets.some(subnet => {
|
||||||
|
if (subnet.includes('/')) {
|
||||||
|
try {
|
||||||
|
const [range, bitsStr] = subnet.split('/');
|
||||||
|
const bits = parseInt(bitsStr, 10);
|
||||||
|
if (isNaN(bits) || bits < 0 || bits > 32) return false;
|
||||||
|
const mask = bits === 0 ? 0 : (~0 << (32 - bits)) >>> 0;
|
||||||
|
return (ipToLong(ip) & mask) === (ipToLong(range) & mask);
|
||||||
|
} catch (e) { return false; }
|
||||||
|
} else { return ip === subnet; }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// PUT /api/dashboard/agents/:uuid/subnets
|
||||||
router.put('/agents/:uuid/subnets', async (req, res) => {
|
router.put('/agents/:uuid/subnets', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const allowedRoles = ['SUPER_ADMIN', 'TENANT_ADMIN', 'COMPANY_ADMIN'];
|
const allowedRoles = ['SUPER_ADMIN', 'TENANT_ADMIN', 'COMPANY_ADMIN'];
|
||||||
@@ -251,6 +272,21 @@ router.put('/agents/:uuid/subnets', async (req, res) => {
|
|||||||
{ uuid: req.params.uuid },
|
{ uuid: req.params.uuid },
|
||||||
{ $set: { allowed_subnets: subnets, subnets_updated_at: new Date() } }
|
{ $set: { allowed_subnets: subnets, subnets_updated_at: new Date() } }
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Auto-cleanup background task
|
||||||
|
if (subnets.length > 0) {
|
||||||
|
setTimeout(async () => {
|
||||||
|
try {
|
||||||
|
const ips = await db.collection('devicestats').distinct('ip_address', { agent_uuid: req.params.uuid });
|
||||||
|
const invalidIps = ips.filter(ip => !ipMatchesSubnets(ip, subnets));
|
||||||
|
if (invalidIps.length > 0) {
|
||||||
|
await db.collection('devicestats').deleteMany({ agent_uuid: req.params.uuid, ip_address: { $in: invalidIps } });
|
||||||
|
await db.collection('flows').deleteMany({ agent_uuid: req.params.uuid, src_ip: { $in: invalidIps } });
|
||||||
|
}
|
||||||
|
} catch (e) { console.error('Auto-cleanup error:', e); }
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
|
||||||
res.json({ ok: true, data: { allowed_subnets: subnets } });
|
res.json({ ok: true, data: { allowed_subnets: subnets } });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
res.status(500).json({ ok: false, error: err.message });
|
res.status(500).json({ ok: false, error: err.message });
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ router.get('/devices', async (req, res) => {
|
|||||||
const flowPipeline = [
|
const flowPipeline = [
|
||||||
{ $match: query },
|
{ $match: query },
|
||||||
{ $group: {
|
{ $group: {
|
||||||
_id: "$src_ip",
|
_id: { ip: "$src_ip", agent: "$agent_uuid" },
|
||||||
download: { $sum: "$download" },
|
download: { $sum: "$download" },
|
||||||
upload: { $sum: "$upload" },
|
upload: { $sum: "$upload" },
|
||||||
flows: { $sum: 1 },
|
flows: { $sum: 1 },
|
||||||
@@ -32,7 +32,7 @@ router.get('/devices', async (req, res) => {
|
|||||||
{ $sort: { download: -1 } },
|
{ $sort: { download: -1 } },
|
||||||
{ $project: {
|
{ $project: {
|
||||||
_id: 1, // needed for mapping later
|
_id: 1, // needed for mapping later
|
||||||
ip_address: "$_id",
|
ip_address: "$_id.ip",
|
||||||
download: 1,
|
download: 1,
|
||||||
upload: 1,
|
upload: 1,
|
||||||
flows: 1,
|
flows: 1,
|
||||||
@@ -54,7 +54,7 @@ router.get('/devices', async (req, res) => {
|
|||||||
const pipeline = [
|
const pipeline = [
|
||||||
{ $match: query },
|
{ $match: query },
|
||||||
{ $sort: { timestamp: -1 } },
|
{ $sort: { timestamp: -1 } },
|
||||||
{ $group: { _id: "$ip_address", doc: { $first: "$$ROOT" } } },
|
{ $group: { _id: { ip: "$ip_address", agent: "$agent_uuid" }, doc: { $first: "$$ROOT" } } },
|
||||||
{ $replaceRoot: { newRoot: "$doc" } },
|
{ $replaceRoot: { newRoot: "$doc" } },
|
||||||
{ $sort: { timestamp: -1, download: -1 } }
|
{ $sort: { timestamp: -1, download: -1 } }
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
require('dotenv').config({ path: 'd:/Kuliah/DPI/netify-dashboard/.env.local' });
|
|
||||||
const mongoose = require('mongoose');
|
|
||||||
|
|
||||||
async function testFilter() {
|
|
||||||
await mongoose.connect('mongodb://backone_inspect:backone_inspect@mongodb.prod.proit.id:27017/backone_inspect_0');
|
|
||||||
console.log("Connected to MongoDB.");
|
|
||||||
|
|
||||||
// Using the exact collection name mongoose uses for DeviceStat
|
|
||||||
const DeviceStat = mongoose.connection.collection('devicestats');
|
|
||||||
|
|
||||||
const kantorUUID = 'F6-2V-DT-8A';
|
|
||||||
const kantorDevices = await DeviceStat.find({ agent_uuid: kantorUUID }).toArray();
|
|
||||||
|
|
||||||
console.log(`\nTotal device records saved in Database specifically for Agent Kantor (${kantorUUID}): ${kantorDevices.length}`);
|
|
||||||
|
|
||||||
const contaminated = kantorDevices.filter(d => d.ip_address && d.ip_address.startsWith('10.6.'));
|
|
||||||
|
|
||||||
console.log(`\nOUT OF THOSE, how many have Balaraja IP (10.6.x.x) despite being tagged as Kantor? ${contaminated.length}`);
|
|
||||||
|
|
||||||
if (contaminated.length > 0) {
|
|
||||||
console.log("Example contaminated IPs in DB under Kantor's UUID:");
|
|
||||||
console.log(contaminated.slice(0, 5).map(d => `- ${d.ip_address}`).join('\n'));
|
|
||||||
}
|
|
||||||
|
|
||||||
process.exit(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
testFilter();
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
const express = require('express');
|
|
||||||
const mongoose = require('mongoose');
|
|
||||||
|
|
||||||
async function testApps() {
|
|
||||||
await mongoose.connect('mongodb://backone_inspect:backone_inspect@mongodb.prod.proit.id:27017/backone_inspect_0');
|
|
||||||
|
|
||||||
const { AppStat } = require('./models/Schemas');
|
|
||||||
|
|
||||||
const pipeline = [
|
|
||||||
{ $match: { site_uuid: '1959bb55_045b_47c7_bbdd_f33b7db197b9' } },
|
|
||||||
{ $group: {
|
|
||||||
_id: '$app_label',
|
|
||||||
download: { $sum: '$download' },
|
|
||||||
upload: { $sum: '$upload' },
|
|
||||||
flows: { $sum: '$flows' }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ $sort: { download: -1 } },
|
|
||||||
{ $limit: 10 }
|
|
||||||
];
|
|
||||||
|
|
||||||
const result = await AppStat.aggregate(pipeline);
|
|
||||||
console.log('Result for Site A:', result);
|
|
||||||
|
|
||||||
const pipelineB = [
|
|
||||||
{ $match: { site_uuid: '6681452d_9cae_4ff4_8ae8_0d504774265e' } },
|
|
||||||
{ $group: {
|
|
||||||
_id: '$app_label',
|
|
||||||
download: { $sum: '$download' },
|
|
||||||
upload: { $sum: '$upload' },
|
|
||||||
flows: { $sum: '$flows' }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ $sort: { download: -1 } },
|
|
||||||
{ $limit: 10 }
|
|
||||||
];
|
|
||||||
|
|
||||||
const resultB = await AppStat.aggregate(pipelineB);
|
|
||||||
console.log('Result for Site B:', resultB);
|
|
||||||
|
|
||||||
mongoose.disconnect();
|
|
||||||
}
|
|
||||||
|
|
||||||
testApps();
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
const express = require('express');
|
|
||||||
const mongoose = require('mongoose');
|
|
||||||
|
|
||||||
async function testSummary() {
|
|
||||||
await mongoose.connect('mongodb://backone_inspect:backone_inspect@mongodb.prod.proit.id:27017/backone_inspect_0');
|
|
||||||
|
|
||||||
const { Summary } = require('./models/Schemas');
|
|
||||||
|
|
||||||
const siteIds = await Summary.distinct('site_uuid', { agent_uuid: null });
|
|
||||||
|
|
||||||
const now = new Date();
|
|
||||||
const delta = 24 * 3600000;
|
|
||||||
const timeFilter = { $gte: new Date(now.getTime() - delta) };
|
|
||||||
|
|
||||||
console.log('Querying with timeFilter:', timeFilter);
|
|
||||||
|
|
||||||
const siteSummaries = await Summary.find({
|
|
||||||
site_uuid: { $in: siteIds },
|
|
||||||
agent_uuid: null,
|
|
||||||
timestamp: timeFilter
|
|
||||||
}).lean();
|
|
||||||
|
|
||||||
let bandwidthDown = 0;
|
|
||||||
let bandwidthUp = 0;
|
|
||||||
|
|
||||||
const validSite = siteSummaries.filter(x => x.bandwidth_down > 0 || x.bandwidth_up > 0);
|
|
||||||
if (validSite.length > 0) {
|
|
||||||
validSite.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));
|
|
||||||
bandwidthDown = validSite[0].bandwidth_down || 0;
|
|
||||||
bandwidthUp = validSite[0].bandwidth_up || 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Test validSite length:', validSite.length);
|
|
||||||
console.log('Resulting Bandwidth:', bandwidthDown, bandwidthUp);
|
|
||||||
|
|
||||||
mongoose.disconnect();
|
|
||||||
}
|
|
||||||
|
|
||||||
testSummary();
|
|
||||||
+1
-25
@@ -1,25 +1 @@
|
|||||||
const { Client } = require('ssh2');
|
const fetch = require(node-fetch); async function main() { console.log(Checking logs...); } main();
|
||||||
|
|
||||||
const config = {
|
|
||||||
host: '103.185.47.52',
|
|
||||||
port: 2222,
|
|
||||||
username: 'adminbackend',
|
|
||||||
password: 'htEo7x6LsBQiEHHH',
|
|
||||||
readyTimeout: 60000
|
|
||||||
};
|
|
||||||
|
|
||||||
const conn = new Client();
|
|
||||||
conn.on('ready', () => {
|
|
||||||
const cmd = `
|
|
||||||
echo "=== PM2 STATUS ==="
|
|
||||||
~/.npm-global/bin/pm2 list || pm2 list || npx pm2 list
|
|
||||||
`;
|
|
||||||
conn.exec(cmd, (err, stream) => {
|
|
||||||
if (err) throw err;
|
|
||||||
stream.on('close', () => conn.end());
|
|
||||||
stream.on('data', (d) => process.stdout.write(d.toString()));
|
|
||||||
stream.stderr.on('data', (d) => process.stderr.write(d.toString()));
|
|
||||||
});
|
|
||||||
}).connect(config);
|
|
||||||
|
|
||||||
|
|
||||||
@@ -38,7 +38,7 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
- NODE_ENV=production
|
- NODE_ENV=production
|
||||||
- NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL}
|
- NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL}
|
||||||
- INTERNAL_API_URL=${INTERNAL_API_URL:-http://backone-inspect-backend-bun-0:3001}
|
- INTERNAL_API_URL=${INTERNAL_API_URL:-http://backend:3001}
|
||||||
- JWT_SECRET=${JWT_SECRET:-super-secret-backone-key}
|
- JWT_SECRET=${JWT_SECRET:-super-secret-backone-key}
|
||||||
- MONGODB_URI=${MONGODB_URI}
|
- MONGODB_URI=${MONGODB_URI}
|
||||||
- BACKONE_API_KEY=${NETIFY_API_KEY:-sk_db_source2}
|
- BACKONE_API_KEY=${NETIFY_API_KEY:-sk_db_source2}
|
||||||
|
|||||||
@@ -15,6 +15,6 @@ EXPOSE 4000
|
|||||||
|
|
||||||
# Health check
|
# Health check
|
||||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=15s --retries=3 \
|
HEALTHCHECK --interval=30s --timeout=10s --start-period=15s --retries=3 \
|
||||||
CMD bun -e "require('http').get('http://localhost:4000/health', r => r.statusCode === 200 ? process.exit(0) : process.exit(1)).on('error', () => process.exit(1))"
|
CMD bun -e "require('http').get('http://127.0.0.1:4000/health', r => r.statusCode === 200 ? process.exit(0) : process.exit(1)).on('error', () => process.exit(1))"
|
||||||
|
|
||||||
CMD ["bun", "run", "index.js"]
|
CMD ["bun", "run", "index.js"]
|
||||||
@@ -13,6 +13,10 @@ async function fetchFlows(limit = 500, agentUuid = null, siteUuid = null, interv
|
|||||||
backoneFetch('/data/stats/top/tls_sni/download', { filter_interval: intervalMinutes, settings_limit: 50 }, agentUuid, siteUuid),
|
backoneFetch('/data/stats/top/tls_sni/download', { filter_interval: intervalMinutes, settings_limit: 50 }, agentUuid, siteUuid),
|
||||||
]);
|
]);
|
||||||
if (!raw || !Array.isArray(raw)) return null;
|
if (!raw || !Array.isArray(raw)) return null;
|
||||||
|
|
||||||
|
// Safety check: Filter flows strictly to the requested agent to prevent Org-level pollution
|
||||||
|
const filteredRaw = agentUuid ? raw.filter(r => r.agent_uuid === agentUuid) : raw;
|
||||||
|
|
||||||
const sniList = [];
|
const sniList = [];
|
||||||
if (sniRaw && Array.isArray(sniRaw)) {
|
if (sniRaw && Array.isArray(sniRaw)) {
|
||||||
for (const r of sniRaw) {
|
for (const r of sniRaw) {
|
||||||
@@ -22,7 +26,7 @@ async function fetchFlows(limit = 500, agentUuid = null, siteUuid = null, interv
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return raw.map(r => {
|
return filteredRaw.map(r => {
|
||||||
const port = r.remote_port ?? null;
|
const port = r.remote_port ?? null;
|
||||||
const portService = port ? (stats.PORT_SERVICE_MAP[port] ?? `Port ${port}`) : null;
|
const portService = port ? (stats.PORT_SERVICE_MAP[port] ?? `Port ${port}`) : null;
|
||||||
const appLabel = r.application?.label || portService;
|
const appLabel = r.application?.label || portService;
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ require('dotenv').config({ path: path.join(__dirname, '..', '.env.local') });
|
|||||||
const axios = require('axios');
|
const axios = require('axios');
|
||||||
|
|
||||||
const BASE_URL = process.env.BACKONE_INFORMATICS_BASE_URL || 'https://api0.dev.backone.cloud/api/v1';
|
const BASE_URL = process.env.BACKONE_INFORMATICS_BASE_URL || 'https://api0.dev.backone.cloud/api/v1';
|
||||||
const JWT_TOKEN = process.env.BACKONE_TOKEN || process.env.BACKONE_JWT_TOKEN;
|
const JWT_TOKEN = process.env.BACKONE_TOKEN || process.env.BACKONE_JWT_TOKEN || process.env.BACKONE_API_KEY || process.env.NETIFY_API_KEY;
|
||||||
const agentMap = {};
|
const agentMap = {};
|
||||||
|
|
||||||
function getHeaders(siteUuid) {
|
function getHeaders(siteUuid) {
|
||||||
@@ -37,7 +37,8 @@ function fixDates(obj) {
|
|||||||
async function backoneFetch(endpoint, params = {}, agentUuid = null, siteUuid = null) {
|
async function backoneFetch(endpoint, params = {}, agentUuid = null, siteUuid = null) {
|
||||||
if (agentUuid) {
|
if (agentUuid) {
|
||||||
const agentId = agentMap[agentUuid];
|
const agentId = agentMap[agentUuid];
|
||||||
if (agentId) {
|
// Only use filter_agents if agentId is a number or numeric string
|
||||||
|
if (agentId && !isNaN(Number(agentId))) {
|
||||||
params.filter_agents = `[${agentId}]`;
|
params.filter_agents = `[${agentId}]`;
|
||||||
} else {
|
} else {
|
||||||
params.settings_agent = agentUuid;
|
params.settings_agent = agentUuid;
|
||||||
|
|||||||
+3
-1
@@ -41,11 +41,13 @@ async function collectAllAgents() {
|
|||||||
$set: {
|
$set: {
|
||||||
uuid: a.uuid,
|
uuid: a.uuid,
|
||||||
serial: a.serial || a.uuid,
|
serial: a.serial || a.uuid,
|
||||||
label: a.label,
|
|
||||||
site_uuid: 'global', // Store globally
|
site_uuid: 'global', // Store globally
|
||||||
provisioned: a.provisioned ?? true,
|
provisioned: a.provisioned ?? true,
|
||||||
activated: a.activated ?? false,
|
activated: a.activated ?? false,
|
||||||
last_seen_at: a.last_seen_at ?? null,
|
last_seen_at: a.last_seen_at ?? null,
|
||||||
|
},
|
||||||
|
$setOnInsert: {
|
||||||
|
label: a.label,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ upsert: true, new: true }
|
{ upsert: true, new: true }
|
||||||
|
|||||||
+2
-2
@@ -63,8 +63,8 @@ async function main() {
|
|||||||
console.log('╚════════════════════════════════════════════════╝\n');
|
console.log('╚════════════════════════════════════════════════╝\n');
|
||||||
console.log(`[Proxy] Mode: ${process.env.PROXY_COLLECT_MODE || 'all'}`);
|
console.log(`[Proxy] Mode: ${process.env.PROXY_COLLECT_MODE || 'all'}`);
|
||||||
|
|
||||||
// Bind to 127.0.0.1 in production — port 4000 must never be exposed externally
|
// In Docker, we should bind to 0.0.0.0. Security is handled by not exposing the port in docker-compose.
|
||||||
const BIND_HOST = process.env.NODE_ENV === 'production' ? '127.0.0.1' : '0.0.0.0';
|
const BIND_HOST = '0.0.0.0';
|
||||||
app.listen(PORT, BIND_HOST, () => {
|
app.listen(PORT, BIND_HOST, () => {
|
||||||
console.log(`\n🚀 Proxy REST API running at http://${BIND_HOST}:${PORT}`);
|
console.log(`\n🚀 Proxy REST API running at http://${BIND_HOST}:${PORT}`);
|
||||||
console.log(` GET /health → liveness check`);
|
console.log(` GET /health → liveness check`);
|
||||||
|
|||||||
@@ -1,27 +0,0 @@
|
|||||||
const mongoose = require('mongoose');
|
|
||||||
|
|
||||||
async function testFilter() {
|
|
||||||
await mongoose.connect('mongodb://backone_inspect:backone_inspect@mongodb.prod.proit.id:27017/backone_inspect_0');
|
|
||||||
console.log("Connected to MongoDB.");
|
|
||||||
|
|
||||||
const kantorUUID = 'F6-2V-DT-8A';
|
|
||||||
|
|
||||||
// 1. Check devices specifically tagged with Kantor's UUID
|
|
||||||
const kantorDevices = await mongoose.connection.collection('device_stats').find({ agent_uuid: kantorUUID }).toArray();
|
|
||||||
|
|
||||||
console.log(`\nTotal device records saved in Database specifically for Agent Kantor (${kantorUUID}): ${kantorDevices.length}`);
|
|
||||||
|
|
||||||
// 2. See if any of these explicitly-tagged Kantor records have Balaraja's IP (10.6.x.x)
|
|
||||||
const contaminated = kantorDevices.filter(d => d.ip_address && d.ip_address.startsWith('10.6.'));
|
|
||||||
|
|
||||||
console.log(`\nOUT OF THOSE, how many have Balaraja IP (10.6.x.x) despite being tagged as Kantor? ${contaminated.length}`);
|
|
||||||
|
|
||||||
if (contaminated.length > 0) {
|
|
||||||
console.log("Example contaminated IPs in DB under Kantor's UUID:");
|
|
||||||
console.log(contaminated.slice(0, 5).map(d => `- ${d.ip_address}`).join('\n'));
|
|
||||||
}
|
|
||||||
|
|
||||||
process.exit(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
testFilter();
|
|
||||||
@@ -21,7 +21,7 @@ export default function UptimeStatsCards({
|
|||||||
<div className="absolute top-0 right-0 p-3 opacity-10 group-hover:opacity-20 transition-opacity">
|
<div className="absolute top-0 right-0 p-3 opacity-10 group-hover:opacity-20 transition-opacity">
|
||||||
<Server className="w-12 h-12 text-blue-400" />
|
<Server className="w-12 h-12 text-blue-400" />
|
||||||
</div>
|
</div>
|
||||||
<span className="text-slate-400 uppercase tracking-wider mb-1 text-[10px] font-medium">Total Agents</span>
|
<span className="text-slate-400 uppercase tracking-wider mb-1 text-[10px] font-medium">TOTAL AGENTS</span>
|
||||||
<span className="text-2xl font-bold text-white tracking-tight">{totalAgents}</span>
|
<span className="text-2xl font-bold text-white tracking-tight">{totalAgents}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-slate-900/50 backdrop-blur-sm border border-slate-800/80 p-5 rounded-2xl flex flex-col relative overflow-hidden group">
|
<div className="bg-slate-900/50 backdrop-blur-sm border border-slate-800/80 p-5 rounded-2xl flex flex-col relative overflow-hidden group">
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { Column } from "@/components/ui/DataTable";
|
|||||||
import { Agent } from "@/lib/actions/agentsCore";
|
import { Agent } from "@/lib/actions/agentsCore";
|
||||||
import { Badge } from "@/components/ui/Badge";
|
import { Badge } from "@/components/ui/Badge";
|
||||||
import { resolveAgentLabel, type ManagedUser } from "@/lib/admin-api";
|
import { resolveAgentLabel, type ManagedUser } from "@/lib/admin-api";
|
||||||
import { Loader2, Trash2, CheckCircle2, XCircle, ChevronRight, UserCog, Eye, MapPin, Network } from "lucide-react";
|
import { Loader2, Trash2, CheckCircle2, XCircle, ChevronRight, UserCog, Eye, MapPin, Network, Edit2 } from "lucide-react";
|
||||||
|
|
||||||
interface GetColumnsProps {
|
interface GetColumnsProps {
|
||||||
role: string | null;
|
role: string | null;
|
||||||
@@ -18,6 +18,7 @@ interface GetColumnsProps {
|
|||||||
handleViewAs: (uuid: string, label: string) => void;
|
handleViewAs: (uuid: string, label: string) => void;
|
||||||
openDelete: (agent: Agent) => void;
|
openDelete: (agent: Agent) => void;
|
||||||
handleEditLocation?: (agentUuid: string, agentLabel: string) => void;
|
handleEditLocation?: (agentUuid: string, agentLabel: string) => void;
|
||||||
|
handleEditLabel?: (agentUuid: string, currentLabel: string) => void;
|
||||||
handleEditSubnet?: (agentUuid: string, agentLabel: string) => void;
|
handleEditSubnet?: (agentUuid: string, agentLabel: string) => void;
|
||||||
/** Set of agent UUIDs that already have a location saved */
|
/** Set of agent UUIDs that already have a location saved */
|
||||||
locationUuids?: Set<string>;
|
locationUuids?: Set<string>;
|
||||||
@@ -35,6 +36,7 @@ export function getAgentColumns({
|
|||||||
handleViewAs,
|
handleViewAs,
|
||||||
openDelete,
|
openDelete,
|
||||||
handleEditLocation,
|
handleEditLocation,
|
||||||
|
handleEditLabel,
|
||||||
handleEditSubnet,
|
handleEditSubnet,
|
||||||
locationUuids = new Set(),
|
locationUuids = new Set(),
|
||||||
}: GetColumnsProps): Column<Agent>[] {
|
}: GetColumnsProps): Column<Agent>[] {
|
||||||
@@ -55,8 +57,7 @@ export function getAgentColumns({
|
|||||||
accessor: (row) => {
|
accessor: (row) => {
|
||||||
const uuid = row.uuid || row.serial;
|
const uuid = row.uuid || row.serial;
|
||||||
const dynamicLabel = resolveAgentLabel(uuid, managedUsers);
|
const dynamicLabel = resolveAgentLabel(uuid, managedUsers);
|
||||||
const hasAccount = managedUsers.some(u => u.agent_uuid === uuid && u.role === 'AGENT_VIEWER');
|
const displayLabel = row.label || (dynamicLabel !== uuid ? dynamicLabel : uuid);
|
||||||
const displayLabel = dynamicLabel !== uuid ? dynamicLabel : (row.label || uuid);
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center gap-2 mx-auto max-w-full">
|
<div className="flex items-center justify-center gap-2 mx-auto max-w-full">
|
||||||
<button
|
<button
|
||||||
@@ -64,11 +65,8 @@ export function getAgentColumns({
|
|||||||
className="text-center hover:text-white transition-colors truncate"
|
className="text-center hover:text-white transition-colors truncate"
|
||||||
title={displayLabel}
|
title={displayLabel}
|
||||||
>
|
>
|
||||||
{dynamicLabel !== uuid ? dynamicLabel : (row.label || <span className="text-muted-foreground italic font-mono text-xs">{uuid}</span>)}
|
{displayLabel !== uuid ? displayLabel : <span className="text-muted-foreground italic font-mono text-xs">{uuid}</span>}
|
||||||
</button>
|
</button>
|
||||||
{!hasAccount && (role === 'SUPER_ADMIN' || role === 'TENANT_ADMIN') && (
|
|
||||||
<Badge variant="orange" className="text-[10px] px-1.5 py-0.5 font-semibold shrink-0 whitespace-nowrap">No account yet</Badge>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -105,7 +103,7 @@ export function getAgentColumns({
|
|||||||
className: "w-[10%] text-center",
|
className: "w-[10%] text-center",
|
||||||
accessor: (row) => {
|
accessor: (row) => {
|
||||||
const uuid = row.uuid || row.serial;
|
const uuid = row.uuid || row.serial;
|
||||||
const uptime = uptimeMap[uuid] ?? 100;
|
const uptime = uptimeMap[uuid] ?? 0;
|
||||||
let color = "text-emerald-400";
|
let color = "text-emerald-400";
|
||||||
if (uptime < 90) color = "text-red-400";
|
if (uptime < 90) color = "text-red-400";
|
||||||
else if (uptime < 98) color = "text-amber-400";
|
else if (uptime < 98) color = "text-amber-400";
|
||||||
@@ -120,7 +118,7 @@ export function getAgentColumns({
|
|||||||
const dynamicLabel = resolveAgentLabel(uuid, managedUsers);
|
const dynamicLabel = resolveAgentLabel(uuid, managedUsers);
|
||||||
const existingUser = managedUsers.find(u => u.agent_uuid === uuid && u.role === 'AGENT_VIEWER') || null;
|
const existingUser = managedUsers.find(u => u.agent_uuid === uuid && u.role === 'AGENT_VIEWER') || null;
|
||||||
const isViewAsLoading = viewAsLoading === uuid;
|
const isViewAsLoading = viewAsLoading === uuid;
|
||||||
const agentLabel = dynamicLabel !== uuid ? dynamicLabel : (row.label || uuid);
|
const agentLabel = row.label || (dynamicLabel !== uuid ? dynamicLabel : uuid);
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center gap-1 mx-auto">
|
<div className="flex items-center justify-center gap-1 mx-auto">
|
||||||
<button onClick={() => setDetailAgent(row)} className="p-1 text-blue-400 hover:text-blue-300 hover:bg-blue-950/30 rounded transition-colors" title="Detail Agent">
|
<button onClick={() => setDetailAgent(row)} className="p-1 text-blue-400 hover:text-blue-300 hover:bg-blue-950/30 rounded transition-colors" title="Detail Agent">
|
||||||
@@ -128,6 +126,15 @@ export function getAgentColumns({
|
|||||||
</button>
|
</button>
|
||||||
{(role === 'SUPER_ADMIN' || role === 'TENANT_ADMIN') && (
|
{(role === 'SUPER_ADMIN' || role === 'TENANT_ADMIN') && (
|
||||||
<>
|
<>
|
||||||
|
{handleEditLabel && (
|
||||||
|
<button
|
||||||
|
onClick={(e) => { e.stopPropagation(); handleEditLabel(uuid, agentLabel); }}
|
||||||
|
className="p-1 text-blue-400 hover:text-blue-300 hover:bg-blue-950/30 rounded transition-colors"
|
||||||
|
title="Edit Label Agent"
|
||||||
|
>
|
||||||
|
<Edit2 className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
<button
|
<button
|
||||||
onClick={(e) => { e.stopPropagation(); setAccountModalAgent({ uuid, label: agentLabel }); setAccountModalOpen(true); }}
|
onClick={(e) => { e.stopPropagation(); setAccountModalAgent({ uuid, label: agentLabel }); setAccountModalOpen(true); }}
|
||||||
className="p-1 text-emerald-400 hover:text-emerald-300 hover:bg-emerald-950/30 rounded transition-colors"
|
className="p-1 text-emerald-400 hover:text-emerald-300 hover:bg-emerald-950/30 rounded transition-colors"
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ export default function AgentsPage() {
|
|||||||
handleViewAs,
|
handleViewAs,
|
||||||
openDelete,
|
openDelete,
|
||||||
handleEditLocation,
|
handleEditLocation,
|
||||||
|
handleEditLabel,
|
||||||
} = useAgentsData();
|
} = useAgentsData();
|
||||||
|
|
||||||
const [subnetModalOpen, setSubnetModalOpen] = useState(false);
|
const [subnetModalOpen, setSubnetModalOpen] = useState(false);
|
||||||
@@ -82,6 +83,7 @@ export default function AgentsPage() {
|
|||||||
handleViewAs,
|
handleViewAs,
|
||||||
openDelete,
|
openDelete,
|
||||||
handleEditLocation,
|
handleEditLocation,
|
||||||
|
handleEditLabel,
|
||||||
handleEditSubnet,
|
handleEditSubnet,
|
||||||
locationUuids: new Set(locations.map((l: any) => l.agent_uuid)),
|
locationUuids: new Set(locations.map((l: any) => l.agent_uuid)),
|
||||||
});
|
});
|
||||||
@@ -97,8 +99,8 @@ export default function AgentsPage() {
|
|||||||
return hum === 'Online' || hum === 'Flows Detected' || (hum && hum.startsWith('Last seen:'));
|
return hum === 'Online' || hum === 'Flows Detected' || (hum && hum.startsWith('Last seen:'));
|
||||||
}).length;
|
}).length;
|
||||||
const offlineAgents = totalAgents - onlineAgents;
|
const offlineAgents = totalAgents - onlineAgents;
|
||||||
const uptimeValues = agents.map(a => uptimeMap[a.uuid || a.serial] ?? 100);
|
const uptimeValues = agents.map(a => uptimeMap[a.uuid || a.serial] ?? 0);
|
||||||
const avgUptime = uptimeValues.length > 0 ? uptimeValues.reduce((s, v) => s + v, 0) / uptimeValues.length : 100;
|
const avgUptime = uptimeValues.length > 0 ? uptimeValues.reduce((s, v) => s + v, 0) / uptimeValues.length : 0;
|
||||||
|
|
||||||
// External Accounts di halaman Agents = akun eksternal teknikal/operasional saja
|
// External Accounts di halaman Agents = akun eksternal teknikal/operasional saja
|
||||||
// AGENT_VIEWER = akun network agent (sudah ada di tabel agents) → BUKAN akun eksternal
|
// AGENT_VIEWER = akun network agent (sudah ada di tabel agents) → BUKAN akun eksternal
|
||||||
@@ -173,11 +175,10 @@ export default function AgentsPage() {
|
|||||||
.filter(loc => agents.some(a => (a.uuid || a.serial) === loc.agent_uuid))
|
.filter(loc => agents.some(a => (a.uuid || a.serial) === loc.agent_uuid))
|
||||||
.map(loc => {
|
.map(loc => {
|
||||||
const agent = agents.find(a => (a.uuid || a.serial) === loc.agent_uuid);
|
const agent = agents.find(a => (a.uuid || a.serial) === loc.agent_uuid);
|
||||||
|
const dynamicLabel = resolveAgentLabel(loc.agent_uuid, managedUsers);
|
||||||
const agentLabel = agent
|
const agentLabel = agent
|
||||||
? (resolveAgentLabel(loc.agent_uuid, managedUsers) !== loc.agent_uuid
|
? (agent.label || (dynamicLabel !== loc.agent_uuid ? dynamicLabel : loc.agent_uuid))
|
||||||
? resolveAgentLabel(loc.agent_uuid, managedUsers)
|
: (loc.label || (dynamicLabel !== loc.agent_uuid ? dynamicLabel : loc.agent_uuid));
|
||||||
: (agent.label || loc.label || loc.agent_uuid))
|
|
||||||
: (loc.label || loc.agent_uuid);
|
|
||||||
const isOnline = !!(agent?.last_seen_at?.human === 'Online' ||
|
const isOnline = !!(agent?.last_seen_at?.human === 'Online' ||
|
||||||
agent?.last_seen_at?.human === 'Flows Detected' ||
|
agent?.last_seen_at?.human === 'Flows Detected' ||
|
||||||
(agent?.last_seen_at?.human && agent.last_seen_at.human.startsWith('Last seen:')));
|
(agent?.last_seen_at?.human && agent.last_seen_at.human.startsWith('Last seen:')));
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { getAgents } from "@/lib/actions/agents";
|
import { getAgents, updateAgent } from "@/lib/actions/agents";
|
||||||
import { Agent } from "@/lib/actions/agentsCore";
|
import { Agent } from "@/lib/actions/agentsCore";
|
||||||
import { getAdminUsers, startViewAs, getViewAsHeaders, type ManagedUser } from "@/lib/admin-api";
|
import { getAdminUsers, startViewAs, getViewAsHeaders, type ManagedUser } from "@/lib/admin-api";
|
||||||
import { useTimeFilter } from "@/contexts/TimeFilterContext";
|
import { useTimeFilter } from "@/contexts/TimeFilterContext";
|
||||||
@@ -194,6 +194,25 @@ export function useAgentsData() {
|
|||||||
setIsLocModalOpen(true);
|
setIsLocModalOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleEditLabel = async (agentUuid: string, currentLabel: string) => {
|
||||||
|
const newLabel = window.prompt("Masukkan label baru untuk agent ini:", currentLabel);
|
||||||
|
if (newLabel !== null && newLabel.trim() !== "" && newLabel !== currentLabel) {
|
||||||
|
try {
|
||||||
|
const result = await updateAgent(agentUuid, newLabel.trim());
|
||||||
|
if (result.success) {
|
||||||
|
// Update local state without waiting for full refresh to feel snappy
|
||||||
|
setAgents(prev => prev.map(a =>
|
||||||
|
(a.uuid === agentUuid || a.serial === agentUuid) ? { ...a, label: newLabel.trim() } : a
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
window.alert("Gagal mengubah label: " + result.message);
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
window.alert("Terjadi kesalahan: " + err.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
role,
|
role,
|
||||||
agents,
|
agents,
|
||||||
@@ -236,5 +255,6 @@ export function useAgentsData() {
|
|||||||
handleViewAs,
|
handleViewAs,
|
||||||
openDelete,
|
openDelete,
|
||||||
handleEditLocation,
|
handleEditLocation,
|
||||||
|
handleEditLabel,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -163,6 +163,12 @@ export default function DevicesPage() {
|
|||||||
if (filters.device_type && filters.device_type !== "All" && row.device_type !== filters.device_type) return false;
|
if (filters.device_type && filters.device_type !== "All" && row.device_type !== filters.device_type) return false;
|
||||||
if (filters.os_label && filters.os_label !== "All" && row.os_label !== filters.os_label) return false;
|
if (filters.os_label && filters.os_label !== "All" && row.os_label !== filters.os_label) return false;
|
||||||
if (filters.manufacturer && filters.manufacturer !== "All" && row.manufacturer !== filters.manufacturer) return false;
|
if (filters.manufacturer && filters.manufacturer !== "All" && row.manufacturer !== filters.manufacturer) return false;
|
||||||
|
if (filters.ip && filters.ip.trim() !== "" && filters.ip !== "All") {
|
||||||
|
if (!row.ip_address || !row.ip_address.toLowerCase().includes(filters.ip.toLowerCase())) return false;
|
||||||
|
}
|
||||||
|
if (filters.mac && filters.mac.trim() !== "" && filters.mac !== "All") {
|
||||||
|
if (!row.mac_address || !row.mac_address.toLowerCase().includes(filters.mac.toLowerCase())) return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (dateFrom || dateTo) {
|
if (dateFrom || dateTo) {
|
||||||
const d = row.last_seen ? new Date(row.last_seen).toISOString().slice(0, 10) : "";
|
const d = row.last_seen ? new Date(row.last_seen).toISOString().slice(0, 10) : "";
|
||||||
|
|||||||
@@ -107,10 +107,11 @@ const AgentLocationMap = React.memo(function AgentLocationMap({
|
|||||||
});
|
});
|
||||||
instanceRef.current = map;
|
instanceRef.current = map;
|
||||||
|
|
||||||
L.tileLayer("https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png", {
|
L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
|
||||||
subdomains: "abcd",
|
subdomains: "abc",
|
||||||
maxZoom: 20,
|
maxZoom: 19,
|
||||||
noWrap: true,
|
noWrap: true,
|
||||||
|
className: "dark-map-tiles"
|
||||||
}).addTo(map);
|
}).addTo(map);
|
||||||
|
|
||||||
if (pins.length > 1) {
|
if (pins.length > 1) {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
export const MAP_CSS = `
|
export const MAP_CSS = `
|
||||||
|
.dark-map-tiles { filter: invert(100%) hue-rotate(180deg) brightness(95%) contrast(90%); }
|
||||||
.leaflet-control-attribution { display:none !important; }
|
.leaflet-control-attribution { display:none !important; }
|
||||||
.agent-popup .leaflet-popup-content-wrapper { background:#0a1224!important; border:1px solid rgba(99,102,241,0.25)!important; box-shadow:0 12px 36px rgba(0,0,0,0.65)!important; padding:0!important; border-radius:12px!important; }
|
.agent-popup .leaflet-popup-content-wrapper { background:#0a1224!important; border:1px solid rgba(99,102,241,0.25)!important; box-shadow:0 12px 36px rgba(0,0,0,0.65)!important; padding:0!important; border-radius:12px!important; }
|
||||||
.agent-popup .leaflet-popup-content { margin:0!important; }
|
.agent-popup .leaflet-popup-content { margin:0!important; }
|
||||||
|
|||||||
@@ -82,8 +82,8 @@ export async function bootMapInstance({
|
|||||||
const centerLng = 0;
|
const centerLng = 0;
|
||||||
|
|
||||||
const tileUrl = isDark
|
const tileUrl = isDark
|
||||||
? "https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png"
|
? "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
||||||
: "https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png";
|
: "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png";
|
||||||
|
|
||||||
if (!mapInstanceRef.current) {
|
if (!mapInstanceRef.current) {
|
||||||
const map = L.map(mapContainer, {
|
const map = L.map(mapContainer, {
|
||||||
@@ -94,7 +94,7 @@ export async function bootMapInstance({
|
|||||||
markerZoomAnimation: false,
|
markerZoomAnimation: false,
|
||||||
}).setView([centerLat, centerLng], 2, { animate: false }); // Zoom 2 for global view
|
}).setView([centerLat, centerLng], 2, { animate: false }); // Zoom 2 for global view
|
||||||
|
|
||||||
const tiles = L.tileLayer(tileUrl, { maxZoom: 19 }).addTo(map);
|
const tiles = L.tileLayer(tileUrl, { maxZoom: 19, className: isDark ? 'dark-map-tiles' : '' }).addTo(map);
|
||||||
tileLayerRef.current = tiles;
|
tileLayerRef.current = tiles;
|
||||||
L.control.zoom({ position: "bottomright" }).addTo(map);
|
L.control.zoom({ position: "bottomright" }).addTo(map);
|
||||||
mapInstanceRef.current = map;
|
mapInstanceRef.current = map;
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ export function TopWidgets({ topApps, topProtocols }: TopWidgetsProps) {
|
|||||||
<div className="col-span-1 lg:col-span-3 space-y-6 w-full min-w-0">
|
<div className="col-span-1 lg:col-span-3 space-y-6 w-full min-w-0">
|
||||||
<Card className="bg-card/80 backdrop-blur-xl border-border/50 flex-1 min-w-0">
|
<Card className="bg-card/80 backdrop-blur-xl border-border/50 flex-1 min-w-0">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-lg text-white">Top Apps (Total Bandwidth)</CardTitle>
|
<CardTitle className="text-lg text-white">Top Apps</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="w-full min-w-0">
|
<CardContent className="w-full min-w-0">
|
||||||
{(!appsData || appsData.length === 0) ? (
|
{(!appsData || appsData.length === 0) ? (
|
||||||
|
|||||||
@@ -5,17 +5,86 @@ import {
|
|||||||
getSiteUuid,
|
getSiteUuid,
|
||||||
getHeaders,
|
getHeaders,
|
||||||
API_URL,
|
API_URL,
|
||||||
createAgent,
|
createAgent
|
||||||
updateAgent,
|
|
||||||
deleteAgent
|
|
||||||
} from "./agentsCore";
|
} from "./agentsCore";
|
||||||
import type { Agent } from "./agentsCore";
|
import type { Agent } from "./agentsCore";
|
||||||
import { fetchAgentsFromMongo } from "./agentsMongo";
|
import { fetchAgentsFromMongo } from "./agentsMongo";
|
||||||
import { getAuthUser } from "./authHelper";
|
import { getAuthUser } from "./authHelper";
|
||||||
import mongoose from "mongoose";
|
import mongoose from "mongoose";
|
||||||
|
|
||||||
// Re-export mutations for backwards compatibility
|
// Override mutations to use MongoDB for On-Premise mode
|
||||||
export { createAgent, updateAgent, deleteAgent };
|
export async function updateAgent(agentId: string, label: string) {
|
||||||
|
try {
|
||||||
|
const MONGODB_URI = process.env.MONGODB_URI || 'mongodb://127.0.0.1:27017/backone_dpi';
|
||||||
|
if (mongoose.connection.readyState !== 1) {
|
||||||
|
await mongoose.connect(MONGODB_URI, { serverSelectionTimeoutMS: 5000 });
|
||||||
|
}
|
||||||
|
const db = mongoose.connection.db;
|
||||||
|
if (!db) {
|
||||||
|
throw new Error("Database tidak tersedia.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if agent exists in agent_registry
|
||||||
|
const agent = await db.collection('agent_registry').findOne({ $or: [{ uuid: agentId }, { serial: agentId }] });
|
||||||
|
if (!agent) {
|
||||||
|
return { success: false, message: "Agent tidak ditemukan di database lokal." };
|
||||||
|
}
|
||||||
|
|
||||||
|
await db.collection('agent_registry').updateOne(
|
||||||
|
{ _id: agent._id },
|
||||||
|
{ $set: { label: label } }
|
||||||
|
);
|
||||||
|
|
||||||
|
// Bismillah: Hapus semua data duplikat/hantu milik agent ini agar tidak ditimpa oleh proxy lawas/bug ganda
|
||||||
|
await db.collection('agent_registry').deleteMany({
|
||||||
|
$or: [{ uuid: agentId }, { serial: agentId }],
|
||||||
|
_id: { $ne: agent._id }
|
||||||
|
});
|
||||||
|
|
||||||
|
// Also try to update the summaries if possible, so historical displays match
|
||||||
|
await db.collection('summaries').updateMany(
|
||||||
|
{ agent_uuid: agent.uuid },
|
||||||
|
{ $set: { agent_label: label } }
|
||||||
|
);
|
||||||
|
|
||||||
|
const { revalidatePath } = require('next/cache');
|
||||||
|
revalidatePath('/agents');
|
||||||
|
|
||||||
|
return { success: true, message: "Label berhasil diubah." };
|
||||||
|
} catch (err: any) {
|
||||||
|
return { success: false, message: err.message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteAgent(agentId: string) {
|
||||||
|
try {
|
||||||
|
const MONGODB_URI = process.env.MONGODB_URI || 'mongodb://127.0.0.1:27017/backone_dpi';
|
||||||
|
if (mongoose.connection.readyState !== 1) {
|
||||||
|
await mongoose.connect(MONGODB_URI, { serverSelectionTimeoutMS: 5000 });
|
||||||
|
}
|
||||||
|
const db = mongoose.connection.db;
|
||||||
|
if (!db) {
|
||||||
|
throw new Error("Database tidak tersedia.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if agent exists in agent_registry
|
||||||
|
const agent = await db.collection('agent_registry').findOne({ $or: [{ uuid: agentId }, { serial: agentId }] });
|
||||||
|
if (!agent) {
|
||||||
|
return { success: false, message: "Agent tidak ditemukan di database lokal." };
|
||||||
|
}
|
||||||
|
|
||||||
|
await db.collection('agent_registry').deleteOne({ _id: agent._id });
|
||||||
|
|
||||||
|
const { revalidatePath } = require('next/cache');
|
||||||
|
revalidatePath('/agents');
|
||||||
|
|
||||||
|
return { success: true, message: "Agent berhasil dihapus." };
|
||||||
|
} catch (err: any) {
|
||||||
|
return { success: false, message: err.message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { createAgent };
|
||||||
|
|
||||||
export async function getAgents(clientSiteUuid?: string): Promise<Agent[]> {
|
export async function getAgents(clientSiteUuid?: string): Promise<Agent[]> {
|
||||||
try {
|
try {
|
||||||
@@ -35,7 +104,7 @@ export async function getAgents(clientSiteUuid?: string): Promise<Agent[]> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// All agent data comes from MongoDB agent_registry (single source of truth)
|
// All agent data comes from MongoDB agent_registry (single source of truth)
|
||||||
if (API_KEY && API_KEY.startsWith("sk_db_")) {
|
if (true) {
|
||||||
const MONGODB_URI = process.env.MONGODB_URI || 'mongodb://127.0.0.1:27017/backone_dpi';
|
const MONGODB_URI = process.env.MONGODB_URI || 'mongodb://127.0.0.1:27017/backone_dpi';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -47,6 +116,10 @@ export async function getAgents(clientSiteUuid?: string): Promise<Agent[]> {
|
|||||||
return []; // Do not throw to avoid Next.js Server Component crash
|
return []; // Do not throw to avoid Next.js Server Component crash
|
||||||
}
|
}
|
||||||
const db = mongoose.connection.db;
|
const db = mongoose.connection.db;
|
||||||
|
if (!db) {
|
||||||
|
console.error("[getAgents Action] Database is undefined.");
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
return await fetchAgentsFromMongo(db, user, SITE_UUID);
|
return await fetchAgentsFromMongo(db, user, SITE_UUID);
|
||||||
}
|
}
|
||||||
@@ -79,7 +152,7 @@ export async function getAgents(clientSiteUuid?: string): Promise<Agent[]> {
|
|||||||
export async function getAgent(agentId: string): Promise<Agent | null> {
|
export async function getAgent(agentId: string): Promise<Agent | null> {
|
||||||
try {
|
try {
|
||||||
const API_KEY = getApiKey();
|
const API_KEY = getApiKey();
|
||||||
if (API_KEY && API_KEY.startsWith("sk_db_")) {
|
if (true) {
|
||||||
const agents = await getAgents();
|
const agents = await getAgents();
|
||||||
return agents.find(a => a.uuid === agentId || String(a.id) === agentId) || null;
|
return agents.find(a => a.uuid === agentId || String(a.id) === agentId) || null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,8 +14,10 @@ export async function fetchAgentsFromMongo(
|
|||||||
const filter: any = {};
|
const filter: any = {};
|
||||||
|
|
||||||
if (!isCompanyRole && SITE_UUID) {
|
if (!isCompanyRole && SITE_UUID) {
|
||||||
if (SITE_UUID === 'all') {
|
if (SITE_UUID === 'all' || SITE_UUID === 'test-site' || SITE_UUID === 'default' || SITE_UUID === 'global') {
|
||||||
const envSites = (process.env.BACKONE_SITE_UUIDS || process.env.BACKONE_SITE_UUID || '').split(',').map(s => s.trim()).filter(Boolean);
|
const dynamicEnv = process['env'];
|
||||||
|
const envSites = (dynamicEnv.BACKONE_SITE_UUIDS || dynamicEnv.BACKONE_SITE_UUID || '').split(',').map(s => s.trim()).filter(Boolean);
|
||||||
|
|
||||||
if (envSites.length > 0) {
|
if (envSites.length > 0) {
|
||||||
filter.site_uuid = { $in: [...envSites, 'global'] };
|
filter.site_uuid = { $in: [...envSites, 'global'] };
|
||||||
}
|
}
|
||||||
@@ -24,6 +26,8 @@ export async function fetchAgentsFromMongo(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if (user.role === 'AGENT_VIEWER' && user.agent_uuid) {
|
if (user.role === 'AGENT_VIEWER' && user.agent_uuid) {
|
||||||
filter.uuid = user.agent_uuid;
|
filter.uuid = user.agent_uuid;
|
||||||
}
|
}
|
||||||
@@ -85,7 +89,7 @@ export async function fetchAgentsFromMongo(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 3. Optimize status check and last seen per agent using parallel indexed queries
|
// 3. Optimize status check and last seen per agent using parallel indexed queries
|
||||||
const twentyFourHoursAgo = new Date(Date.now() - 24 * 3600000);
|
const fifteenMinutesAgoQuery = new Date(Date.now() - 15 * 60000);
|
||||||
|
|
||||||
const agentResults = await Promise.all(
|
const agentResults = await Promise.all(
|
||||||
agentSource.map(async (item: any, idx: number) => {
|
agentSource.map(async (item: any, idx: number) => {
|
||||||
@@ -95,7 +99,7 @@ export async function fetchAgentsFromMongo(
|
|||||||
// Execute status checks for active flow, latest flow, and latest summary in parallel
|
// Execute status checks for active flow, latest flow, and latest summary in parallel
|
||||||
const [activeFlow, latestFlow, latestSummary] = await Promise.all([
|
const [activeFlow, latestFlow, latestSummary] = await Promise.all([
|
||||||
db.collection('flows').findOne(
|
db.collection('flows').findOne(
|
||||||
{ agent_uuid: uuid, timestamp: { $gte: twentyFourHoursAgo } },
|
{ agent_uuid: uuid, timestamp: { $gte: fifteenMinutesAgoQuery } },
|
||||||
{ projection: { _id: 1 } }
|
{ projection: { _id: 1 } }
|
||||||
),
|
),
|
||||||
db.collection('flows').findOne(
|
db.collection('flows').findOne(
|
||||||
@@ -108,8 +112,6 @@ export async function fetchAgentsFromMongo(
|
|||||||
),
|
),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const isOnline = !!activeFlow;
|
|
||||||
|
|
||||||
const registryLastSeenRaw = item.last_seen_at;
|
const registryLastSeenRaw = item.last_seen_at;
|
||||||
let lastSeenDate: Date | null = null;
|
let lastSeenDate: Date | null = null;
|
||||||
if (registryLastSeenRaw?.date) {
|
if (registryLastSeenRaw?.date) {
|
||||||
@@ -133,19 +135,21 @@ export async function fetchAgentsFromMongo(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isOnline = !!activeFlow;
|
||||||
|
|
||||||
let statusHuman = 'No Flows Detected';
|
let statusHuman = 'No Flows Detected';
|
||||||
if (isOnline) {
|
if (lastSeenDate) {
|
||||||
if (lastSeenDate) {
|
const now = new Date();
|
||||||
const now = new Date();
|
const isToday = lastSeenDate.getDate() === now.getDate() &&
|
||||||
const isToday = lastSeenDate.getDate() === now.getDate() &&
|
lastSeenDate.getMonth() === now.getMonth() &&
|
||||||
lastSeenDate.getMonth() === now.getMonth() &&
|
lastSeenDate.getFullYear() === now.getFullYear();
|
||||||
lastSeenDate.getFullYear() === now.getFullYear();
|
const timeStr = lastSeenDate.toLocaleTimeString('id-ID', { timeZone: 'Asia/Jakarta', hour: '2-digit', minute: '2-digit' }) + ' WIB';
|
||||||
const timeStr = lastSeenDate.toLocaleTimeString('id-ID', { timeZone: 'Asia/Jakarta', hour: '2-digit', minute: '2-digit' }) + ' WIB';
|
const dateStr = lastSeenDate.toLocaleDateString('id-ID', { timeZone: 'Asia/Jakarta', day: '2-digit', month: '2-digit' });
|
||||||
const dateStr = lastSeenDate.toLocaleDateString('id-ID', { timeZone: 'Asia/Jakarta', day: '2-digit', month: '2-digit' });
|
const seenStr = isToday ? timeStr : `${dateStr} ${timeStr}`;
|
||||||
statusHuman = isToday ? `Last seen: ${timeStr}` : `Last seen: ${dateStr} ${timeStr}`;
|
|
||||||
} else {
|
statusHuman = isOnline ? `Last seen: ${seenStr}` : `Offline (Last seen: ${seenStr})`;
|
||||||
statusHuman = 'Last seen: Just now';
|
} else if (isOnline) {
|
||||||
}
|
statusHuman = 'Last seen: Just now';
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -1,175 +0,0 @@
|
|||||||
// test/agent_geolocation_test.js
|
|
||||||
const mongoose = require('../backend/node_modules/mongoose');
|
|
||||||
const path = require('path');
|
|
||||||
|
|
||||||
// Load environment variables
|
|
||||||
require('dotenv').config({ path: path.join(__dirname, '../.env.local') });
|
|
||||||
|
|
||||||
const MONGODB_URI = process.env.MONGODB_URI || 'mongodb://127.0.0.1:27017/backone_dpi';
|
|
||||||
|
|
||||||
const { CustomAgentLocation, DeviceStat, Flow } = require('../backend/models/Schemas');
|
|
||||||
|
|
||||||
let testCount = 0;
|
|
||||||
let passedCount = 0;
|
|
||||||
|
|
||||||
function assert(label, condition) {
|
|
||||||
testCount++;
|
|
||||||
if (condition) {
|
|
||||||
console.log(` ✓ PASSED: ${label}`);
|
|
||||||
passedCount++;
|
|
||||||
} else {
|
|
||||||
console.error(` ✗ FAILED: ${label}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function runTests() {
|
|
||||||
console.log('\n=========================================');
|
|
||||||
console.log(' RUNNING TDD TESTS FOR GEOLOCATION & FLOWS');
|
|
||||||
console.log('=========================================\n');
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 1. Connect to DB
|
|
||||||
await mongoose.connect(MONGODB_URI);
|
|
||||||
console.log('✓ Connected to MongoDB');
|
|
||||||
|
|
||||||
// 2. Validate Geolocation Seeding
|
|
||||||
const count = await CustomAgentLocation.countDocuments();
|
|
||||||
assert('CustomAgentLocation collection should have seeded documents', count >= 3);
|
|
||||||
|
|
||||||
const balarajaLoc = await CustomAgentLocation.findOne({ agent_uuid: 'F6-2V-DT-8A' });
|
|
||||||
assert('CPI Balaraja Agent location should exist', !!balarajaLoc);
|
|
||||||
if (balarajaLoc) {
|
|
||||||
assert('CPI Balaraja latitude matches', balarajaLoc.latitude === -6.2263304);
|
|
||||||
assert('CPI Balaraja longitude matches', balarajaLoc.longitude === 106.4247322);
|
|
||||||
assert('CPI Balaraja belongs to SIAB site', balarajaLoc.site_uuid === '6681452d_9cae_4ff4_8ae8_0d504774265e');
|
|
||||||
}
|
|
||||||
|
|
||||||
const cibuburLoc = await CustomAgentLocation.findOne({ agent_uuid: '2F-TF-1D-GK' });
|
|
||||||
assert('JRP Cibubur Agent location should exist', !!cibuburLoc);
|
|
||||||
if (cibuburLoc) {
|
|
||||||
assert('JRP Cibubur latitude matches', cibuburLoc.latitude === -6.3763318);
|
|
||||||
assert('JRP Cibubur longitude matches', cibuburLoc.longitude === 106.8983017);
|
|
||||||
}
|
|
||||||
|
|
||||||
const ifgLoc = await CustomAgentLocation.findOne({ agent_uuid: '8A-V3-PB-85' });
|
|
||||||
assert('IFG LT.18 Agent location should exist', !!ifgLoc);
|
|
||||||
if (ifgLoc) {
|
|
||||||
assert('IFG LT.18 latitude matches', ifgLoc.latitude === -6.2253265);
|
|
||||||
assert('IFG LT.18 longitude matches', ifgLoc.longitude === 106.8061484);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Test Location Updates (Simulation of POST API)
|
|
||||||
const testUuid = 'TEST-AGENT-UUID';
|
|
||||||
const testSite = 'test-site-uuid';
|
|
||||||
|
|
||||||
// Remove if exists
|
|
||||||
await CustomAgentLocation.deleteOne({ agent_uuid: testUuid });
|
|
||||||
|
|
||||||
const updated = await CustomAgentLocation.findOneAndUpdate(
|
|
||||||
{ agent_uuid: testUuid },
|
|
||||||
{
|
|
||||||
agent_uuid: testUuid,
|
|
||||||
site_uuid: testSite,
|
|
||||||
latitude: -6.1234,
|
|
||||||
longitude: 106.1234,
|
|
||||||
label: 'Test Location Label'
|
|
||||||
},
|
|
||||||
{ new: true, upsert: true }
|
|
||||||
);
|
|
||||||
assert('Location upsert should return the saved document', !!updated);
|
|
||||||
assert('Upserted latitude matches', updated.latitude === -6.1234);
|
|
||||||
assert('Upserted longitude matches', updated.longitude === 106.1234);
|
|
||||||
assert('Upserted label matches', updated.label === 'Test Location Label');
|
|
||||||
|
|
||||||
// 4. Test Location Reset (Simulation of DELETE API)
|
|
||||||
const delRes = await CustomAgentLocation.deleteOne({ agent_uuid: testUuid });
|
|
||||||
assert('Location delete should succeed', delRes.deletedCount > 0);
|
|
||||||
|
|
||||||
const checkedDel = await CustomAgentLocation.findOne({ agent_uuid: testUuid });
|
|
||||||
assert('Location should no longer exist after delete', !checkedDel);
|
|
||||||
|
|
||||||
// 5. Test Inter-Agent Flow Aggregation Logic
|
|
||||||
// Let's seed a mock device belonging to Cibubur agent and a flow from Balaraja to Cibubur's device IP.
|
|
||||||
const mockDestIp = '192.168.99.123';
|
|
||||||
|
|
||||||
// Cleanup if existing
|
|
||||||
await DeviceStat.deleteOne({ ip_address: mockDestIp });
|
|
||||||
await Flow.deleteMany({ dst_ip: mockDestIp });
|
|
||||||
|
|
||||||
// Seed mock device monitored by Cibubur agent (2F-TF-1D-GK)
|
|
||||||
await DeviceStat.create({
|
|
||||||
timestamp: new Date(),
|
|
||||||
ip_address: mockDestIp,
|
|
||||||
agent_uuid: '2F-TF-1D-GK',
|
|
||||||
site_uuid: '6681452d_9cae_4ff4_8ae8_0d504774265e',
|
|
||||||
mac_address: '00:11:22:33:44:55',
|
|
||||||
});
|
|
||||||
|
|
||||||
// Seed mock flow from Balaraja agent (F6-2V-DT-8A) to mockDestIp
|
|
||||||
await Flow.create({
|
|
||||||
timestamp: new Date(),
|
|
||||||
agent_uuid: 'F6-2V-DT-8A',
|
|
||||||
site_uuid: '6681452d_9cae_4ff4_8ae8_0d504774265e',
|
|
||||||
src_ip: '192.168.10.50',
|
|
||||||
dst_ip: mockDestIp,
|
|
||||||
download: 15000000,
|
|
||||||
upload: 5000000,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Run the aggregation logic
|
|
||||||
const devices = await DeviceStat.find({ site_uuid: '6681452d_9cae_4ff4_8ae8_0d504774265e' }).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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const flows = await Flow.find({ site_uuid: '6681452d_9cae_4ff4_8ae8_0d504774265e' }).select('agent_uuid src_ip dst_ip download upload').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
|
|
||||||
};
|
|
||||||
}
|
|
||||||
flowMap[key].bytes += ((flow.download || 0) + (flow.upload || 0));
|
|
||||||
flowMap[key].flowsCount += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = Object.values(flowMap);
|
|
||||||
assert('Inter-agent flows aggregation should detect flows', result.length > 0);
|
|
||||||
|
|
||||||
const detectedFlow = result.find(f => f.source === 'F6-2V-DT-8A' && f.target === '2F-TF-1D-GK');
|
|
||||||
assert('Mock flow from Balaraja to Cibubur should be detected', !!detectedFlow);
|
|
||||||
if (detectedFlow) {
|
|
||||||
assert('Mock flow bytes matches (15MB down + 5MB up = 20MB)', detectedFlow.bytes >= 20000000);
|
|
||||||
assert('Mock flow connection count is correct', detectedFlow.flowsCount >= 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cleanup mock telemetry
|
|
||||||
await DeviceStat.deleteOne({ ip_address: mockDestIp });
|
|
||||||
await Flow.deleteMany({ dst_ip: mockDestIp });
|
|
||||||
|
|
||||||
console.log('\n=========================================');
|
|
||||||
console.log(` RESULT: ${passedCount} / ${testCount} tests passed.`);
|
|
||||||
console.log('=========================================\n');
|
|
||||||
|
|
||||||
await mongoose.disconnect();
|
|
||||||
} catch (err) {
|
|
||||||
console.error('TDD Test run error:', err);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
runTests();
|
|
||||||
@@ -1,118 +0,0 @@
|
|||||||
// test/architecture_test.js
|
|
||||||
// TDD: Verifikasi arsitektur 2 container BackOne DPI
|
|
||||||
|
|
||||||
const fs = require('fs');
|
|
||||||
const path = require('path');
|
|
||||||
|
|
||||||
const ROOT = path.join(__dirname, '..');
|
|
||||||
|
|
||||||
function readFile(relPath) {
|
|
||||||
return fs.readFileSync(path.join(ROOT, relPath), 'utf8');
|
|
||||||
}
|
|
||||||
|
|
||||||
let passed = 0;
|
|
||||||
let failed = 0;
|
|
||||||
|
|
||||||
function test(name, condition, detail = '') {
|
|
||||||
if (condition) {
|
|
||||||
console.log(` ✓ PASS: ${name}`);
|
|
||||||
passed++;
|
|
||||||
} else {
|
|
||||||
console.log(` ✗ FAIL: ${name}${detail ? ' — ' + detail : ''}`);
|
|
||||||
failed++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('\n══════════════════════════════════════════════════════');
|
|
||||||
console.log(' BackOne DPI — TDD Architecture Verification');
|
|
||||||
console.log('══════════════════════════════════════════════════════\n');
|
|
||||||
|
|
||||||
// ─── TEST GROUP 1: Backend bersih dari SQLite/Netify API ───────────────────
|
|
||||||
console.log('[GROUP 1] Backend — Isolasi dari SQLite & Netify API');
|
|
||||||
const serverCode = readFile('backend/server.js');
|
|
||||||
const dashCode = readFile('backend/routes/dashboard.js') +
|
|
||||||
readFile('backend/routes/dashboard/helpers.js') +
|
|
||||||
readFile('backend/routes/dashboard/summary.js') +
|
|
||||||
readFile('backend/routes/dashboard/apps.js') +
|
|
||||||
readFile('backend/routes/dashboard/devices.js') +
|
|
||||||
readFile('backend/routes/dashboard/flows.js') +
|
|
||||||
readFile('backend/routes/dashboard/threats.js') +
|
|
||||||
readFile('backend/routes/dashboard/geo.js') +
|
|
||||||
readFile('backend/routes/dashboard/tls.js') +
|
|
||||||
readFile('backend/routes/dashboard/telemetry.js') +
|
|
||||||
readFile('backend/routes/dashboard/events.js') +
|
|
||||||
readFile('backend/routes/dashboard/agents.js');
|
|
||||||
const authCode = readFile('backend/routes/auth.js');
|
|
||||||
|
|
||||||
test('server.js tidak memanggil ingestionService', !serverCode.includes('ingestionService'));
|
|
||||||
test('server.js tidak import database.js (SQLite)', !serverCode.includes("require('./database')") && !serverCode.includes("require('../database')"));
|
|
||||||
test('server.js tidak memanggil startScheduler', !serverCode.includes('startScheduler'));
|
|
||||||
test('dashboard.js tidak import database.js', !dashCode.includes("require('../database')"));
|
|
||||||
test('dashboard.js tidak import scheduler.js lama', !dashCode.includes("require('../scheduler')"));
|
|
||||||
test('dashboard.js tidak memanggil netify.ai API', !dashCode.includes('netify.ai') && !dashCode.includes('fetchTopApps'));
|
|
||||||
test('auth.js tidak import database.js', !authCode.includes("require('../database')"));
|
|
||||||
test('auth.js tidak menggunakan better-sqlite3', !authCode.includes('better-sqlite3') && !authCode.includes('getDB()'));
|
|
||||||
console.log('');
|
|
||||||
|
|
||||||
// ─── TEST GROUP 2: Multi-tenant isolation ──────────────────────────────────
|
|
||||||
console.log('[GROUP 2] Dashboard — Multi-Tenant Agent UUID Isolation');
|
|
||||||
test('dashboard.js punya getBaseFilter() helper', dashCode.includes('function getBaseFilter'));
|
|
||||||
test('getBaseFilter menambahkan agent_uuid filter', dashCode.includes('filter.agent_uuid = req.user.agent_uuid'));
|
|
||||||
test('getBaseFilter menambahkan site_uuid filter', dashCode.includes('filter.site_uuid = req.user.site_uuid') || dashCode.includes('filter.site_uuid ='));
|
|
||||||
test('/apps pakai getBaseFilter', dashCode.includes("router.get('/apps'") && dashCode.includes('getBaseFilter'));
|
|
||||||
test('/devices pakai getBaseFilter', dashCode.includes("router.get('/devices'") && dashCode.includes('getBaseFilter'));
|
|
||||||
test('/flows pakai getBaseFilter', dashCode.includes("router.get('/flows'") && dashCode.includes('getBaseFilter'));
|
|
||||||
test('/threats pakai getBaseFilter', dashCode.includes("router.get('/threats'") && dashCode.includes('getBaseFilter'));
|
|
||||||
test('/summary pakai getBaseFilter', dashCode.includes("router.get('/summary'") && dashCode.includes('getBaseFilter'));
|
|
||||||
test('/agents endpoint hanya untuk SUPER_ADMIN', dashCode.includes("SUPER_ADMIN") && dashCode.includes("router.get('/agents'"));
|
|
||||||
console.log('');
|
|
||||||
|
|
||||||
// ─── TEST GROUP 3: Proxy 2 mode ────────────────────────────────────────────
|
|
||||||
console.log('[GROUP 3] Proxy — 2 Mode Pengambilan Data');
|
|
||||||
const schedulerCode = readFile('proxy/scheduler.js');
|
|
||||||
const collectorCode = readFile('proxy/collector.js');
|
|
||||||
|
|
||||||
test('scheduler.js membaca PROXY_COLLECT_MODE env', schedulerCode.includes('PROXY_COLLECT_MODE'));
|
|
||||||
test('scheduler.js membaca PROXY_AGENT_UUID env', schedulerCode.includes('PROXY_AGENT_UUID'));
|
|
||||||
test("scheduler.js mendukung mode 'all'", schedulerCode.includes("COLLECT_MODE === 'agent'"));
|
|
||||||
test('collector.js export collectAllAgents()', collectorCode.includes('collectAllAgents'));
|
|
||||||
test('collector.js export collectSpecificAgent()', collectorCode.includes('collectSpecificAgent'));
|
|
||||||
test('collector.js tag data dengan agent_uuid', collectorCode.includes('agent_uuid: agentUuid'));
|
|
||||||
test('collector.js tag data dengan site_uuid', collectorCode.includes('site_uuid: SITE_UUID') || collectorCode.includes('site_uuid: SITE_UUID') || collectorCode.includes('site_uuid: siteUuid'));
|
|
||||||
console.log('');
|
|
||||||
|
|
||||||
// ─── TEST GROUP 4: Docker Compose 2 Container Groups ─────────────────────
|
|
||||||
console.log('[GROUP 4] Docker Compose — 2 Container Groups + 2 Networks');
|
|
||||||
const composeCode = readFile('docker-compose.yml');
|
|
||||||
|
|
||||||
test('docker-compose.yml punya service: mongodb', composeCode.includes('mongodb:'));
|
|
||||||
test('docker-compose.yml punya service: proxy', composeCode.includes('proxy:'));
|
|
||||||
test('docker-compose.yml punya service: backend', composeCode.includes('backend:'));
|
|
||||||
test('docker-compose.yml punya service: frontend', composeCode.includes('frontend:'));
|
|
||||||
test('docker-compose.yml punya network: backone-infra', composeCode.includes('backone-infra'));
|
|
||||||
test('docker-compose.yml punya network: backone-app', composeCode.includes('backone-app'));
|
|
||||||
test('proxy connect ke backone-infra (Container 1)', composeCode.includes('backone-infra'));
|
|
||||||
test('backend connect ke backone-app (Container 2)', composeCode.includes('backone-app'));
|
|
||||||
test('proxy depends on mongodb', composeCode.includes('service_healthy'));
|
|
||||||
test('env PROXY_COLLECT_MODE di docker-compose', composeCode.includes('PROXY_COLLECT_MODE'));
|
|
||||||
test('env PROXY_AGENT_UUID di docker-compose', composeCode.includes('PROXY_AGENT_UUID'));
|
|
||||||
test('PROXY_URL dikirim ke backend', composeCode.includes('PROXY_URL=http://proxy:4000'));
|
|
||||||
console.log('');
|
|
||||||
|
|
||||||
// ─── TEST GROUP 5: MongoDB Schemas sinkron ────────────────────────────────
|
|
||||||
console.log('[GROUP 5] MongoDB Schemas — Proxy & Backend Sinkron');
|
|
||||||
const proxySchema = readFile('proxy/models/Schemas.js');
|
|
||||||
const backendSchema = readFile('backend/models/Schemas.js');
|
|
||||||
|
|
||||||
const sharedFields = ['agent_uuid', 'site_uuid', 'timestamp', 'bandwidth_down', 'bandwidth_up', 'ip_address', 'mac_address', 'flow_id', 'src_ip', 'dst_ip', 'threat_type', 'severity'];
|
|
||||||
for (const field of sharedFields) {
|
|
||||||
test(`Field '${field}' ada di kedua schema`, proxySchema.includes(field) && backendSchema.includes(field));
|
|
||||||
}
|
|
||||||
console.log('');
|
|
||||||
|
|
||||||
// ─── SUMMARY ──────────────────────────────────────────────────────────────
|
|
||||||
console.log('══════════════════════════════════════════════════════');
|
|
||||||
console.log(` Results: ${passed} PASSED | ${failed} FAILED`);
|
|
||||||
console.log('══════════════════════════════════════════════════════\n');
|
|
||||||
|
|
||||||
if (failed > 0) process.exit(1);
|
|
||||||
@@ -1,98 +0,0 @@
|
|||||||
// test/branding_unit_test.js
|
|
||||||
const fs = require('fs');
|
|
||||||
const path = require('path');
|
|
||||||
const vm = require('vm');
|
|
||||||
|
|
||||||
console.log('=========================================');
|
|
||||||
console.log(' RUNNING TDD UNIT TEST FOR BRANDING LOGIC');
|
|
||||||
console.log('=========================================\n');
|
|
||||||
|
|
||||||
// 1. Read and transpile src/lib/branding.ts slightly to make it run in Node.js
|
|
||||||
const brandingCodePath = path.join(__dirname, '../src/lib/branding.ts');
|
|
||||||
let code = fs.readFileSync(brandingCodePath, 'utf8');
|
|
||||||
|
|
||||||
// Strip TypeScript interfaces, return types, parameter types, and export keywords
|
|
||||||
code = code.replace(/export\s+interface\s+\w+\s*\{[^}]*\}/g, '');
|
|
||||||
code = code.replace(/:\s*Branding\b/g, '');
|
|
||||||
code = code.replace(/selectedSiteUuid\?\s*:\s*string/g, 'selectedSiteUuid');
|
|
||||||
code = code.replace(/export\s+/g, '');
|
|
||||||
|
|
||||||
let testCount = 0;
|
|
||||||
let passedCount = 0;
|
|
||||||
|
|
||||||
function assert(label, condition) {
|
|
||||||
testCount++;
|
|
||||||
if (condition) {
|
|
||||||
console.log(` ✓ PASSED: ${label}`);
|
|
||||||
passedCount++;
|
|
||||||
} else {
|
|
||||||
console.error(` ✗ FAILED: ${label}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper to run getSiteBranding with mocked environment
|
|
||||||
function runGetSiteBranding({ selectedSiteUuid, hostname, search, localStorageVal }) {
|
|
||||||
const sandbox = {
|
|
||||||
window: {
|
|
||||||
location: {
|
|
||||||
hostname: hostname || 'localhost',
|
|
||||||
search: search || ''
|
|
||||||
}
|
|
||||||
},
|
|
||||||
localStorage: {
|
|
||||||
getItem: () => localStorageVal || null
|
|
||||||
},
|
|
||||||
console
|
|
||||||
};
|
|
||||||
vm.createContext(sandbox);
|
|
||||||
vm.runInContext(code, sandbox);
|
|
||||||
return sandbox.getSiteBranding(selectedSiteUuid);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Test Case 1: Default branding (no local storage, localhost)
|
|
||||||
const brand1 = runGetSiteBranding({
|
|
||||||
hostname: 'localhost'
|
|
||||||
});
|
|
||||||
assert('Default branding should be BackOne', brand1.name === 'BackOne' && brand1.logo === '/backone-logo.png');
|
|
||||||
|
|
||||||
// Test Case 2: SIAB site UUID passed explicitly on demoplace.my.id
|
|
||||||
const brand2 = runGetSiteBranding({
|
|
||||||
selectedSiteUuid: '6681452d_9cae_4ff4_8ae8_0d504774265e',
|
|
||||||
hostname: 'demoplace.my.id'
|
|
||||||
});
|
|
||||||
assert('SIAB site UUID on demoplace.my.id should return BackOne branding', brand2.name === 'BackOne' && brand2.logo === '/backone-logo.png');
|
|
||||||
|
|
||||||
// Test Case 3: SIAB site UUID in localStorage on demoplace.my.id
|
|
||||||
const brand3 = runGetSiteBranding({
|
|
||||||
localStorageVal: '6681452d_9cae_4ff4_8ae8_0d504774265e',
|
|
||||||
hostname: 'demoplace.my.id'
|
|
||||||
});
|
|
||||||
assert('SIAB site UUID in localStorage on demoplace.my.id should return BackOne branding', brand3.name === 'BackOne' && brand3.logo === '/backone-logo.png');
|
|
||||||
|
|
||||||
// Test Case 4: Nexus site UUID passed explicitly
|
|
||||||
const brand4 = runGetSiteBranding({
|
|
||||||
selectedSiteUuid: 'd7902405_0dc2_458b_8584_ed4d24b64f24',
|
|
||||||
hostname: 'localhost'
|
|
||||||
});
|
|
||||||
assert('Nexus site UUID should return Nexus branding', brand4.name === 'Nexus' && brand4.logo === '/nexus-logo.png');
|
|
||||||
|
|
||||||
// Test Case 5: Default fallback on demoplace.my.id when no site is selected
|
|
||||||
const brand5 = runGetSiteBranding({
|
|
||||||
hostname: 'demoplace.my.id'
|
|
||||||
});
|
|
||||||
assert('Fallback on demoplace.my.id should be BackOne branding', brand5.name === 'BackOne' && brand5.logo === '/backone-logo.png');
|
|
||||||
|
|
||||||
console.log(`\n=========================================`);
|
|
||||||
console.log(` RESULT: ${passedCount} / ${testCount} tests passed.`);
|
|
||||||
console.log(`=========================================\n`);
|
|
||||||
|
|
||||||
if (passedCount !== testCount) {
|
|
||||||
process.exit(1);
|
|
||||||
} else {
|
|
||||||
process.exit(0);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Test execution failed with error:', err);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
@@ -1,104 +0,0 @@
|
|||||||
// test/field_alignment.js
|
|
||||||
// Check field name alignment between backend and frontend
|
|
||||||
const fs = require('fs');
|
|
||||||
|
|
||||||
const dash = fs.readFileSync('backend/routes/dashboard.js','utf8') +
|
|
||||||
fs.readFileSync('backend/routes/dashboard/helpers.js','utf8') +
|
|
||||||
fs.readFileSync('backend/routes/dashboard/summary.js','utf8') +
|
|
||||||
fs.readFileSync('backend/routes/dashboard/apps.js','utf8') +
|
|
||||||
fs.readFileSync('backend/routes/dashboard/devices.js','utf8') +
|
|
||||||
fs.readFileSync('backend/routes/dashboard/flows.js','utf8') +
|
|
||||||
fs.readFileSync('backend/routes/dashboard/threats.js','utf8') +
|
|
||||||
fs.readFileSync('backend/routes/dashboard/geo.js','utf8') +
|
|
||||||
fs.readFileSync('backend/routes/dashboard/tls.js','utf8') +
|
|
||||||
fs.readFileSync('backend/routes/dashboard/telemetry.js','utf8') +
|
|
||||||
fs.readFileSync('backend/routes/dashboard/events.js','utf8');
|
|
||||||
const api = fs.readFileSync('src/lib/api.ts','utf8') +
|
|
||||||
fs.readFileSync('src/lib/api-types.ts','utf8');
|
|
||||||
|
|
||||||
console.log('=== Field Name Alignment: Backend ↔ Frontend ===\n');
|
|
||||||
|
|
||||||
function check(label, backendHas, frontendExpects, mismatchDesc = '') {
|
|
||||||
if (backendHas && frontendExpects) {
|
|
||||||
console.log(` ✓ PASS: ${label} is aligned`);
|
|
||||||
} else if (backendHas && !frontendExpects) {
|
|
||||||
console.log(` ⚠ WARNING: Backend defines ${label} but Frontend doesn't expect it`);
|
|
||||||
} else if (!backendHas && frontendExpects) {
|
|
||||||
console.log(` ✗ FAIL: ${label} is missing in backend routing!`);
|
|
||||||
if (mismatchDesc) console.log(` → ${mismatchDesc}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// /protocols
|
|
||||||
console.log('[/protocols]');
|
|
||||||
check(
|
|
||||||
'protocol_label',
|
|
||||||
dash.includes('protocol_label'),
|
|
||||||
api.includes('protocol_label'),
|
|
||||||
'Backend doesn\'t return protocol_label to frontend'
|
|
||||||
);
|
|
||||||
console.log('');
|
|
||||||
|
|
||||||
// /timeline
|
|
||||||
console.log('[/timeline]');
|
|
||||||
check(
|
|
||||||
'fetched_at',
|
|
||||||
dash.includes('fetched_at'),
|
|
||||||
api.includes('fetched_at'),
|
|
||||||
'Backend sends timestamp, frontend reads fetched_at'
|
|
||||||
);
|
|
||||||
console.log('');
|
|
||||||
|
|
||||||
// /countries
|
|
||||||
console.log('[/countries]');
|
|
||||||
check(
|
|
||||||
'country_name',
|
|
||||||
dash.includes('country_name'),
|
|
||||||
api.includes('country_name'),
|
|
||||||
'Backend does not return country_name'
|
|
||||||
);
|
|
||||||
check(
|
|
||||||
'country_code',
|
|
||||||
dash.includes('country_code'),
|
|
||||||
api.includes('country_code'),
|
|
||||||
'Backend does not return country_code'
|
|
||||||
);
|
|
||||||
console.log('');
|
|
||||||
|
|
||||||
// /dns
|
|
||||||
console.log('[/dns]');
|
|
||||||
check(
|
|
||||||
'domain',
|
|
||||||
dash.includes('domain'),
|
|
||||||
api.includes('domain'),
|
|
||||||
'Backend does not map domain field'
|
|
||||||
);
|
|
||||||
console.log('');
|
|
||||||
|
|
||||||
// /summary
|
|
||||||
console.log('[/summary]');
|
|
||||||
check('total_devices', dash.includes('total_devices'), api.includes('total_devices'));
|
|
||||||
check('bandwidth_down', dash.includes('bandwidth_down'), api.includes('bandwidth_down'));
|
|
||||||
check('download_speed', dash.includes('download_speed'), api.includes('download_speed'));
|
|
||||||
console.log('');
|
|
||||||
|
|
||||||
// /apps
|
|
||||||
console.log('[/apps]');
|
|
||||||
check('app_label', dash.includes('app_label'), api.includes('app_label'));
|
|
||||||
console.log('');
|
|
||||||
|
|
||||||
// /flows
|
|
||||||
console.log('[/flows]');
|
|
||||||
check('src_ip', dash.includes('src_ip'), api.includes('src_ip'));
|
|
||||||
check('dst_port', dash.includes('dst_port'), api.includes('dst_port'));
|
|
||||||
console.log('');
|
|
||||||
|
|
||||||
// app-categories
|
|
||||||
console.log('[/app-categories]');
|
|
||||||
check(
|
|
||||||
'category_label',
|
|
||||||
dash.includes('category_label'),
|
|
||||||
api.includes('category_label'),
|
|
||||||
'Backend sends label, frontend expects category_label'
|
|
||||||
);
|
|
||||||
console.log('');
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
// test/middleware_verification_test.js
|
|
||||||
const fs = require('fs');
|
|
||||||
const path = require('path');
|
|
||||||
const assert = require('assert');
|
|
||||||
|
|
||||||
const ROOT = path.join(__dirname, '..');
|
|
||||||
|
|
||||||
console.log('=== Running TDD Middleware Verification Test ===');
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 1. Verify src/middleware.ts exists
|
|
||||||
const middlewarePath = path.join(ROOT, 'src', 'middleware.ts');
|
|
||||||
const exists = fs.existsSync(middlewarePath);
|
|
||||||
assert.ok(exists, 'src/middleware.ts must exist');
|
|
||||||
console.log('✓ src/middleware.ts exists');
|
|
||||||
|
|
||||||
// 2. Verify it exports 'middleware' function
|
|
||||||
const content = fs.readFileSync(middlewarePath, 'utf8');
|
|
||||||
assert.ok(content.includes('export function middleware'), 'src/middleware.ts must export function middleware');
|
|
||||||
assert.ok(!content.includes('export function proxy'), 'src/middleware.ts must NOT export function proxy');
|
|
||||||
console.log('✓ src/middleware.ts exports function middleware correctly');
|
|
||||||
|
|
||||||
console.log('=== ALL TESTS PASSED ===');
|
|
||||||
process.exit(0);
|
|
||||||
} catch (err) {
|
|
||||||
console.error('✗ TEST FAILED:', err.message);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
// test/multipart_proxy_test.js
|
|
||||||
const fs = require('fs');
|
|
||||||
const path = require('path');
|
|
||||||
const assert = require('assert');
|
|
||||||
|
|
||||||
const ROOT = path.join(__dirname, '..');
|
|
||||||
|
|
||||||
console.log('=========================================');
|
|
||||||
console.log(' RUNNING TDD TESTS FOR MULTIPART PROXIES');
|
|
||||||
console.log('=========================================\n');
|
|
||||||
|
|
||||||
let testCount = 0;
|
|
||||||
let passedCount = 0;
|
|
||||||
|
|
||||||
function runAssert(label, condition) {
|
|
||||||
testCount++;
|
|
||||||
if (condition) {
|
|
||||||
console.log(` ✓ PASSED: ${label}`);
|
|
||||||
passedCount++;
|
|
||||||
} else {
|
|
||||||
console.error(` ✗ FAILED: ${label}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Test Case 1: Cek keberadaan file src/app/api/auth/admin/create-external-user/route.ts
|
|
||||||
const createRoutePath = path.join(ROOT, 'src', 'app', 'api', 'auth', 'admin', 'create-external-user', 'route.ts');
|
|
||||||
const createExists = fs.existsSync(createRoutePath);
|
|
||||||
runAssert('create-external-user/route.ts should exist', createExists);
|
|
||||||
|
|
||||||
if (createExists) {
|
|
||||||
const content = fs.readFileSync(createRoutePath, 'utf8');
|
|
||||||
runAssert('create-external-user/route.ts should export POST handler', content.includes('export async function POST'));
|
|
||||||
runAssert('create-external-user/route.ts should read req.json(), req.body or req.formData()', content.includes('req.json()') || content.includes('req.body') || content.includes('req.formData()'));
|
|
||||||
runAssert('create-external-user/route.ts should forward auth cookie', content.includes('Cookie') || content.includes('cookie'));
|
|
||||||
runAssert('create-external-user/route.ts should use NEXT_PUBLIC_API_URL or port 3011', content.includes('NEXT_PUBLIC_API_URL') || content.includes('3011'));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test Case 2: Cek keberadaan file src/app/api/auth/admin/update-agent-user/route.ts
|
|
||||||
const updateRoutePath = path.join(ROOT, 'src', 'app', 'api', 'auth', 'admin', 'update-agent-user', 'route.ts');
|
|
||||||
const updateExists = fs.existsSync(updateRoutePath);
|
|
||||||
runAssert('update-agent-user/route.ts should exist', updateExists);
|
|
||||||
|
|
||||||
if (updateExists) {
|
|
||||||
const content = fs.readFileSync(updateRoutePath, 'utf8');
|
|
||||||
runAssert('update-agent-user/route.ts should export POST handler', content.includes('export async function POST'));
|
|
||||||
runAssert('update-agent-user/route.ts should read req.json(), req.body or req.formData()', content.includes('req.json()') || content.includes('req.body') || content.includes('req.formData()'));
|
|
||||||
runAssert('update-agent-user/route.ts should forward auth cookie', content.includes('Cookie') || content.includes('cookie'));
|
|
||||||
runAssert('update-agent-user/route.ts should use NEXT_PUBLIC_API_URL or port 3011', content.includes('NEXT_PUBLIC_API_URL') || content.includes('3011'));
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`\n=========================================`);
|
|
||||||
console.log(` RESULT: ${passedCount} / ${testCount} tests passed.`);
|
|
||||||
console.log(`=========================================\n`);
|
|
||||||
|
|
||||||
if (passedCount !== testCount) {
|
|
||||||
process.exit(1);
|
|
||||||
} else {
|
|
||||||
process.exit(0);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Test execution failed with error:', err);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
@@ -1,149 +0,0 @@
|
|||||||
// test/multitenant_branding_test.js
|
|
||||||
const mongoose = require('../backend/node_modules/mongoose');
|
|
||||||
const path = require('path');
|
|
||||||
|
|
||||||
// Load environment variables
|
|
||||||
require('dotenv').config({ path: path.join(__dirname, '../.env.local') });
|
|
||||||
|
|
||||||
const MONGODB_URI = process.env.MONGODB_URI || 'mongodb://127.0.0.1:27017/backone_dpi';
|
|
||||||
|
|
||||||
const User = require('../backend/models/User');
|
|
||||||
const { TenantConfig } = require('../backend/models/Schemas');
|
|
||||||
|
|
||||||
let testCount = 0;
|
|
||||||
let passedCount = 0;
|
|
||||||
|
|
||||||
function assert(label, condition) {
|
|
||||||
testCount++;
|
|
||||||
if (condition) {
|
|
||||||
console.log(` ✓ PASSED: ${label}`);
|
|
||||||
passedCount++;
|
|
||||||
} else {
|
|
||||||
console.error(` ✗ FAILED: ${label}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function runTests() {
|
|
||||||
console.log('\n=========================================');
|
|
||||||
console.log(' RUNNING TDD TESTS FOR MULTI-TENANCY & BRANDING');
|
|
||||||
console.log('=========================================\n');
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 1. Connect to DB
|
|
||||||
await mongoose.connect(MONGODB_URI);
|
|
||||||
console.log('✓ Connected to MongoDB');
|
|
||||||
|
|
||||||
// Ensure at least one summary document exists for both SIAB and Nexus
|
|
||||||
const dbConnection = mongoose.connection.db;
|
|
||||||
const hasSiabSummary = await dbConnection.collection('summaries').findOne({ site_uuid: '6681452d_9cae_4ff4_8ae8_0d504774265e' });
|
|
||||||
if (!hasSiabSummary) {
|
|
||||||
await dbConnection.collection('summaries').insertOne({
|
|
||||||
timestamp: new Date(),
|
|
||||||
agent_uuid: 'mock_siab_agent_uuid',
|
|
||||||
site_uuid: '6681452d_9cae_4ff4_8ae8_0d504774265e',
|
|
||||||
bandwidth_down: 100,
|
|
||||||
bandwidth_up: 50,
|
|
||||||
active_flows: 10
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const hasNexusSummary = await dbConnection.collection('summaries').findOne({ site_uuid: 'd7902405_0dc2_458b_8584_ed4d24b64f24' });
|
|
||||||
if (!hasNexusSummary) {
|
|
||||||
await dbConnection.collection('summaries').insertOne({
|
|
||||||
timestamp: new Date(),
|
|
||||||
agent_uuid: 'mock_nexus_agent_uuid',
|
|
||||||
site_uuid: 'd7902405_0dc2_458b_8584_ed4d24b64f24',
|
|
||||||
bandwidth_down: 100,
|
|
||||||
bandwidth_up: 50,
|
|
||||||
active_flows: 10
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Validate User Accounts Seeding
|
|
||||||
const adminUser = await User.findOne({ username: 'admin' });
|
|
||||||
assert('User "admin" should exist', !!adminUser);
|
|
||||||
if (adminUser) {
|
|
||||||
assert('User "admin" should have SUPER_ADMIN role', adminUser.role === 'SUPER_ADMIN');
|
|
||||||
}
|
|
||||||
|
|
||||||
const siabUser = await User.findOne({ username: 'siab' });
|
|
||||||
assert('User "siab" should exist', !!siabUser);
|
|
||||||
if (siabUser) {
|
|
||||||
assert('User "siab" should have TENANT_ADMIN role', siabUser.role === 'TENANT_ADMIN');
|
|
||||||
assert('User "siab" should belong to SIAB site_uuid', siabUser.site_uuid === '6681452d_9cae_4ff4_8ae8_0d504774265e');
|
|
||||||
}
|
|
||||||
|
|
||||||
const nexusUser = await User.findOne({ username: 'nexus' });
|
|
||||||
assert('User "nexus" should exist', !!nexusUser);
|
|
||||||
if (nexusUser) {
|
|
||||||
assert('User "nexus" should have TENANT_ADMIN role', nexusUser.role === 'TENANT_ADMIN');
|
|
||||||
assert('User "nexus" should belong to Nexus site_uuid', nexusUser.site_uuid === 'd7902405_0dc2_458b_8584_ed4d24b64f24');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Validate TenantConfig configurations
|
|
||||||
const backoneConfig = await TenantConfig.findOne({ site_uuid: 'default' });
|
|
||||||
assert('Default branding config should exist', !!backoneConfig);
|
|
||||||
if (backoneConfig) {
|
|
||||||
assert('Default brand_name should be BackOne', backoneConfig.brand_name === 'BackOne');
|
|
||||||
assert('Default brand_logo should be /backone-logo.png', backoneConfig.brand_logo === '/backone-logo.png');
|
|
||||||
}
|
|
||||||
|
|
||||||
const siabConfig = await TenantConfig.findOne({ site_uuid: '6681452d_9cae_4ff4_8ae8_0d504774265e' });
|
|
||||||
assert('SIAB branding config should exist', !!siabConfig);
|
|
||||||
if (siabConfig) {
|
|
||||||
assert('SIAB brand_name should be SIAB', siabConfig.brand_name === 'SIAB');
|
|
||||||
assert('SIAB brand_logo should be /siab-logo.png', siabConfig.brand_logo === '/siab-logo.png');
|
|
||||||
}
|
|
||||||
|
|
||||||
const nexusConfig = await TenantConfig.findOne({ site_uuid: 'd7902405_0dc2_458b_8584_ed4d24b64f24' });
|
|
||||||
assert('Nexus branding config should exist', !!nexusConfig);
|
|
||||||
if (nexusConfig) {
|
|
||||||
assert('Nexus brand_name should be Nexus', nexusConfig.brand_name === 'Nexus');
|
|
||||||
assert('Nexus brand_logo should be /nexus-logo.png', nexusConfig.brand_logo === '/nexus-logo.png');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. Validate Agent listing isolation per site_uuid (summaries collection)
|
|
||||||
const db = mongoose.connection.db;
|
|
||||||
const siabAgents = await db.collection('summaries').distinct('agent_uuid', { site_uuid: '6681452d_9cae_4ff4_8ae8_0d504774265e' });
|
|
||||||
const nexusAgents = await db.collection('summaries').distinct('agent_uuid', { site_uuid: 'd7902405_0dc2_458b_8584_ed4d24b64f24' });
|
|
||||||
|
|
||||||
assert('SIAB agents count should be greater than 0', siabAgents.length > 0);
|
|
||||||
assert('Nexus agents count should be greater than 0', nexusAgents.length > 0);
|
|
||||||
|
|
||||||
// Check that agents do not overlap
|
|
||||||
const overlap = siabAgents.filter(uuid => nexusAgents.includes(uuid));
|
|
||||||
assert('SIAB and Nexus agent lists should have 0 overlapping agents', overlap.length === 0);
|
|
||||||
|
|
||||||
// 5. Validate User creation isolation (created_by query)
|
|
||||||
const testAdminCreated = await User.find({ created_by: 'admin' });
|
|
||||||
const testNexusCreated = await User.find({ created_by: 'nexus' });
|
|
||||||
assert('Superadmin users should return a queryable list of registered accounts', Array.isArray(testAdminCreated));
|
|
||||||
assert('Nexus admin users should return a queryable list of registered accounts', Array.isArray(testNexusCreated));
|
|
||||||
|
|
||||||
// Verify tenant-admin custom query filter returns AGENT_VIEWER accounts for their site
|
|
||||||
const mockTenantUser = { role: 'TENANT_ADMIN', site_uuid: 'd7902405_0dc2_458b_8584_ed4d24b64f24', username: 'nexus' };
|
|
||||||
const query = {
|
|
||||||
$or: [
|
|
||||||
{ role: 'AGENT_VIEWER', site_uuid: mockTenantUser.site_uuid },
|
|
||||||
{ created_by: mockTenantUser.username }
|
|
||||||
]
|
|
||||||
};
|
|
||||||
const queryResults = await User.find(query);
|
|
||||||
assert('Tenant user list query should execute successfully', Array.isArray(queryResults));
|
|
||||||
|
|
||||||
console.log(`\n=========================================`);
|
|
||||||
console.log(` RESULT: ${passedCount} / ${testCount} tests passed.`);
|
|
||||||
console.log(`=========================================\n`);
|
|
||||||
|
|
||||||
if (passedCount !== testCount) {
|
|
||||||
process.exit(1);
|
|
||||||
} else {
|
|
||||||
process.exit(0);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Test execution failed with error:', err);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
runTests();
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
// test/sftp_manifest_test.js
|
|
||||||
const fs = require('fs');
|
|
||||||
const path = require('path');
|
|
||||||
const assert = require('assert');
|
|
||||||
|
|
||||||
const ROOT = path.join(__dirname, '..');
|
|
||||||
|
|
||||||
console.log('=== Running TDD SFTP Manifest Verification Test ===');
|
|
||||||
|
|
||||||
try {
|
|
||||||
const deployScriptPath = path.join(ROOT, 'scripts', 'deploy-sftp.js');
|
|
||||||
const exists = fs.existsSync(deployScriptPath);
|
|
||||||
assert.ok(exists, 'scripts/deploy-sftp.js must exist');
|
|
||||||
|
|
||||||
const content = fs.readFileSync(deployScriptPath, 'utf8');
|
|
||||||
|
|
||||||
// Verify .next/static is in the upload manifest
|
|
||||||
assert.ok(
|
|
||||||
content.includes("local: '.next/static'") || content.includes('local: ".next/static"'),
|
|
||||||
'scripts/deploy-sftp.js must include local: \'.next/static\' in UPLOAD_MANIFEST'
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log('✓ scripts/deploy-sftp.js includes .next/static in UPLOAD_MANIFEST');
|
|
||||||
console.log('=== ALL TESTS PASSED ===');
|
|
||||||
process.exit(0);
|
|
||||||
} catch (err) {
|
|
||||||
console.error('✗ TEST FAILED:', err.message);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
@@ -1,118 +0,0 @@
|
|||||||
// test/sidebar_styling_test.js
|
|
||||||
const fs = require('fs');
|
|
||||||
const path = require('path');
|
|
||||||
|
|
||||||
const sidebarPath = path.join(__dirname, '../src/components/layout/Sidebar.tsx');
|
|
||||||
const siteSelectorPath = path.join(__dirname, '../src/components/layout/SidebarSiteSelector.tsx');
|
|
||||||
const timeSelectorPath = path.join(__dirname, '../src/components/layout/SidebarTimeSelector.tsx');
|
|
||||||
const globalsCssPath = path.join(__dirname, '../src/app/globals.css');
|
|
||||||
const agentsPagePath = path.join(__dirname, '../src/app/(dashboard)/agents/page.tsx');
|
|
||||||
|
|
||||||
let testCount = 0;
|
|
||||||
let passedCount = 0;
|
|
||||||
|
|
||||||
function assert(label, condition) {
|
|
||||||
testCount++;
|
|
||||||
if (condition) {
|
|
||||||
console.log(` ✓ PASSED: ${label}`);
|
|
||||||
passedCount++;
|
|
||||||
} else {
|
|
||||||
console.error(` ✗ FAILED: ${label}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function runTests() {
|
|
||||||
console.log('\n=========================================');
|
|
||||||
console.log(' RUNNING TDD TESTS FOR SIDEBAR & PAGE STYLING');
|
|
||||||
console.log('=========================================\n');
|
|
||||||
|
|
||||||
try {
|
|
||||||
const sidebarContent = fs.readFileSync(sidebarPath, 'utf8');
|
|
||||||
assert(
|
|
||||||
'Sidebar.tsx should use Times New Roman font family stack',
|
|
||||||
sidebarContent.includes("'Times New Roman', Times, serif")
|
|
||||||
);
|
|
||||||
assert(
|
|
||||||
'Sidebar.tsx link items should be compact (text-xs or text-[13px])',
|
|
||||||
sidebarContent.includes('text-xs') || sidebarContent.includes('text-[13px]')
|
|
||||||
);
|
|
||||||
} catch (err) {
|
|
||||||
assert(`Sidebar.tsx read error: ${err.message}`, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const siteSelectorContent = fs.readFileSync(siteSelectorPath, 'utf8');
|
|
||||||
assert(
|
|
||||||
'SidebarSiteSelector.tsx should use Times New Roman font family stack',
|
|
||||||
siteSelectorContent.includes("'Times New Roman', Times, serif")
|
|
||||||
);
|
|
||||||
assert(
|
|
||||||
'SidebarSiteSelector.tsx label should use correct size (text-[9.5px] to text-[10.5px])',
|
|
||||||
siteSelectorContent.includes('text-[10px]') || siteSelectorContent.includes('text-[10.5px]') || siteSelectorContent.includes('text-[9.5px]')
|
|
||||||
);
|
|
||||||
assert(
|
|
||||||
'SidebarSiteSelector.tsx value should use correct size (text-xs or text-sm)',
|
|
||||||
siteSelectorContent.includes('text-xs') || siteSelectorContent.includes('text-sm') || siteSelectorContent.includes('text-[13.5px]')
|
|
||||||
);
|
|
||||||
} catch (err) {
|
|
||||||
assert(`SidebarSiteSelector.tsx read error: ${err.message}`, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const timeSelectorContent = fs.readFileSync(timeSelectorPath, 'utf8');
|
|
||||||
assert(
|
|
||||||
'SidebarTimeSelector.tsx should use Times New Roman font family stack',
|
|
||||||
timeSelectorContent.includes("'Times New Roman', Times, serif")
|
|
||||||
);
|
|
||||||
assert(
|
|
||||||
'SidebarTimeSelector.tsx label should use correct size (text-[9.5px] to text-[10.5px])',
|
|
||||||
timeSelectorContent.includes('text-[10px]') || timeSelectorContent.includes('text-[10.5px]') || timeSelectorContent.includes('text-[9.5px]')
|
|
||||||
);
|
|
||||||
assert(
|
|
||||||
'SidebarTimeSelector.tsx value should use correct size (text-xs or text-sm)',
|
|
||||||
timeSelectorContent.includes('text-xs') || timeSelectorContent.includes('text-sm') || timeSelectorContent.includes('text-[13.5px]')
|
|
||||||
);
|
|
||||||
} catch (err) {
|
|
||||||
assert(`SidebarTimeSelector.tsx read error: ${err.message}`, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const globalsCssContent = fs.readFileSync(globalsCssPath, 'utf8');
|
|
||||||
assert(
|
|
||||||
'globals.css body rule should enforce Times New Roman font-family',
|
|
||||||
globalsCssContent.includes("font-family: 'Times New Roman', Times, serif;")
|
|
||||||
);
|
|
||||||
} catch (err) {
|
|
||||||
assert(`globals.css read error: ${err.message}`, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const agentsPageContent = fs.readFileSync(agentsPagePath, 'utf8');
|
|
||||||
assert(
|
|
||||||
'agents/page.tsx should display "Agents Inventory" title',
|
|
||||||
agentsPageContent.includes('Agents Inventory')
|
|
||||||
);
|
|
||||||
assert(
|
|
||||||
'agents/page.tsx should display subtitle description',
|
|
||||||
agentsPageContent.includes('Managed DPI probes, remote collectors, and probe infrastructure.')
|
|
||||||
);
|
|
||||||
assert(
|
|
||||||
'agents/page.tsx header should not render the Server icon',
|
|
||||||
!agentsPageContent.includes('<Server className=') && !agentsPageContent.includes('<Server ')
|
|
||||||
);
|
|
||||||
} catch (err) {
|
|
||||||
assert(`agents/page.tsx read error: ${err.message}`, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('\n=========================================');
|
|
||||||
console.log(` RESULT: ${passedCount}/${testCount} tests passed`);
|
|
||||||
console.log('=========================================\n');
|
|
||||||
|
|
||||||
if (passedCount !== testCount) {
|
|
||||||
process.exit(1);
|
|
||||||
} else {
|
|
||||||
process.exit(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
runTests();
|
|
||||||
@@ -1,197 +0,0 @@
|
|||||||
// test/sync_test.js
|
|
||||||
// TDD Integration Test: Verifikasi sinkronisasi 100% file antara Localhost dan Domain
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
const { Client } = require('ssh2');
|
|
||||||
const fs = require('fs');
|
|
||||||
const path = require('path');
|
|
||||||
const crypto = require('crypto');
|
|
||||||
|
|
||||||
// Load environment variables if available
|
|
||||||
try {
|
|
||||||
require('dotenv').config({ path: path.join(__dirname, '../.env.local') });
|
|
||||||
} catch (e) {}
|
|
||||||
|
|
||||||
const SSH = {
|
|
||||||
host: '103.185.47.52',
|
|
||||||
port: 2222,
|
|
||||||
username: 'adminbackend',
|
|
||||||
password: 'htEo7x6LsBQiEHHH',
|
|
||||||
readyTimeout: 60000
|
|
||||||
};
|
|
||||||
|
|
||||||
const REMOTE_ROOT = '/home/adminbackend/web/dev.demoplace.my.id/public_html';
|
|
||||||
const LOCAL_ROOT = path.join(__dirname, '..');
|
|
||||||
|
|
||||||
let testCount = 0;
|
|
||||||
let passedCount = 0;
|
|
||||||
|
|
||||||
function assert(label, condition) {
|
|
||||||
testCount++;
|
|
||||||
if (condition) {
|
|
||||||
console.log(` ✓ PASSED: ${label}`);
|
|
||||||
passedCount++;
|
|
||||||
} else {
|
|
||||||
console.error(` ✗ FAILED: ${label}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getMD5(filePath) {
|
|
||||||
try {
|
|
||||||
const data = fs.readFileSync(filePath);
|
|
||||||
return crypto.createHash('md5').update(data).digest('hex');
|
|
||||||
} catch (e) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getFilesLocal(dir, files = []) {
|
|
||||||
const list = fs.readdirSync(dir);
|
|
||||||
for (const item of list) {
|
|
||||||
const fullPath = path.join(dir, item);
|
|
||||||
const stat = fs.statSync(fullPath);
|
|
||||||
if (stat.isDirectory()) {
|
|
||||||
if (item !== 'node_modules' && item !== '.next' && item !== '.git') {
|
|
||||||
getFilesLocal(fullPath, files);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
files.push(path.relative(LOCAL_ROOT, fullPath).replace(/\\/g, '/'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return files;
|
|
||||||
}
|
|
||||||
|
|
||||||
const remoteScript = `
|
|
||||||
const fs = require('fs');
|
|
||||||
const path = require('path');
|
|
||||||
const crypto = require('crypto');
|
|
||||||
|
|
||||||
const root = '/home/adminbackend/web/dev.demoplace.my.id/public_html';
|
|
||||||
|
|
||||||
function getMD5(filePath) {
|
|
||||||
try {
|
|
||||||
const data = fs.readFileSync(filePath);
|
|
||||||
return crypto.createHash('md5').update(data).digest('hex');
|
|
||||||
} catch (e) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getFilesRemote(dir, files = {}) {
|
|
||||||
try {
|
|
||||||
const list = fs.readdirSync(dir);
|
|
||||||
for (const item of list) {
|
|
||||||
const fullPath = path.join(dir, item);
|
|
||||||
const stat = fs.statSync(fullPath);
|
|
||||||
if (stat.isDirectory()) {
|
|
||||||
if (item !== 'node_modules' && item !== '.next' && item !== '.git') {
|
|
||||||
getFilesRemote(fullPath, files);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const rel = path.relative(root, fullPath);
|
|
||||||
files[rel] = { md5: getMD5(fullPath), size: stat.size };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch(e) {}
|
|
||||||
return files;
|
|
||||||
}
|
|
||||||
|
|
||||||
const filesMap = {};
|
|
||||||
getFilesRemote(path.join(root, 'src'), filesMap);
|
|
||||||
getFilesRemote(path.join(root, 'public'), filesMap);
|
|
||||||
getFilesRemote(path.join(root, 'backend'), filesMap);
|
|
||||||
getFilesRemote(path.join(root, 'proxy'), filesMap);
|
|
||||||
|
|
||||||
const rootConfigs = ['package.json', 'tsconfig.json', 'next.config.ts', '.env.production'];
|
|
||||||
for (const f of rootConfigs) {
|
|
||||||
const full = path.join(root, f);
|
|
||||||
if (fs.existsSync(full)) {
|
|
||||||
const stat = fs.statSync(full);
|
|
||||||
filesMap[f] = { md5: getMD5(full), size: stat.size };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(JSON.stringify(filesMap));
|
|
||||||
`;
|
|
||||||
|
|
||||||
async function runTests() {
|
|
||||||
console.log('\n=========================================');
|
|
||||||
console.log(' RUNNING TDD SYNC COMPARISON TESTS');
|
|
||||||
console.log('=========================================\n');
|
|
||||||
|
|
||||||
let remoteFiles = {};
|
|
||||||
try {
|
|
||||||
const conn = new Client();
|
|
||||||
const remoteFilesStr = await new Promise((resolve, reject) => {
|
|
||||||
conn.on('ready', () => {
|
|
||||||
const b64 = Buffer.from(remoteScript).toString('base64');
|
|
||||||
conn.exec(`echo "${b64}" | base64 -d | node`, (err, stream) => {
|
|
||||||
if (err) return reject(err);
|
|
||||||
let out = '';
|
|
||||||
let errOut = '';
|
|
||||||
stream.on('data', d => out += d);
|
|
||||||
stream.stderr.on('data', d => errOut += d);
|
|
||||||
stream.on('close', () => {
|
|
||||||
conn.end();
|
|
||||||
if (errOut.trim()) {
|
|
||||||
reject(new Error(errOut));
|
|
||||||
} else {
|
|
||||||
resolve(out);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}).on('error', reject).connect(SSH);
|
|
||||||
});
|
|
||||||
|
|
||||||
remoteFiles = JSON.parse(remoteFilesStr);
|
|
||||||
console.log('✓ Connected to remote server and retrieved remote file list.');
|
|
||||||
} catch (err) {
|
|
||||||
console.error('✗ Failed to connect to remote server:', err.message);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get local files
|
|
||||||
const localFiles = {};
|
|
||||||
const pathsToCheck = ['src', 'public', 'backend', 'proxy'];
|
|
||||||
for (const p of pathsToCheck) {
|
|
||||||
const full = path.join(LOCAL_ROOT, p);
|
|
||||||
if (fs.existsSync(full)) {
|
|
||||||
const files = getFilesLocal(full);
|
|
||||||
for (const f of files) {
|
|
||||||
const localPath = path.join(LOCAL_ROOT, f);
|
|
||||||
localFiles[f] = { md5: getMD5(localPath) };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const rootConfigs = ['package.json', 'tsconfig.json', 'next.config.ts', '.env.production'];
|
|
||||||
for (const f of rootConfigs) {
|
|
||||||
const full = path.join(LOCAL_ROOT, f);
|
|
||||||
if (fs.existsSync(full)) {
|
|
||||||
localFiles[f] = { md5: getMD5(full) };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Perform assertions
|
|
||||||
for (const [relPath, info] of Object.entries(remoteFiles)) {
|
|
||||||
const localInfo = localFiles[relPath];
|
|
||||||
assert(`File "${relPath}" should exist locally`, !!localInfo);
|
|
||||||
if (localInfo) {
|
|
||||||
assert(`File "${relPath}" content should match remote exactly`, localInfo.md5 === info.md5);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`\n=========================================`);
|
|
||||||
console.log(` RESULT: ${passedCount} / ${testCount} tests passed.`);
|
|
||||||
console.log(`=========================================\n`);
|
|
||||||
|
|
||||||
if (passedCount !== testCount) {
|
|
||||||
process.exit(1);
|
|
||||||
} else {
|
|
||||||
process.exit(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
runTests().catch(e => {
|
|
||||||
console.error('[FATAL]', e.message);
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
-28
@@ -1,28 +0,0 @@
|
|||||||
const axios = require('axios');
|
|
||||||
const BASE_URL = 'https://api0.dev.backone.cloud/api/v1';
|
|
||||||
const { backoneFetch } = require('./proxy/backoneClientCore');
|
|
||||||
|
|
||||||
async function test() {
|
|
||||||
const agentUuid = 'F6-2V-DT-8A';
|
|
||||||
console.log(`Fetching flows for Agent UUID: ${agentUuid}...`);
|
|
||||||
|
|
||||||
const flows = await backoneFetch('/data/flows', { settings_limit: 200 }, agentUuid);
|
|
||||||
const ips = flows ? flows.map(f => f.local_ip?.address).filter(Boolean) : [];
|
|
||||||
|
|
||||||
const ips10_6 = ips.filter(ip => ip.startsWith('10.6'));
|
|
||||||
const ips192 = ips.filter(ip => ip.startsWith('192.168'));
|
|
||||||
const ips10_1 = ips.filter(ip => ip.startsWith('10.1.'));
|
|
||||||
|
|
||||||
console.log('\n--- Hasil Test untuk Agen', agentUuid, '---');
|
|
||||||
console.log(`Total IPs didapat: ${ips.length}`);
|
|
||||||
console.log(`Jumlah IP Balaraja (10.6.x.x): ${ips10_6.length}`);
|
|
||||||
if (ips10_6.length > 0) console.log('Contoh 10.6.x.x:', ips10_6.slice(0, 5));
|
|
||||||
|
|
||||||
console.log(`\nJumlah IP Kantor/Lainnya (192.168.x.x): ${ips192.length}`);
|
|
||||||
if (ips192.length > 0) console.log('Contoh 192.168.x.x:', ips192.slice(0, 5));
|
|
||||||
|
|
||||||
console.log(`\nJumlah IP Lainnya (10.1.x.x): ${ips10_1.length}`);
|
|
||||||
if (ips10_1.length > 0) console.log('Contoh 10.1.x.x:', ips10_1.slice(0, 5));
|
|
||||||
}
|
|
||||||
|
|
||||||
test();
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
const mongoose = require('mongoose');
|
|
||||||
mongoose.connect('mongodb://backone_inspect:backone_inspect@mongodb.prod.proit.id:27017/backone_inspect_0').then(async () => {
|
|
||||||
const db = mongoose.connection.db;
|
|
||||||
const raw = await db.collection('countrystats').aggregate([
|
|
||||||
{ $group: {
|
|
||||||
_id: '$country_name',
|
|
||||||
download: { $sum: '$download' },
|
|
||||||
upload: { $sum: '$upload' },
|
|
||||||
flow_count: { $sum: { $ifNull: ['$flows', 1] } },
|
|
||||||
country_code: { $first: '$country_name' }
|
|
||||||
}},
|
|
||||||
{ $sort: { download: -1 } },
|
|
||||||
{ $limit: 200 }
|
|
||||||
]).toArray();
|
|
||||||
console.log(raw.length, 'countries returned');
|
|
||||||
console.log(raw.slice(0, 10));
|
|
||||||
mongoose.disconnect();
|
|
||||||
});
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
const mongoose = require('mongoose');
|
|
||||||
mongoose.connect('mongodb://backone_inspect:backone_inspect@mongodb.prod.proit.id:27017/backone_inspect_0').then(async () => {
|
|
||||||
const db = mongoose.connection.db;
|
|
||||||
const raw = await db.collection('countrystats').aggregate([
|
|
||||||
{ $group: {
|
|
||||||
_id: '$country_name',
|
|
||||||
download: { $sum: '$download' },
|
|
||||||
upload: { $sum: '$upload' },
|
|
||||||
flow_count: { $sum: { $ifNull: ['$flows', 1] } },
|
|
||||||
country_code: { $first: '$country_name' }
|
|
||||||
}},
|
|
||||||
{ $sort: { download: -1 } },
|
|
||||||
{ $limit: 200 }
|
|
||||||
]).toArray();
|
|
||||||
const data = raw.filter(r => r.country_code && r.country_code !== 'Unknown' && r.country_code.length === 2);
|
|
||||||
const dataWithBW = data.filter(r => r.download > 0 || r.upload > 0);
|
|
||||||
console.log('Total valid countries:', data.length);
|
|
||||||
console.log('Countries with > 0 bandwidth:', dataWithBW.length);
|
|
||||||
mongoose.disconnect();
|
|
||||||
});
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
const mongoose = require('mongoose');
|
|
||||||
mongoose.connect('mongodb://backone_inspect:backone_inspect@mongodb.prod.proit.id:27017/backone_inspect_0').then(async () => {
|
|
||||||
const db = mongoose.connection.db;
|
|
||||||
const raw = await db.collection('countrystats').aggregate([
|
|
||||||
{ $match: { agent_uuid: 'F6-2V-DT-8A' } },
|
|
||||||
{ $group: {
|
|
||||||
_id: '$country_name',
|
|
||||||
download: { $sum: '$download' },
|
|
||||||
upload: { $sum: '$upload' }
|
|
||||||
}},
|
|
||||||
{ $sort: { download: -1 } },
|
|
||||||
{ $limit: 200 }
|
|
||||||
]).toArray();
|
|
||||||
const valid = raw.filter(r => r.download > 0 || r.upload > 0);
|
|
||||||
console.log(valid.length, 'valid countries for Kantor in CountryStat');
|
|
||||||
|
|
||||||
const flows = await db.collection('flows').aggregate([
|
|
||||||
{ $match: { agent_uuid: 'F6-2V-DT-8A', country_name: { $ne: null } } },
|
|
||||||
{ $group: {
|
|
||||||
_id: '$country_name',
|
|
||||||
download: { $sum: '$download' },
|
|
||||||
upload: { $sum: '$upload' }
|
|
||||||
}},
|
|
||||||
{ $sort: { download: -1 } },
|
|
||||||
{ $limit: 200 }
|
|
||||||
]).toArray();
|
|
||||||
const validFlows = flows.filter(r => r.download > 0 || r.upload > 0);
|
|
||||||
console.log(validFlows.length, 'valid countries for Kantor in Flow');
|
|
||||||
mongoose.disconnect();
|
|
||||||
});
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
const mongoose = require('mongoose');
|
|
||||||
mongoose.connect('mongodb://backone_inspect:backone_inspect@mongodb.prod.proit.id:27017/backone_inspect_0').then(async () => {
|
|
||||||
const db = mongoose.connection.db;
|
|
||||||
const now = new Date();
|
|
||||||
const filter = { timestamp: { $gte: new Date(now.getTime() - 24 * 3600000) } };
|
|
||||||
const raw = await db.collection('countrystats').aggregate([
|
|
||||||
{ $match: filter },
|
|
||||||
{ $group: {
|
|
||||||
_id: '$country_name',
|
|
||||||
download: { $sum: '$download' },
|
|
||||||
upload: { $sum: '$upload' }
|
|
||||||
}}
|
|
||||||
]).toArray();
|
|
||||||
console.log(raw.length, 'countries in last 24h');
|
|
||||||
mongoose.disconnect();
|
|
||||||
});
|
|
||||||
-11
@@ -1,11 +0,0 @@
|
|||||||
const mongoose = require('mongoose');
|
|
||||||
mongoose.connect('mongodb://backone_inspect:backone_inspect@mongodb.prod.proit.id:27017/backone_inspect_0').then(async () => {
|
|
||||||
const db = mongoose.connection.db;
|
|
||||||
const flows = await db.collection('flows').aggregate([
|
|
||||||
{ $group: { _id: '$agent_uuid', src_ips: { $addToSet: '$src_ip' } } }
|
|
||||||
]).toArray();
|
|
||||||
flows.forEach(f => {
|
|
||||||
console.log('Agent:', f._id, 'IPs:', f.src_ips.slice(0, 10).join(', '));
|
|
||||||
});
|
|
||||||
mongoose.disconnect();
|
|
||||||
});
|
|
||||||
Reference in new issue
Block a user