8 Commits
9 changed files with 206 additions and 21 deletions

No files matched your search

+2 -2
View File
@@ -16,7 +16,7 @@ Produk BackOne oleh **PT. Data Bisnis Solusi** — Dashboard monitoring jaringan
│ │ Netify API → │ │ MongoDB │ │
│ │ MongoDB writer │ │ Database │ │
│ └──────────────────┘ └─────────────┘ │
│ Network: backone-infra │
│ Network: backone-infra │
└─────────────────────────────────────────┘
│ MongoDB shared
┌─────────────────────────────────────────┐
@@ -28,7 +28,7 @@ Produk BackOne oleh **PT. Data Bisnis Solusi** — Dashboard monitoring jaringan
│ │ REST API │──│ Next.js │ │
│ │ MongoDB reader │ │ Dashboard │ │
│ └──────────────────┘ └─────────────┘ │
│ Network: backone-app │
│ Network: backone-app │
└─────────────────────────────────────────┘
```
+9 -3
View File
@@ -214,11 +214,17 @@ router.get('/agents/list', async (req, res) => {
const agents = await db.collection('agent_registry')
.find(filter)
.project({ uuid: 1, label: 1, _id: 0 })
.project({ uuid: 1, label: 1, protected_label: 1, _id: 0 })
.sort({ uuid: 1 })
.toArray();
res.json({ ok: true, data: agents });
// Use protected_label if available to prevent proxy overwrite bug
const mappedAgents = agents.map(a => ({
uuid: a.uuid,
label: a.protected_label || a.label || a.uuid
}));
res.json({ ok: true, data: mappedAgents });
} catch (err) {
res.status(500).json({ ok: false, error: err.message });
}
+12 -4
View File
@@ -34,6 +34,8 @@ async function collectAllAgents() {
const totalAgents = agents.length;
console.log(`[Collector] Processing ${totalAgents} unique agents globally`);
// Step 1: Upsert technical metadata only (NEVER touch label field during updates)
// This prevents ANY version of proxy from overwriting a custom label set by the UI.
await Promise.allSettled(agents.map(a =>
AgentRegistry.findOneAndUpdate(
{ uuid: a.uuid },
@@ -41,18 +43,24 @@ async function collectAllAgents() {
$set: {
uuid: a.uuid,
serial: a.serial || a.uuid,
site_uuid: 'global', // Store globally
site_uuid: 'global',
provisioned: a.provisioned ?? true,
activated: a.activated ?? false,
last_seen_at: a.last_seen_at ?? null,
},
$setOnInsert: {
label: a.label,
}
},
{ upsert: true, new: true }
)
));
// Step 2: Set default label ONLY for agents that have no label yet (brand new agents).
// We NEVER overwrite an existing label — even if it was set by an older proxy version.
await Promise.allSettled(agents.map(a =>
AgentRegistry.updateOne(
{ uuid: a.uuid, $or: [{ label: { $exists: false } }, { label: null }, { label: '' }] },
{ $set: { label: a.label } }
)
));
console.log(`[Collector] ✓ ${agents.length} agents upserted into registry globally`);
for (const agent of agents) {
+69 -1
View File
@@ -2,6 +2,7 @@
// ─────────────────────────────────────────────────────────────────────────────
// Pruning process for BackOne MongoDB data retention.
// Removes telemetry records older than 30 days to conserve database space.
// Also removes device/flow records that are outside the agent's configured subnet.
// ─────────────────────────────────────────────────────────────────────────────
const mongoose = require('mongoose');
@@ -31,6 +32,73 @@ async function pruneOldData() {
console.error(`[Collector] [Retention] ✗ Failed to prune ${name}: ${err.message}`);
}
}
// Also prune out-of-subnet data
await pruneOutOfSubnetData();
}
module.exports = { pruneOldData };
// ─────────────────────────────────────────────────────────────────────────────
// Subnet-based cleanup: remove devices/flows that are outside the agent's
// configured allowed_subnets. Runs after every collection cycle automatically.
// ─────────────────────────────────────────────────────────────────────────────
function _ipToLong(ip) {
return ip.split('.').reduce((acc, o) => (acc << 8) + parseInt(o, 10), 0) >>> 0;
}
function _ipMatchesSubnets(ip, subnets) {
if (!ip || ip.includes(':')) return false; // skip IPv6
if (!subnets || subnets.length === 0) return true; // no restriction
return subnets.some(s => {
if (s.includes('/')) {
try {
const [r, b] = s.split('/');
const bits = parseInt(b, 10);
if (isNaN(bits) || bits < 0 || bits > 32) return false;
const mask = bits === 0 ? 0 : (~0 << (32 - bits)) >>> 0;
return (_ipToLong(ip) & mask) === (_ipToLong(r) & mask);
} catch { return false; }
}
return ip.startsWith(s + '.') || ip === s;
});
}
async function pruneOutOfSubnetData() {
try {
const db = mongoose.connection.db;
const agents = await db.collection('agent_registry')
.find({ allowed_subnets: { $exists: true, $not: { $size: 0 } } })
.toArray();
for (const agent of agents) {
const uuid = agent.uuid;
const subnets = (agent.allowed_subnets || []).map(s => s.trim()).filter(Boolean);
if (subnets.length === 0) continue;
// devicestats
const devIps = await db.collection('devicestats').distinct('ip_address', { agent_uuid: uuid });
const badDevIps = devIps.filter(ip => !_ipMatchesSubnets(ip, subnets));
if (badDevIps.length > 0) {
const r1 = await db.collection('devicestats').deleteMany({ agent_uuid: uuid, ip_address: { $in: badDevIps } });
const r2 = await db.collection('deviceappstats').deleteMany({ agent_uuid: uuid, ip_address: { $in: badDevIps } });
if (r1.deletedCount + r2.deletedCount > 0) {
console.log(`[Collector] [Subnet] ${uuid}: removed ${r1.deletedCount} device + ${r2.deletedCount} deviceapp records (${badDevIps.length} bad IPs)`);
}
}
// flows
const flowIps = await db.collection('flows').distinct('src_ip', { agent_uuid: uuid });
const badFlowIps = flowIps.filter(ip => !_ipMatchesSubnets(ip, subnets));
if (badFlowIps.length > 0) {
const r = await db.collection('flows').deleteMany({ agent_uuid: uuid, src_ip: { $in: badFlowIps } });
if (r.deletedCount > 0) {
console.log(`[Collector] [Subnet] ${uuid}: removed ${r.deletedCount} flow records (${badFlowIps.length} bad IPs)`);
}
}
}
} catch (err) {
console.error('[Collector] [Subnet] pruneOutOfSubnetData error:', err.message);
}
}
module.exports = { pruneOldData, pruneOutOfSubnetData };
+10 -2
View File
@@ -50,10 +50,18 @@ export default function SummaryPage() {
const mapData = useMemo(() => {
if (!countries.data) return [];
// Jika summary menunjukkan tidak ada traffic sama sekali, jangan tampilkan globe
const totalTraffic = (summary.data?.bandwidth_down || 0) + (summary.data?.bandwidth_up || 0);
if (totalTraffic === 0) return [];
const origin = getOriginLocation();
return countries.data.reduce((acc: any[], row) => {
// Hanya tampilkan negara yang punya traffic aktual > 0
const traffic = (row.download || 0) + (row.upload || 0);
if (traffic === 0) return acc;
const dest = getDestinationLocation(row.country_code, row.country_name);
if (dest) {
acc.push({
@@ -63,7 +71,7 @@ export default function SummaryPage() {
endLng: dest.lng,
fromLabel: origin.label,
toLabel: dest.label,
value: row.download + row.upload,
value: traffic,
download: row.download,
upload: row.upload,
countryCode: row.country_code
@@ -71,7 +79,7 @@ export default function SummaryPage() {
}
return acc;
}, []);
}, [countries.data]);
}, [countries.data, summary.data]);
useEffect(() => {
setMounted(true);
@@ -0,0 +1,85 @@
/**
* GET /api/dashboard/agents/list
*
* Override route: bypass backend container dan baca langsung dari MongoDB.
* Prioritaskan protected_label agar label tidak teroverwrite oleh proxy lama.
*/
import { NextRequest, NextResponse } from 'next/server';
import { cookies } from 'next/headers';
import jwt from 'jsonwebtoken';
import mongoose from 'mongoose';
const JWT_SECRET = process.env.JWT_SECRET || 'super-secret-backone-key';
const MONGODB_URI = process.env.MONGODB_URI || '';
async function connectMongo() {
if (mongoose.connection.readyState !== 1) {
await mongoose.connect(MONGODB_URI, { serverSelectionTimeoutMS: 5000 });
}
return mongoose.connection.db!;
}
export async function GET(req: NextRequest) {
try {
// Verify JWT from cookie
const cookieStore = await cookies();
const token = cookieStore.get('token')?.value;
if (!token) {
return NextResponse.json({ ok: false, error: 'Unauthorized' }, { status: 401 });
}
let user: any;
try {
user = jwt.verify(token, JWT_SECRET) as any;
} catch {
return NextResponse.json({ ok: false, error: 'Invalid token' }, { status: 401 });
}
const db = await connectMongo();
const companyRoles = ['COMPANY_ADMIN', 'COMPANY_OPERATOR', 'COMPANY_VIEWER'];
const isCompanyRole = companyRoles.includes(user?.role);
const filter: any = {};
if (isCompanyRole) {
const agentUuids: string[] = user?.agent_uuids || [];
if (agentUuids.length === 0) {
return NextResponse.json({ ok: true, data: [] });
}
filter.uuid = { $in: agentUuids };
} else {
// Admin/SUPER_ADMIN: filter by site UUID
const siteUuidHeader = req.headers.get('x-backone-site-uuid');
const effectiveRole = user?._originalRole || user?.role;
const isGlobalUser = effectiveRole === 'SUPER_ADMIN' || effectiveRole === 'EXECUTIVE';
const envSites = (process.env.BACKONE_SITE_UUIDS || process.env.BACKONE_SITE_UUID || '')
.split(',').map((s: string) => s.trim()).filter(Boolean);
if (isGlobalUser && siteUuidHeader && siteUuidHeader !== 'all' && envSites.includes(siteUuidHeader)) {
filter.site_uuid = { $in: [siteUuidHeader, 'global'] };
} else if (envSites.length > 0) {
filter.site_uuid = { $in: [...envSites, 'global'] };
}
}
const agents = await db.collection('agent_registry')
.find(filter)
.project({ uuid: 1, label: 1, protected_label: 1, _id: 0 })
.sort({ uuid: 1 })
.toArray();
// Prioritaskan protected_label untuk mencegah UUID overwrite dari proxy lama
const mappedAgents = agents.map((a: any) => ({
uuid: a.uuid,
label: a.protected_label || a.label || a.uuid,
}));
return NextResponse.json({ ok: true, data: mappedAgents });
} catch (err: any) {
console.error('[agents/list override]', err.message);
return NextResponse.json({ ok: false, error: err.message }, { status: 500 });
}
}
+3 -2
View File
@@ -52,10 +52,11 @@ export function Sidebar({ isMobile = false, onClose }: SidebarProps) {
// Selalu pakai site_uuid dari DB (mereka tidak bisa ganti site).
const globalRoles = ['SUPER_ADMIN', 'EXECUTIVE', 'SOC_ANALYST', 'ENGINEER'];
const isGlobalRole = globalRoles.includes(data.user.role || '');
const storedSiteUuid = localStorage.getItem('backone_site_uuid');
if (isGlobalRole) {
const siteToUse = storedSiteUuid || 'all';
// Karena UI site selector sudah diganti menjadi Agent Selector (View As),
// Global roles harus selalu diset ke 'all' agar tidak tersangkut di filter site sebelumnya (dari akun lain).
const siteToUse = 'all';
localStorage.setItem('backone_site_uuid', siteToUse);
setSelectedSite(siteToUse);
} else if (data.user.site_uuid) {
+4 -1
View File
@@ -30,9 +30,12 @@ export async function updateAgent(agentId: string, label: string) {
return { success: false, message: "Agent tidak ditemukan di database lokal." };
}
// Write both label AND protected_label.
// protected_label is ONLY set by the UI — the Proxy NEVER touches it.
// This ensures custom names survive even if the Proxy overwrites label field.
await db.collection('agent_registry').updateOne(
{ _id: agent._id },
{ $set: { label: label } }
{ $set: { label: label, protected_label: label } }
);
// Bismillah: Hapus semua data duplikat/hantu milik agent ini agar tidak ditimpa oleh proxy lawas/bug ganda
+12 -6
View File
@@ -89,7 +89,8 @@ export async function fetchAgentsFromMongo(
}
// 3. Optimize status check and last seen per agent using parallel indexed queries
const fifteenMinutesAgoQuery = new Date(Date.now() - 15 * 60000);
// Agent dianggap Online jika ada flow dalam 1 jam terakhir
const oneHourAgoQuery = new Date(Date.now() - 60 * 60000);
const agentResults = await Promise.all(
agentSource.map(async (item: any, idx: number) => {
@@ -99,7 +100,7 @@ export async function fetchAgentsFromMongo(
// Execute status checks for active flow, latest flow, and latest summary in parallel
const [activeFlow, latestFlow, latestSummary] = await Promise.all([
db.collection('flows').findOne(
{ agent_uuid: uuid, timestamp: { $gte: fifteenMinutesAgoQuery } },
{ agent_uuid: uuid, timestamp: { $gte: oneHourAgoQuery } },
{ projection: { _id: 1 } }
),
db.collection('flows').findOne(
@@ -145,9 +146,14 @@ export async function fetchAgentsFromMongo(
lastSeenDate.getFullYear() === now.getFullYear();
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 seenStr = isToday ? timeStr : `${dateStr} ${timeStr}`;
statusHuman = isOnline ? `Last seen: ${seenStr}` : `Offline (Last seen: ${seenStr})`;
if (isOnline) {
// Online: cukup tampilkan jam saja (hari ini pasti)
statusHuman = `Last seen: ${timeStr}`;
} else {
// Offline: selalu tampilkan tanggal + jam lengkap
statusHuman = `Offline (Last seen: ${dateStr} ${timeStr})`;
}
} else if (isOnline) {
statusHuman = 'Last seen: Just now';
}
@@ -160,7 +166,7 @@ export async function fetchAgentsFromMongo(
organization_uuid: '',
provisioned: item.provisioned ?? true,
activated: true,
label: item.label || uuid,
label: item.protected_label || item.label || uuid,
created_at: { human: 'N/A', date: '', unix_time: 0 },
updated_at: { human: 'N/A', date: '', unix_time: 0 },
last_seen_at: {