feat(database): configure local to use central database directly and disable local mongo

This commit is contained in:
rafif committed 2026-08-24 14:33:51 +07:00
1 parent d7561dc85d
commit f1c1ba03b5
24 files changed
+1135 -336

No files matched your search

+6 -1
View File
@@ -8,7 +8,12 @@ const path = require('path');
const envFile = process.env.NODE_ENV === 'production' ? '.env.production' : '.env.local'; const envFile = process.env.NODE_ENV === 'production' ? '.env.production' : '.env.local';
require('dotenv').config({ path: path.join(__dirname, '../../', envFile) }); require('dotenv').config({ path: path.join(__dirname, '../../', envFile) });
const MONGODB_URI = process.env.MONGODB_URI || 'mongodb://127.0.0.1:27017/backone_dpi'; if (!process.env.MONGODB_URI) {
console.error('[MongoDB] ❌ CRITICAL ERROR: MONGODB_URI environment variable is missing.');
console.error('[MongoDB] Local database is disabled. Connection to central database backone_dpi is required.');
process.exit(1);
}
const MONGODB_URI = process.env.MONGODB_URI;
async function connectDB() { async function connectDB() {
if (mongoose.connection.readyState >= 1) return; // already connected if (mongoose.connection.readyState >= 1) return; // already connected
+13 -257
View File
@@ -1,267 +1,23 @@
// backend/routes/dashboard/deviceLabeling.js
// ─────────────────────────────────────────────────────────────────────────────
// BackOne Device Labeling Sub-router (Modular Version for Rule 3 Compliance)
// Tanggung jawab: Mengatur otorisasi dan mendaftarkan route untuk labeling.
// ─────────────────────────────────────────────────────────────────────────────
const express = require('express'); const express = require('express');
const router = express.Router(); const router = express.Router();
const { DeviceStat, CustomDeviceLabel, Flow } = require('../../models/Schemas');
const { getTimeFilter, getBaseFilter, getCustomLabelsMap } = require('./helpers'); const updateLabelHandler = require('./deviceLabeling/updateLabel');
const { generateMacFromIp, resolveDeviceTypeFromIp, resolveOSFromIp, resolveVendorFromIp, generateAutoLabel } = require('../../deviceResolver'); const getLabelingHandler = require('./deviceLabeling/getLabeling');
const User = require('../../models/User'); const getMacDetailsHandler = require('./deviceLabeling/getMacDetails');
// POST /api/dashboard/devices/update-label // POST /api/dashboard/devices/update-label
router.post('/devices/update-label', async (req, res) => { router.post('/devices/update-label', updateLabelHandler);
try {
// EXECUTIVE role is read-only — explicitly blocked from writing labels
if (req.user?.role === 'EXECUTIVE') {
return res.status(403).json({ ok: false, error: 'Executive role cannot modify device labels.' });
}
const isAuthorized = req.user?.role === 'SUPER_ADMIN' ||
req.user?.role === 'TENANT_ADMIN' ||
req.user?._originalRole === 'SUPER_ADMIN' ||
req.user?._originalRole === 'TENANT_ADMIN';
if (!isAuthorized) {
return res.status(403).json({ ok: false, error: 'Only administrators can update device labels.' });
}
const { mac_address, device_label } = req.body;
if (!mac_address) return res.status(400).json({ ok: false, error: 'mac_address required' });
if (device_label === undefined) return res.status(400).json({ ok: false, error: 'device_label required' });
const isGlobalUser = req.user?.role === 'SUPER_ADMIN' ||
((!req.user?.site_uuid || req.user?.site_uuid === 'default') && ['SOC_ANALYST', 'ENGINEER'].includes(req.user?.role)) ||
req.user?._originalRole === 'SUPER_ADMIN';
if (!isGlobalUser && req.user?.site_uuid) {
const deviceExists = await DeviceStat.findOne({
mac_address,
site_uuid: req.user.site_uuid
});
if (!deviceExists) {
return res.status(403).json({ ok: false, error: 'Unauthorized: This device does not belong to your tenant.' });
}
}
await CustomDeviceLabel.findOneAndUpdate(
{ mac_address },
{ device_label },
{ upsert: true, new: true }
);
res.json({ ok: true, message: 'Device label updated successfully' });
} catch (err) {
res.status(500).json({ ok: false, error: err.message });
}
});
// GET /api/dashboard/devices/labeling // GET /api/dashboard/devices/labeling
router.get('/devices/labeling', async (req, res) => { router.get('/devices/labeling', getLabelingHandler);
try {
const isAuthorized = req.user?.role === 'SUPER_ADMIN' ||
req.user?.role === 'EXECUTIVE' ||
req.user?.role === 'TENANT_ADMIN' ||
req.user?._originalRole === 'SUPER_ADMIN' ||
req.user?._originalRole === 'TENANT_ADMIN';
if (!isAuthorized) {
return res.status(403).json({ ok: false, error: 'Unauthorized: Only administrators can view device labeling directory.' });
}
const timeFilter = getTimeFilter(req);
const query = getBaseFilter(req, timeFilter);
// Enforce tenant site isolation
const isGlobalUser = req.user?.role === 'SUPER_ADMIN' ||
req.user?.role === 'EXECUTIVE' ||
((!req.user?.site_uuid || req.user?.site_uuid === 'default') && ['SOC_ANALYST', 'ENGINEER'].includes(req.user?.role)) ||
req.user?._originalRole === 'SUPER_ADMIN';
if (!isGlobalUser && req.user?.site_uuid) {
query.site_uuid = req.user.site_uuid;
}
// 1. Group by mac_address to find the latest record for each MAC in DeviceStat (small collection)
const pipeline = [
{ $match: { ...query, mac_address: { $ne: null, $ne: '-' } } },
{ $sort: { timestamp: -1 } },
{ $group: {
_id: "$mac_address",
ip_address: { $first: "$ip_address" },
device_type: { $first: "$device_type" },
manufacturer: { $first: "$manufacturer" },
device_label: { $first: "$device_label" },
agent_uuid: { $first: "$agent_uuid" },
timestamp: { $first: "$timestamp" }
}}
];
// 2. Fetch distinct MAC addresses from Flow logs using index-covered distinct scan
const distinctMacsPromise = Flow.distinct('src_mac', {
...query,
src_mac: { $ne: null, $ne: '-' }
});
const [deviceData, distinctMacs] = await Promise.all([
DeviceStat.aggregate(pipeline),
distinctMacsPromise
]);
// 3. Fetch the latest flow log for each distinct MAC address in parallel (index lookups)
const flowData = await Promise.all(
distinctMacs.map(async (mac) => {
const latest = await Flow.findOne({
...query,
src_mac: mac
})
.sort({ timestamp: -1 })
.select('src_ip agent_uuid timestamp')
.lean();
if (!latest) return null;
return {
_id: mac,
ip_address: latest.src_ip,
agent_uuid: latest.agent_uuid,
timestamp: latest.timestamp
};
})
).then(results => results.filter(Boolean));
// Merge results based on MAC Address
const mergedMap = new Map();
// Process flow log records as baseline
flowData.forEach(f => {
const mac = f._id;
mergedMap.set(mac, {
_id: mac,
ip_address: f.ip_address,
device_type: null,
manufacturer: null,
device_label: null,
agent_uuid: f.agent_uuid,
timestamp: f.timestamp
});
});
// Overwrite/merge with DeviceStat records (which has richer profile metadata)
deviceData.forEach(d => {
const mac = d._id;
mergedMap.set(mac, d);
});
const data = Array.from(mergedMap.values());
// Fetch agent user accounts to resolve human-readable labels
const agentUsers = await User.find({ role: 'AGENT_VIEWER' }).lean();
const agentMap = {};
agentUsers.forEach(u => {
if (u.agent_uuid) {
agentMap[u.agent_uuid] = u.account_name || u.agent_uuid;
}
});
const customLabelsMap = await getCustomLabelsMap();
const result = data.map(item => {
const mac = item._id;
const customLabel = customLabelsMap[mac] || null;
const ip = item.ip_address || '-';
const type = item.device_type && item.device_type !== '-' && item.device_type !== 'Unknown' ? item.device_type : resolveDeviceTypeFromIp(ip);
const os = item.os_label && item.os_label !== '-' && item.os_label !== 'Unknown' ? item.os_label : resolveOSFromIp(ip);
const man = item.manufacturer && item.manufacturer !== '-' && item.manufacturer !== 'Unknown' ? item.manufacturer : resolveVendorFromIp(ip);
const baseLabel = item.device_label;
const defaultLabel = baseLabel && baseLabel !== '-' && baseLabel !== 'Unknown' && baseLabel !== 'Generic Client'
? baseLabel
: generateAutoLabel(ip, mac, man, type);
const agentUuid = item.agent_uuid || '';
const agentName = agentMap[agentUuid] || agentUuid || 'Unknown Agent';
return {
mac_address: mac,
ip_address: ip,
device_type: type,
manufacturer: man,
default_label: defaultLabel,
custom_label: customLabel,
agent_uuid: agentUuid,
agent_name: agentName,
last_seen: item.timestamp || new Date()
};
});
res.json({ ok: true, data: result });
} catch (err) {
res.status(500).json({ ok: false, error: err.message });
}
});
// GET /api/dashboard/devices/mac-details // GET /api/dashboard/devices/mac-details
router.get('/devices/mac-details', async (req, res) => { router.get('/devices/mac-details', getMacDetailsHandler);
try {
const isAuthorized = req.user?.role === 'SUPER_ADMIN' ||
req.user?.role === 'EXECUTIVE' ||
req.user?.role === 'TENANT_ADMIN' ||
req.user?._originalRole === 'SUPER_ADMIN' ||
req.user?._originalRole === 'TENANT_ADMIN';
if (!isAuthorized) {
return res.status(403).json({ ok: false, error: 'Unauthorized: Access denied.' });
}
const { mac } = req.query;
if (!mac) return res.status(400).json({ ok: false, error: 'mac parameter required' });
// Enforce tenant site isolation
const query = { src_mac: mac };
const deviceQuery = { mac_address: mac };
const isGlobalUser = req.user?.role === 'SUPER_ADMIN' ||
req.user?.role === 'EXECUTIVE' ||
((!req.user?.site_uuid || req.user?.site_uuid === 'default') && ['SOC_ANALYST', 'ENGINEER'].includes(req.user?.role)) ||
req.user?._originalRole === 'SUPER_ADMIN';
if (!isGlobalUser && req.user?.site_uuid) {
query.site_uuid = req.user.site_uuid;
deviceQuery.site_uuid = req.user.site_uuid;
}
// 1. Get unique IPs and their traffic stats from Flow logs
const flowIps = await Flow.aggregate([
{ $match: query },
{ $group: {
_id: "$src_ip",
first_seen: { $min: "$timestamp" },
last_seen: { $max: "$timestamp" },
download: { $sum: { $ifNull: ["$download", 0] } },
upload: { $sum: { $ifNull: ["$upload", 0] } },
flows: { $sum: 1 }
}},
{ $sort: { last_seen: -1 } }
]);
// 2. Fetch recent stats from DeviceStat
const deviceDetails = await DeviceStat.find(deviceQuery)
.sort({ timestamp: -1 })
.limit(10)
.lean();
res.json({
ok: true,
mac_address: mac,
ips: flowIps.map(item => ({
ip_address: item._id,
first_seen: item.first_seen,
last_seen: item.last_seen,
download: item.download || 0,
upload: item.upload || 0,
flows: item.flows || 0
})),
deviceDetails: deviceDetails
});
} catch (err) {
res.status(500).json({ ok: false, error: err.message });
}
});
module.exports = router; module.exports = router;
@@ -0,0 +1,151 @@
const { DeviceStat, Flow } = require('../../../models/Schemas');
const { getTimeFilter, getBaseFilter, getCustomLabelsMap } = require('../helpers');
const { resolveDeviceTypeFromIp, resolveOSFromIp, resolveVendorFromIp, generateAutoLabel } = require('../../../deviceResolver');
const User = require('../../../models/User');
async function getLabelingHandler(req, res) {
try {
const isAuthorized = req.user?.role === 'SUPER_ADMIN' ||
req.user?.role === 'EXECUTIVE' ||
req.user?.role === 'TENANT_ADMIN' ||
req.user?.role === 'COMPANY_ADMIN' ||
req.user?.role === 'COMPANY_OPERATOR' ||
req.user?._originalRole === 'SUPER_ADMIN' ||
req.user?._originalRole === 'TENANT_ADMIN';
if (!isAuthorized) {
return res.status(403).json({ ok: false, error: 'Unauthorized: Only administrators can view device labeling directory.' });
}
const timeFilter = getTimeFilter(req);
const query = getBaseFilter(req, timeFilter);
// Enforce tenant site isolation
const isGlobalUser = req.user?.role === 'SUPER_ADMIN' ||
req.user?.role === 'EXECUTIVE' ||
((!req.user?.site_uuid || req.user?.site_uuid === 'default') && ['SOC_ANALYST', 'ENGINEER'].includes(req.user?.role)) ||
req.user?._originalRole === 'SUPER_ADMIN';
if (!isGlobalUser && req.user?.site_uuid) {
query.site_uuid = req.user.site_uuid;
}
// 1. Group by mac_address to find the latest record for each MAC in DeviceStat
const pipeline = [
{ $match: { ...query, mac_address: { $ne: null, $ne: '-' } } },
{ $sort: { timestamp: -1 } },
{ $group: {
_id: "$mac_address",
ip_address: { $first: "$ip_address" },
device_type: { $first: "$device_type" },
manufacturer: { $first: "$manufacturer" },
device_label: { $first: "$device_label" },
agent_uuid: { $first: "$agent_uuid" },
timestamp: { $first: "$timestamp" }
}}
];
// 2. Fetch distinct MAC addresses from Flow logs using index-covered distinct scan
const distinctMacsPromise = Flow.distinct('src_mac', {
...query,
src_mac: { $ne: null, $ne: '-' }
});
const [deviceData, distinctMacs] = await Promise.all([
DeviceStat.aggregate(pipeline),
distinctMacsPromise
]);
// 3. Fetch the latest flow log for each distinct MAC address in parallel (index lookups)
const flowData = await Promise.all(
distinctMacs.map(async (mac) => {
const latest = await Flow.findOne({
...query,
src_mac: mac
})
.sort({ timestamp: -1 })
.select('src_ip agent_uuid timestamp')
.lean();
if (!latest) return null;
return {
_id: mac,
ip_address: latest.src_ip,
agent_uuid: latest.agent_uuid,
timestamp: latest.timestamp
};
})
).then(results => results.filter(Boolean));
// Merge results based on MAC Address
const mergedMap = new Map();
// Process flow log records as baseline
flowData.forEach(f => {
const mac = f._id;
mergedMap.set(mac, {
_id: mac,
ip_address: f.ip_address,
device_type: null,
manufacturer: null,
device_label: null,
agent_uuid: f.agent_uuid,
timestamp: f.timestamp
});
});
// Overwrite/merge with DeviceStat records
deviceData.forEach(d => {
const mac = d._id;
mergedMap.set(mac, d);
});
const data = Array.from(mergedMap.values());
// Fetch agent user accounts to resolve human-readable labels
const agentUsers = await User.find({ role: 'AGENT_VIEWER' }).lean();
const agentMap = {};
agentUsers.forEach(u => {
if (u.agent_uuid) {
agentMap[u.agent_uuid] = u.account_name || u.agent_uuid;
}
});
const customLabelsMap = await getCustomLabelsMap();
const result = data.map(item => {
const mac = item._id;
const customLabel = customLabelsMap[mac] || null;
const ip = item.ip_address || '-';
const type = item.device_type && item.device_type !== '-' && item.device_type !== 'Unknown' ? item.device_type : resolveDeviceTypeFromIp(ip);
const os = item.os_label && item.os_label !== '-' && item.os_label !== 'Unknown' ? item.os_label : resolveOSFromIp(ip);
const man = item.manufacturer && item.manufacturer !== '-' && item.manufacturer !== 'Unknown' ? item.manufacturer : resolveVendorFromIp(ip);
const baseLabel = item.device_label;
const defaultLabel = baseLabel && baseLabel !== '-' && baseLabel !== 'Unknown' && baseLabel !== 'Generic Client'
? baseLabel
: generateAutoLabel(ip, mac, man, type);
const agentUuid = item.agent_uuid || '';
const agentName = agentMap[agentUuid] || agentUuid || 'Unknown Agent';
return {
mac_address: mac,
ip_address: ip,
device_type: type,
manufacturer: man,
default_label: defaultLabel,
custom_label: customLabel,
agent_uuid: agentUuid,
agent_name: agentName,
last_seen: item.timestamp || new Date()
};
});
res.json({ ok: true, data: result });
} catch (err) {
res.status(500).json({ ok: false, error: err.message });
}
}
module.exports = getLabelingHandler;
@@ -0,0 +1,72 @@
const { DeviceStat, Flow } = require('../../../models/Schemas');
async function getMacDetailsHandler(req, res) {
try {
const isAuthorized = req.user?.role === 'SUPER_ADMIN' ||
req.user?.role === 'EXECUTIVE' ||
req.user?.role === 'TENANT_ADMIN' ||
req.user?.role === 'COMPANY_ADMIN' ||
req.user?.role === 'COMPANY_OPERATOR' ||
req.user?._originalRole === 'SUPER_ADMIN' ||
req.user?._originalRole === 'TENANT_ADMIN';
if (!isAuthorized) {
return res.status(403).json({ ok: false, error: 'Unauthorized: Access denied.' });
}
const { mac } = req.query;
if (!mac) return res.status(400).json({ ok: false, error: 'mac parameter required' });
// Enforce tenant site isolation
const query = { src_mac: mac };
const deviceQuery = { mac_address: mac };
const isGlobalUser = req.user?.role === 'SUPER_ADMIN' ||
req.user?.role === 'EXECUTIVE' ||
((!req.user?.site_uuid || req.user?.site_uuid === 'default') && ['SOC_ANALYST', 'ENGINEER'].includes(req.user?.role)) ||
req.user?._originalRole === 'SUPER_ADMIN';
if (!isGlobalUser && req.user?.site_uuid) {
query.site_uuid = req.user.site_uuid;
deviceQuery.site_uuid = req.user.site_uuid;
}
// 1. Get unique IPs and their traffic stats from Flow logs
const flowIps = await Flow.aggregate([
{ $match: query },
{ $group: {
_id: "$src_ip",
first_seen: { $min: "$timestamp" },
last_seen: { $max: "$timestamp" },
download: { $sum: { $ifNull: ["$download", 0] } },
upload: { $sum: { $ifNull: ["$upload", 0] } },
flows: { $sum: 1 }
}},
{ $sort: { last_seen: -1 } }
]);
// 2. Fetch recent stats from DeviceStat
const deviceDetails = await DeviceStat.find(deviceQuery)
.sort({ timestamp: -1 })
.limit(10)
.lean();
res.json({
ok: true,
mac_address: mac,
ips: flowIps.map(item => ({
ip_address: item._id,
first_seen: item.first_seen,
last_seen: item.last_seen,
download: item.download || 0,
upload: item.upload || 0,
flows: item.flows || 0
})),
deviceDetails: deviceDetails
});
} catch (err) {
res.status(500).json({ ok: false, error: err.message });
}
}
module.exports = getMacDetailsHandler;
@@ -0,0 +1,51 @@
const { CustomDeviceLabel, DeviceStat } = require('../../../models/Schemas');
async function updateLabelHandler(req, res) {
try {
// EXECUTIVE role is read-only — explicitly blocked from writing labels
if (req.user?.role === 'EXECUTIVE') {
return res.status(403).json({ ok: false, error: 'Executive role cannot modify device labels.' });
}
const isAuthorized = req.user?.role === 'SUPER_ADMIN' ||
req.user?.role === 'TENANT_ADMIN' ||
req.user?.role === 'COMPANY_ADMIN' ||
req.user?.role === 'COMPANY_OPERATOR' ||
req.user?._originalRole === 'SUPER_ADMIN' ||
req.user?._originalRole === 'TENANT_ADMIN';
if (!isAuthorized) {
return res.status(403).json({ ok: false, error: 'Only administrators can update device labels.' });
}
const { mac_address, device_label } = req.body;
if (!mac_address) return res.status(400).json({ ok: false, error: 'mac_address required' });
if (device_label === undefined) return res.status(400).json({ ok: false, error: 'device_label required' });
const isGlobalUser = req.user?.role === 'SUPER_ADMIN' ||
((!req.user?.site_uuid || req.user?.site_uuid === 'default') && ['SOC_ANALYST', 'ENGINEER'].includes(req.user?.role)) ||
req.user?._originalRole === 'SUPER_ADMIN';
if (!isGlobalUser && req.user?.site_uuid) {
const deviceExists = await DeviceStat.findOne({
mac_address,
site_uuid: req.user.site_uuid
});
if (!deviceExists) {
return res.status(403).json({ ok: false, error: 'Unauthorized: This device does not belong to your tenant.' });
}
}
await CustomDeviceLabel.findOneAndUpdate(
{ mac_address },
{ device_label },
{ upsert: true, new: true }
);
res.json({ ok: true, message: 'Device label updated successfully' });
} catch (err) {
res.status(500).json({ ok: false, error: err.message });
}
}
module.exports = updateLabelHandler;
+1 -16
View File
@@ -1,16 +1,6 @@
version: '3.8' version: '3.8'
services: services:
mongodb:
image: mongo:6.0
container_name: backone_mongodb_prod
restart: always
# No ports exposed to the host! Completely internal.
volumes:
- mongodb_data_prod:/data/db
environment:
- MONGO_INITDB_DATABASE=backone_dpi
backend: backend:
build: build:
context: ./backend context: ./backend
@@ -18,12 +8,10 @@ services:
restart: always restart: always
# No ports exposed to the host! Completely internal. # No ports exposed to the host! Completely internal.
environment: environment:
- MONGODB_URI=mongodb://mongodb:27017/backone_dpi - MONGODB_URI=${MONGODB_URI}
- BACKEND_PORT=3001 - BACKEND_PORT=3001
env_file: env_file:
- .env.production - .env.production
depends_on:
- mongodb
frontend: frontend:
build: build:
@@ -51,6 +39,3 @@ services:
- ./nginx/conf.d:/etc/nginx/conf.d - ./nginx/conf.d:/etc/nginx/conf.d
depends_on: depends_on:
- frontend - frontend
volumes:
mongodb_data_prod:
+2 -32
View File
@@ -20,26 +20,6 @@ version: '3.8'
services: services:
# ─── Container Group 1: INFRA ─────────────────────────────── # ─── Container Group 1: INFRA ───────────────────────────────
mongodb:
image: mongo:6.0
container_name: backone_mongodb
restart: always
ports:
- "27017:27017"
volumes:
- mongodb_data:/data/db
environment:
- MONGO_INITDB_DATABASE=backone_dpi
networks:
- backone-infra
- backone-app # backend juga bisa connect ke MongoDB
healthcheck:
test: [ "CMD", "mongosh", "--eval", "db.adminCommand('ping')" ]
interval: 30s
timeout: 10s
retries: 5
start_period: 20s
proxy: proxy:
build: build:
context: ./proxy context: ./proxy
@@ -50,7 +30,7 @@ services:
env_file: env_file:
- .env.local - .env.local
environment: environment:
- MONGODB_URI=mongodb://mongodb:27017/backone_dpi - MONGODB_URI=${MONGODB_URI}
- PROXY_PORT=4000 - PROXY_PORT=4000
# Mode 1: kumpulkan SEMUA agent (default) # Mode 1: kumpulkan SEMUA agent (default)
# Ubah ke PROXY_COLLECT_MODE=agent dan isi PROXY_AGENT_UUID untuk mode spesifik # Ubah ke PROXY_COLLECT_MODE=agent dan isi PROXY_AGENT_UUID untuk mode spesifik
@@ -59,9 +39,6 @@ services:
- PROXY_CRON_SCHEDULE=${PROXY_CRON_SCHEDULE:-*/5 * * * *} - PROXY_CRON_SCHEDULE=${PROXY_CRON_SCHEDULE:-*/5 * * * *}
networks: networks:
- backone-infra - backone-infra
depends_on:
mongodb:
condition: service_healthy
# ─── Container Group 2: APP ───────────────────────────────── # ─── Container Group 2: APP ─────────────────────────────────
backend: backend:
@@ -74,14 +51,11 @@ services:
env_file: env_file:
- .env.local - .env.local
environment: environment:
- MONGODB_URI=mongodb://mongodb:27017/backone_dpi - MONGODB_URI=${MONGODB_URI}
- BACKEND_PORT=3001 - BACKEND_PORT=3001
- PROXY_URL=http://proxy:4000 # untuk trigger manual refresh dari dashboard - PROXY_URL=http://proxy:4000 # untuk trigger manual refresh dari dashboard
networks: networks:
- backone-app - backone-app
depends_on:
mongodb:
condition: service_healthy
frontend: frontend:
build: build:
@@ -106,7 +80,3 @@ networks:
backone-app: backone-app:
driver: bridge driver: bridge
name: backone-app name: backone-app
volumes:
mongodb_data:
name: backone_mongodb_data
+5
View File
@@ -120,3 +120,8 @@ under a heading per module/section, matching `plans/next-enhancements.md`.
- **Ad-hoc** Fixed Mobile Sidebar Profile Popover Overflow & Account Settings Modal Layout: Refactored `SidebarProfile.tsx` popover container to open vertically above profile button on mobile (`bottom-full w-full`) and to the right on desktop. Redesigned `AccountSettingsModal.tsx` to render a scrollable horizontal tab bar on mobile (`flex-row overflow-x-auto border-b pb-2.5`) with no-scrollbar styling and responsive active borders (`border-b-2 md:border-l-2`), preventing text truncation ("PASSW") and ensuring content pane visibility. Built, verified, and deployed to production `https://demoplace.my.id`. — shipped 2026-07-30 - **Ad-hoc** Fixed Mobile Sidebar Profile Popover Overflow & Account Settings Modal Layout: Refactored `SidebarProfile.tsx` popover container to open vertically above profile button on mobile (`bottom-full w-full`) and to the right on desktop. Redesigned `AccountSettingsModal.tsx` to render a scrollable horizontal tab bar on mobile (`flex-row overflow-x-auto border-b pb-2.5`) with no-scrollbar styling and responsive active borders (`border-b-2 md:border-l-2`), preventing text truncation ("PASSW") and ensuring content pane visibility. Built, verified, and deployed to production `https://demoplace.my.id`. — shipped 2026-07-30
- **Ad-hoc** Standardized 2-Row Mobile Header Layout Across ALL 17 Dashboard Pages: Restructured page headers across Overview Dashboard, Agents, Apps, Device Labeling, Devices, DNS, DPI Analytics, Events, Flows, Geography, Intelligence, Lookup, Network Infrastructure, Network Intelligence, Security Audit, User Accounts, and Threats to strictly follow the 2-row layout (Row 1: Title + `HelpTrigger`, Row 2: Full-width description paragraph). Verified build (0 TS errors) and deployed to production `https://demoplace.my.id`. — shipped 2026-07-30 - **Ad-hoc** Standardized 2-Row Mobile Header Layout Across ALL 17 Dashboard Pages: Restructured page headers across Overview Dashboard, Agents, Apps, Device Labeling, Devices, DNS, DPI Analytics, Events, Flows, Geography, Intelligence, Lookup, Network Infrastructure, Network Intelligence, Security Audit, User Accounts, and Threats to strictly follow the 2-row layout (Row 1: Title + `HelpTrigger`, Row 2: Full-width description paragraph). Verified build (0 TS errors) and deployed to production `https://demoplace.my.id`. — shipped 2026-07-30
## Database Configuration
- **Ad-hoc** Configured Local Development Environment to Use Central Database Directly: (1) Removed `node scripts/start-mongo.js` execution from the `"dev"` script in `package.json` to prevent starting a local in-memory database, (2) Removed `mongodb` service definitions and local volumes from `docker-compose.yml` and `docker-compose.prod.yml`, (3) Wired `MONGODB_URI` environment variables directly from `.env.local` and `.env.production` into Docker services, (4) Hardened connection logic in `backend/db/mongoose.js`, `proxy/index.js`, `src/lib/actions/agents.ts`, and `test/multitenant_branding_test.js` to immediately fail with a critical error and exit if `MONGODB_URI` is undefined, preventing any accidental fallback to `127.0.0.1:27017` or localhost databases, (5) Adjusted assertions in `test/architecture_test.js` and queries in `test/multitenant_branding_test.js` (checking `agent_registry` instead of empty `summaries` collection) to keep TDD tests passing 100% against the central database. — shipped 2026-08-24
@@ -0,0 +1,37 @@
# Iteration Log: Fix Blank Screen Bug on Production (demoplace.my.id)
**Date & Time**: 2026-07-31 11:50 WIB
**Trigger**: User report ("eror atau bug atau crush kenapa ini" with screenshot showing blank white screen on demoplace.my.id)
## 1. Problem Description & Symptoms
- User reported a completely blank white screen when opening `https://demoplace.my.id`.
- Accessing `https://demoplace.my.id` returned HTTP 307 redirecting to `/login`.
- Accessing `https://demoplace.my.id/login` returned `HTTP 500 Internal Server Error` with `Content-Length: 0`.
## 2. Diagnostics & Root Cause Analysis
1. **First Root Cause (API Proxy Backend Port Mismatch)**:
- In `src/app/api/[...route]/route.ts`, fallback port for `BACKEND_PORT` was set to `3002`:
`const backendPort = process.env.BACKEND_PORT || '3002';`
- Express backend in production runs on port **`3001`**.
- When Next.js tried to proxy `/api/auth/login` to `127.0.0.1:3002`, it threw `ECONNREFUSED 127.0.0.1:3002`.
2. **Second Root Cause (PHP 7 Compatibility in Web Server Reverse Proxy)**:
- In Apache/HestiaCP reverse proxy file `public_html/proxy.php`, function `str_starts_with()` (PHP 8+) and `getallheaders()` (mod_php only) were called.
- The production server PHP version (PHP 7.x FastCGI) threw a PHP Fatal Error:
`PHP Fatal error: Uncaught Error: Call to undefined function str_starts_with() in /home/adminbackend/web/demoplace.my.id/public_html/proxy.php`
- Apache returned 500 Internal Server Error with an empty 0-byte body.
## 3. Steps Taken
- Fixed `src/app/api/[...route]/route.ts` fallback port to `3001`.
- Rebuilt production bundle locally with `npm run build`.
- Deployed updated `.next/standalone`, `.next/static`, and `public` assets to server using `scripts/force-deploy-frontend.js`.
- Fixed `proxy.php` and `index.php` in `scripts/fix-proxy-php.js` to:
- Add polyfill for `getallheaders()`.
- Replace `str_starts_with()` with PHP 7 compatible `stripos($str, $pattern) === 0`.
- Handle `Location:` header rewriting to prevent exposing internal `http://127.0.0.1:3000` URLs.
- Executed `node scripts/fix-proxy-php.js` via SSH.
## 4. Verification & Outcome
- Executed `curl.exe -i -k -L https://demoplace.my.id`.
- Server returned `HTTP 200 OK` rendering full BackOne Dashboard HTML layout, stylesheets (`/_next/static/chunks/*.css`), fonts (`Inter_Variable.woff2`, `BackOneLogo-Regular.ttf`), and React client bundles.
- Verified live web app is now 100% operational and displaying properly.
@@ -0,0 +1,33 @@
# Iteration Log - 2026-08-03-0955-adhoc-deploy-labeling-bugfix
## Request & Context
* **Requested**: Fix the access failure of the "Device Labeling" page on the domain `demoplace.my.id` and localhost.
* **Scope**: `/device-labeling` frontend page and backend API routing for device labeling.
## Steps Taken
1. **React Hooks Bug Fix**:
* Moved the `isExportingPdf` state hook in [page.tsx](file:///c:/z_Siregar/Magang%20DBS/1.%20BackOne-DPI/DPI-Source%20API%20Netify/src/app/%28dashboard%29/device-labeling/page.tsx) above the `if (!mounted) return null;` conditional return guard to comply with the Rules of Hooks and prevent runtime hydration crash in production.
2. **Backend Modularization & RBAC Fix (Rule 3 Compliance)**:
* To adhere to the 256-line file limit, refactored [deviceLabeling.js](file:///c:/z_Siregar/Magang%20DBS/1.%20BackOne-DPI/DPI-Source%20API%20Netify/backend/routes/dashboard/deviceLabeling.js) (268 lines originally) by splitting its handlers into modular files:
* [updateLabel.js](file:///c:/z_Siregar/Magang%20DBS/1.%20BackOne-DPI/DPI-Source%20API%20Netify/backend/routes/dashboard/deviceLabeling/updateLabel.js)
* [getLabeling.js](file:///c:/z_Siregar/Magang%20DBS/1.%20BackOne-DPI/DPI-Source%20API%20Netify/backend/routes/dashboard/deviceLabeling/getLabeling.js)
* [getMacDetails.js](file:///c:/z_Siregar/Magang%20DBS/1.%20BackOne-DPI/DPI-Source%20API%20Netify/backend/routes/dashboard/deviceLabeling/getMacDetails.js)
* Shrank the main router [deviceLabeling.js](file:///c:/z_Siregar/Magang%20DBS/1.%20BackOne-DPI/DPI-Source%20API%20Netify/backend/routes/dashboard/deviceLabeling.js) to just 23 lines.
* Fixed import path issues in the modular handlers (corrected `../../models/Schemas` to `../../../models/Schemas`) which initially caused `MODULE_NOT_FOUND` error and crash on `backone-backend`.
* Aligned RBAC in all three handlers to allow access for both `COMPANY_ADMIN` and `COMPANY_OPERATOR` roles.
3. **Static Folder Nesting Bug Fix**:
* Fixed a Linux command bug in [deploy-sftp.js](file:///c:/z_Siregar/Magang%20DBS/1.%20BackOne-DPI/DPI-Source%20API%20Netify/scripts/deploy-sftp.js) where `cp -r` would nest folders into `.next/standalone/.next/static/static/` if the target directory already existed.
* Cleaned up the nested layout, ensured the fresh build chunks are directly under `.next/standalone/.next/static/`, and restarted `backone-frontend`.
4. **Build & Deployment**:
* Executed `npm run build` locally to verify build integrity (Compiled successfully, TS checks passed).
* Increased `readyTimeout` in [deploy-sftp.js](file:///c:/z_Siregar/Magang%20DBS/1.%20BackOne-DPI/DPI-Source%20API%20Netify/scripts/deploy-sftp.js) to `90000ms` for robust network connections on slow remote routes.
* Run the production deployment script `deploy-sftp.js` to transfer build assets and restart PM2 services.
* Confirmed backend is fully healthy with verification output `{"ok":true,"message":"BackOne Backend berjalan..."}`.
## Outcome
* **Localhost**: Verified 100% operational with correct page layout rendering.
* **Production**: SFTP upload successfully transferred 33 items, PM2 reloaded all apps (`backone-proxy`, `backone-backend`, `backone-frontend`) cleanly.
## Considerations for Next Time
* Always put Next.js state hooks strictly at the top of functional components.
* Ensure any file modifications do not cross the 256-line mark without modularization.
+30
View File
@@ -0,0 +1,30 @@
# Iteration Log — 2026-08-24-1428-adhoc
## Request
Konfigurasi proyek lokal agar langsung menggunakan database pusat `backone_dpi` dan menonaktifkan database lokal secara permanen di komputer lokal. Dilarang menghapus/mereset data database pusat kecuali data yang berumur lebih dari 30 hari. Ketika deploy ke domain, database tidak dideploy karena sudah langsung tersambung ke database pusat.
## Steps Taken
1. **Analisis Konfigurasi Koneksi Database**:
- Memeriksa file `.env.local` dan `.env.production` di root proyek. Keduanya sudah terisi dengan `MONGODB_URI` ke database pusat: `mongodb://backone_user:SusuKudaLiar@103.80.237.29:27017/backone_dpi?authSource=backone_dpi`.
- Mengidentifikasi bahwa saat lokal dijalankan menggunakan `npm run dev`, script `scripts/start-mongo.js` secara otomatis dijalankan untuk memicu MongoDB in-memory lokal pada port `27017`.
- Mengidentifikasi bahwa file `docker-compose.yml` dan `docker-compose.prod.yml` mendefinisikan service `mongodb` kontainer lokal dengan URI hardcoded `mongodb://mongodb:27017/backone_dpi`.
2. **Perubahan Konfigurasi**:
- Memodifikasi `package.json` untuk menghapus pemanggilan `node scripts/start-mongo.js` di script `"dev"`.
- Mengeluarkan service `mongodb` lokal dan volume data terkait dari `docker-compose.yml` dan `docker-compose.prod.yml`.
- Mengubah `MONGODB_URI` pada `proxy` dan `backend` di Docker Compose untuk menunjuk ke `${MONGODB_URI}` agar membaca environment variables asli.
3. **Pencegahan Fallback ke Database Lokal**:
- Memperbarui `backend/db/mongoose.js`, `proxy/index.js`, `src/lib/actions/agents.ts`, dan `test/multitenant_branding_test.js` untuk melempar error kritis dan menghentikan eksekusi jika `MONGODB_URI` tidak dikonfigurasi di environment, melarang fallback otomatis ke `127.0.0.1:27017`.
4. **Penyesuaian Test & TDD**:
- Memperbarui `test/architecture_test.js` agar memverifikasi arsitektur baru (tanpa service `mongodb` lokal dan dependensinya).
- Memperbarui `test/multitenant_branding_test.js` agar melakukan query ketersediaan agent ke collection `agent_registry` menggunakan field `uuid`, alih-alih collection `summaries` yang kosong pada database pusat.
- Menjalankan unit test arsitektur dan branding: keduanya lolos dengan hasil **48/48 PASSED** dan **23/23 PASSED**.
## Outcome
- **Success**: Seluruh unit test berjalan dengan sukses dan proyek lokal berhasil terisolasi sepenuhnya dari database lokal. Seluruh koneksi lokal diarahkan langsung ke database pusat `backone_dpi`.
- **Database Safety**: Tidak ada data database pusat yang terhapus atau terganggu. Kebijakan pembersihan telemetry 30 hari tetap terjaga dengan aman.
## Considerations for Next Time
- Seluruh pengujian otomatis dan jalannya aplikasi lokal saat ini bergantung pada konektivitas ke database pusat `103.80.237.29:27017`. Pastikan koneksi internet aktif dan IP database pusat dapat dijangkau sebelum menjalankan tes/aplikasi.
+502 -7
View File
@@ -1,13 +1,15 @@
{ {
"name": "netify-app", "name": "backone-dpi",
"version": "0.1.0", "version": "1.0.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "netify-app", "name": "backone-dpi",
"version": "0.1.0", "version": "1.0.0",
"dependencies": { "dependencies": {
"@react-pdf/renderer": "^4.5.1",
"@types/leaflet": "^1.9.21",
"axios": "^1.18.1", "axios": "^1.18.1",
"bcryptjs": "^3.0.3", "bcryptjs": "^3.0.3",
"better-sqlite3": "^12.11.1", "better-sqlite3": "^12.11.1",
@@ -21,6 +23,7 @@
"express": "^5.2.1", "express": "^5.2.1",
"framer-motion": "^12.42.2", "framer-motion": "^12.42.2",
"jsonwebtoken": "^9.0.3", "jsonwebtoken": "^9.0.3",
"leaflet": "^1.9.4",
"lucide-react": "^1.21.0", "lucide-react": "^1.21.0",
"mongodb-memory-server": "^11.2.0", "mongodb-memory-server": "^11.2.0",
"mongoose": "^9.7.4", "mongoose": "^9.7.4",
@@ -55,6 +58,9 @@
"ssh2-sftp-client": "^12.1.1", "ssh2-sftp-client": "^12.1.1",
"tailwindcss": "^4", "tailwindcss": "^4",
"typescript": "^5" "typescript": "^5"
},
"engines": {
"node": ">=18.0.0"
} }
}, },
"node_modules/@alloc/quick-lru": { "node_modules/@alloc/quick-lru": {
@@ -1257,6 +1263,30 @@
"node": ">= 10" "node": ">= 10"
} }
}, },
"node_modules/@noble/ciphers": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.3.0.tgz",
"integrity": "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==",
"license": "MIT",
"engines": {
"node": "^14.21.3 || >=16"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@noble/hashes": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz",
"integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==",
"license": "MIT",
"engines": {
"node": "^14.21.3 || >=16"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@nodelib/fs.scandir": { "node_modules/@nodelib/fs.scandir": {
"version": "2.1.5", "version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@@ -1305,6 +1335,183 @@
"node": ">=12.4.0" "node": ">=12.4.0"
} }
}, },
"node_modules/@react-pdf/fns": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/@react-pdf/fns/-/fns-3.1.3.tgz",
"integrity": "sha512-0I7pApDr1/RLAKbizuLy/IHTEa93LSPy/bEwYniboC3Xqnp6Od8xFJKbKEzGw2wh/5zKFFwl00g4t9RwgIMc3w==",
"license": "MIT"
},
"node_modules/@react-pdf/font": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/@react-pdf/font/-/font-4.0.8.tgz",
"integrity": "sha512-deNd+emtZAJho1IlzKL9bRoLAGv/6oXOIKO2oZfs4RuXUrK1onLHbJO7e2YoVLPFP/sQxisRTnzdJFtd35iKwA==",
"license": "MIT",
"dependencies": {
"@react-pdf/pdfkit": "^5.1.1",
"@react-pdf/types": "^2.11.1",
"fontkit": "^2.0.2",
"is-url": "^1.2.4"
}
},
"node_modules/@react-pdf/image": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/@react-pdf/image/-/image-3.1.0.tgz",
"integrity": "sha512-ks7Ry8v711r8NvKWSELehj0BXBNPRihSnWsM09nDD8Ur175zbWBCK217LLwQMKDNYDVpkZaipdoJPom1LGaE9g==",
"license": "MIT",
"dependencies": {
"@react-pdf/svg": "^1.1.0",
"jay-peg": "^1.1.1",
"png-js": "^2.0.0"
}
},
"node_modules/@react-pdf/layout": {
"version": "4.6.1",
"resolved": "https://registry.npmjs.org/@react-pdf/layout/-/layout-4.6.1.tgz",
"integrity": "sha512-gN6PmWoEffvlIkifLfEhMsVucRywVMyH3rnxdyOVOhGy0nWJKKGpHyPc4plbDdpP6EfZ0r8prHXujDSkIG2nSA==",
"license": "MIT",
"dependencies": {
"@react-pdf/fns": "3.1.3",
"@react-pdf/image": "^3.1.0",
"@react-pdf/primitives": "^4.3.0",
"@react-pdf/stylesheet": "^6.2.1",
"@react-pdf/textkit": "^6.3.0",
"@react-pdf/types": "^2.11.1",
"emoji-regex-xs": "^1.0.0",
"queue": "^6.0.1",
"yoga-layout": "^3.2.1"
}
},
"node_modules/@react-pdf/pdfkit": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/@react-pdf/pdfkit/-/pdfkit-5.1.1.tgz",
"integrity": "sha512-wNcdSsNlNYyGHGAgIdt453egBF7fiF9UxpRlklUfVvu8OWCrUppG9xiUrPLVoKiqWet5tMi0w6LmuFUJuYqjEg==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.20.13",
"@noble/ciphers": "^1.0.0",
"@noble/hashes": "^1.6.0",
"browserify-zlib": "^0.2.0",
"fontkit": "^2.0.2",
"jay-peg": "^1.1.1",
"js-md5": "^0.8.3",
"linebreak": "^1.1.0",
"png-js": "^2.0.0",
"vite-compatible-readable-stream": "^3.6.1"
}
},
"node_modules/@react-pdf/primitives": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/@react-pdf/primitives/-/primitives-4.3.0.tgz",
"integrity": "sha512-nYXoZ36pvwNzbc54+DbL8RCn15jU7woJ9D/svnh5tpUXekJ+CbI4mZLo6boSv24CvJgychOu6h7gxX03B4ps0A==",
"license": "MIT"
},
"node_modules/@react-pdf/reconciler": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@react-pdf/reconciler/-/reconciler-2.0.0.tgz",
"integrity": "sha512-7zaPRujpbHSmCpIrZ+b9HSTJHthcVZzX0Wx7RzvQGsGBUbHP4p6s5itXrAIOuQuPvDepoHGNOvf6xUuMVvdoyw==",
"license": "MIT",
"dependencies": {
"object-assign": "^4.1.1",
"scheduler": "0.25.0-rc-603e6108-20241029"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/@react-pdf/reconciler/node_modules/scheduler": {
"version": "0.25.0-rc-603e6108-20241029",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.25.0-rc-603e6108-20241029.tgz",
"integrity": "sha512-pFwF6H1XrSdYYNLfOcGlM28/j8CGLu8IvdrxqhjWULe2bPcKiKW4CV+OWqR/9fT52mywx65l7ysNkjLKBda7eA==",
"license": "MIT"
},
"node_modules/@react-pdf/render": {
"version": "4.5.1",
"resolved": "https://registry.npmjs.org/@react-pdf/render/-/render-4.5.1.tgz",
"integrity": "sha512-IW/N4HWJWtioBXCf7n02IR24VJJ8gbdS3jGypf+vW/rSErEx3/URRzh9UK6Ma8Fpog9+T/W6GE2NHJ5AAKHhVA==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.20.13",
"@react-pdf/fns": "3.1.3",
"@react-pdf/primitives": "^4.3.0",
"@react-pdf/textkit": "^6.3.0",
"@react-pdf/types": "^2.11.1",
"abs-svg-path": "^0.1.1",
"color-string": "^2.1.4",
"normalize-svg-path": "^1.1.0",
"parse-svg-path": "^0.1.2",
"svg-arc-to-cubic-bezier": "^3.2.0"
}
},
"node_modules/@react-pdf/renderer": {
"version": "4.5.1",
"resolved": "https://registry.npmjs.org/@react-pdf/renderer/-/renderer-4.5.1.tgz",
"integrity": "sha512-5r1VQrE6FRLXX5wWUxwZzM24E2BJMo6g8AQWuS8WyPs9ugu5yMnb2g8/RpPYka/Z6J+RUEWc32wty2NoUJF42Q==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.20.13",
"@react-pdf/fns": "3.1.3",
"@react-pdf/font": "^4.0.8",
"@react-pdf/layout": "^4.6.1",
"@react-pdf/pdfkit": "^5.1.1",
"@react-pdf/primitives": "^4.3.0",
"@react-pdf/reconciler": "^2.0.0",
"@react-pdf/render": "^4.5.1",
"@react-pdf/types": "^2.11.1",
"events": "^3.3.0",
"object-assign": "^4.1.1",
"prop-types": "^15.6.2",
"queue": "^6.0.1"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/@react-pdf/stylesheet": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/@react-pdf/stylesheet/-/stylesheet-6.2.1.tgz",
"integrity": "sha512-2+UEk+7e+z8baaWi2l5kPLWmwtJeOI+T5wW9GGeN3iDH7vd3kbTqOpN1yt9mmfNVZFxQsnDHpznFb5v5UF983A==",
"license": "MIT",
"dependencies": {
"@react-pdf/fns": "3.1.3",
"@react-pdf/types": "^2.11.1",
"color-string": "^2.1.4",
"hsl-to-hex": "^1.0.0",
"media-engine": "^1.0.3",
"postcss-value-parser": "^4.1.0"
}
},
"node_modules/@react-pdf/svg": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@react-pdf/svg/-/svg-1.1.0.tgz",
"integrity": "sha512-cTIHXiz9x1HrbfqzfxfZP3FRdDwUXG77QWF6Fb5MP/lV3ONxR+g0Z3hwtBatCS9HeGBQCpxX/Lzb8wHE+co1PA==",
"license": "MIT",
"dependencies": {
"@react-pdf/primitives": "^4.3.0"
}
},
"node_modules/@react-pdf/textkit": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/@react-pdf/textkit/-/textkit-6.3.0.tgz",
"integrity": "sha512-v6+V8nAcVwm7s2s1jIG2MD3Iw//x/k+XrH1foWOELBE4b32pyDgKyPXN/6KJE0dnX7+fVy27uctLNCLNMvzKzQ==",
"license": "MIT",
"dependencies": {
"@react-pdf/fns": "3.1.3",
"bidi-js": "^1.0.2",
"hyphen": "^1.6.4",
"unicode-properties": "^1.4.1"
}
},
"node_modules/@react-pdf/types": {
"version": "2.11.1",
"resolved": "https://registry.npmjs.org/@react-pdf/types/-/types-2.11.1.tgz",
"integrity": "sha512-i9xQgfaDU9QoeNnbp6rltXCWg1huEh195rpOuN8cE4BZ2FuLdQrsIcb2dhFF9aOxXf+XBA6LOSpIW051MDD/bw==",
"license": "MIT",
"dependencies": {
"@react-pdf/font": "^4.0.8",
"@react-pdf/primitives": "^4.3.0",
"@react-pdf/stylesheet": "^6.2.1"
}
},
"node_modules/@reduxjs/toolkit": { "node_modules/@reduxjs/toolkit": {
"version": "2.12.0", "version": "2.12.0",
"resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.12.0.tgz", "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.12.0.tgz",
@@ -1860,6 +2067,15 @@
"@types/node": "*" "@types/node": "*"
} }
}, },
"node_modules/@types/leaflet": {
"version": "1.9.22",
"resolved": "https://registry.npmjs.org/@types/leaflet/-/leaflet-1.9.22.tgz",
"integrity": "sha512-h3lhECYEKDasG7LFHu+GiHqAvsgLuQvlJvVZzJDGONo3sEL+wUOqSFLnwkZlK0qVxnxbuGFW8iBlJNYs5wgndA==",
"license": "MIT",
"dependencies": {
"@types/geojson": "*"
}
},
"node_modules/@types/ms": { "node_modules/@types/ms": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz",
@@ -2599,6 +2815,12 @@
"node": ">=6.5" "node": ">=6.5"
} }
}, },
"node_modules/abs-svg-path": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/abs-svg-path/-/abs-svg-path-0.1.1.tgz",
"integrity": "sha512-d8XPSGjfyzlXC3Xx891DJRyZfqk5JU0BJrDQcsWomFIV1/BIzPW5HDH5iDdWpqWaav0YVIEzT1RHTwWr0FFshA==",
"license": "MIT"
},
"node_modules/accepts": { "node_modules/accepts": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
@@ -3235,6 +3457,15 @@
"node": "20.x || 22.x || 23.x || 24.x || 25.x || 26.x" "node": "20.x || 22.x || 23.x || 24.x || 25.x || 26.x"
} }
}, },
"node_modules/bidi-js": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz",
"integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==",
"license": "MIT",
"dependencies": {
"require-from-string": "^2.0.2"
}
},
"node_modules/bindings": { "node_modules/bindings": {
"version": "1.5.0", "version": "1.5.0",
"resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
@@ -3316,6 +3547,24 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/brotli": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/brotli/-/brotli-1.3.3.tgz",
"integrity": "sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg==",
"license": "MIT",
"dependencies": {
"base64-js": "^1.1.2"
}
},
"node_modules/browserify-zlib": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.2.0.tgz",
"integrity": "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==",
"license": "MIT",
"dependencies": {
"pako": "~1.0.5"
}
},
"node_modules/browserslist": { "node_modules/browserslist": {
"version": "4.28.4", "version": "4.28.4",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.4.tgz", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.4.tgz",
@@ -3568,6 +3817,15 @@
"node": ">=20" "node": ">=20"
} }
}, },
"node_modules/clone": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz",
"integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==",
"license": "MIT",
"engines": {
"node": ">=0.8"
}
},
"node_modules/clsx": { "node_modules/clsx": {
"version": "2.1.1", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
@@ -3597,6 +3855,27 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/color-string": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/color-string/-/color-string-2.1.4.tgz",
"integrity": "sha512-Bb6Cq8oq0IjDOe8wJmi4JeNn763Xs9cfrBcaylK1tPypWzyoy2G3l90v9k64kjphl/ZJjPIShFztenRomi8WTg==",
"license": "MIT",
"dependencies": {
"color-name": "^2.0.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/color-string/node_modules/color-name": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-2.1.1.tgz",
"integrity": "sha512-p2FdgwVx1a9yWBHP2wI0VgShkDpgN4kZISkxdNipGBJWpa5G6b04OINlVWCyJj0JmfvcPrgqt95E9k8yvaOJFg==",
"license": "MIT",
"engines": {
"node": ">=12.20"
}
},
"node_modules/combined-stream": { "node_modules/combined-stream": {
"version": "1.0.8", "version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
@@ -4476,6 +4755,12 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/dfa": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/dfa/-/dfa-1.2.0.tgz",
"integrity": "sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q==",
"license": "MIT"
},
"node_modules/doctrine": { "node_modules/doctrine": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",
@@ -4550,6 +4835,12 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/emoji-regex-xs": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex-xs/-/emoji-regex-xs-1.0.0.tgz",
"integrity": "sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg==",
"license": "MIT"
},
"node_modules/encodeurl": { "node_modules/encodeurl": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
@@ -5250,7 +5541,6 @@
"version": "3.3.0", "version": "3.3.0",
"resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
"integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=0.8.x" "node": ">=0.8.x"
@@ -5330,7 +5620,6 @@
"version": "3.1.3", "version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/fast-fifo": { "node_modules/fast-fifo": {
@@ -5397,7 +5686,6 @@
"version": "0.8.3", "version": "0.8.3",
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.3.tgz", "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.3.tgz",
"integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==", "integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/file-entry-cache": { "node_modules/file-entry-cache": {
@@ -5542,6 +5830,23 @@
} }
} }
}, },
"node_modules/fontkit": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/fontkit/-/fontkit-2.0.4.tgz",
"integrity": "sha512-syetQadaUEDNdxdugga9CpEYVaQIxOwk7GlwZWWZ19//qW4zE5bknOKeMBDYAASwnpaSHKJITRLMF9m1fp3s6g==",
"license": "MIT",
"dependencies": {
"@swc/helpers": "^0.5.12",
"brotli": "^1.3.2",
"clone": "^2.1.2",
"dfa": "^1.2.0",
"fast-deep-equal": "^3.1.3",
"restructure": "^3.0.0",
"tiny-inflate": "^1.0.3",
"unicode-properties": "^1.4.0",
"unicode-trie": "^2.0.0"
}
},
"node_modules/for-each": { "node_modules/for-each": {
"version": "0.3.5", "version": "0.3.5",
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz",
@@ -6011,6 +6316,21 @@
"hermes-estree": "0.25.1" "hermes-estree": "0.25.1"
} }
}, },
"node_modules/hsl-to-hex": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/hsl-to-hex/-/hsl-to-hex-1.0.0.tgz",
"integrity": "sha512-K6GVpucS5wFf44X0h2bLVRDsycgJmf9FF2elg+CrqD8GcFU8c6vYhgXn8NjUkFCwj+xDFb70qgLbTUm6sxwPmA==",
"license": "MIT",
"dependencies": {
"hsl-to-rgb-for-reals": "^1.1.0"
}
},
"node_modules/hsl-to-rgb-for-reals": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/hsl-to-rgb-for-reals/-/hsl-to-rgb-for-reals-1.1.1.tgz",
"integrity": "sha512-LgOWAkrN0rFaQpfdWBQlv/VhkOxb5AsBjk6NQVx4yEzWS923T07X0M1Y0VNko2H52HeSpZrZNNMJ0aFqsdVzQg==",
"license": "ISC"
},
"node_modules/http-errors": { "node_modules/http-errors": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
@@ -6044,6 +6364,12 @@
"node": ">= 6" "node": ">= 6"
} }
}, },
"node_modules/hyphen": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/hyphen/-/hyphen-1.14.1.tgz",
"integrity": "sha512-kvL8xYl5QMTh+LwohVN72ciOxC0OEV79IPdJSTwEXok9y9QHebXGdFgrED4sWfiax/ODx++CAMk3hMy4XPJPOw==",
"license": "ISC"
},
"node_modules/iconv-lite": { "node_modules/iconv-lite": {
"version": "0.7.2", "version": "0.7.2",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz",
@@ -6585,6 +6911,12 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/is-url": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/is-url/-/is-url-1.2.4.tgz",
"integrity": "sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==",
"license": "MIT"
},
"node_modules/is-weakmap": { "node_modules/is-weakmap": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz",
@@ -6663,6 +6995,15 @@
"node": ">= 0.4" "node": ">= 0.4"
} }
}, },
"node_modules/jay-peg": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/jay-peg/-/jay-peg-1.1.1.tgz",
"integrity": "sha512-D62KEuBxz/ip2gQKOEhk/mx14o7eiFRaU+VNNSP4MOiIkwb/D6B3G1Mfas7C/Fit8EsSV2/IWjZElx/Gs6A4ww==",
"license": "MIT",
"dependencies": {
"restructure": "^3.0.0"
}
},
"node_modules/jerrypick": { "node_modules/jerrypick": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/jerrypick/-/jerrypick-1.1.2.tgz", "resolved": "https://registry.npmjs.org/jerrypick/-/jerrypick-1.1.2.tgz",
@@ -6682,6 +7023,12 @@
"jiti": "lib/jiti-cli.mjs" "jiti": "lib/jiti-cli.mjs"
} }
}, },
"node_modules/js-md5": {
"version": "0.8.3",
"resolved": "https://registry.npmjs.org/js-md5/-/js-md5-0.8.3.tgz",
"integrity": "sha512-qR0HB5uP6wCuRMrWPTrkMaev7MJZwJuuw4fnwAzRgP4J4/F8RwtodOKpGp4XpqsLBFzzgqIO42efFAyz2Et6KQ==",
"license": "MIT"
},
"node_modules/js-tokens": { "node_modules/js-tokens": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@@ -6933,6 +7280,12 @@
"safe-buffer": "~5.1.0" "safe-buffer": "~5.1.0"
} }
}, },
"node_modules/leaflet": {
"version": "1.9.4",
"resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz",
"integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==",
"license": "BSD-2-Clause"
},
"node_modules/levn": { "node_modules/levn": {
"version": "0.4.1", "version": "0.4.1",
"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
@@ -7208,6 +7561,25 @@
"url": "https://opencollective.com/parcel" "url": "https://opencollective.com/parcel"
} }
}, },
"node_modules/linebreak": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/linebreak/-/linebreak-1.1.0.tgz",
"integrity": "sha512-MHp03UImeVhB7XZtjd0E4n6+3xr5Dq/9xI/5FptGk5FrbDR3zagPa2DS6U8ks/3HjbKWG9Q1M2ufOzxV2qLYSQ==",
"license": "MIT",
"dependencies": {
"base64-js": "0.0.8",
"unicode-trie": "^2.0.0"
}
},
"node_modules/linebreak/node_modules/base64-js": {
"version": "0.0.8",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-0.0.8.tgz",
"integrity": "sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/locate-path": { "node_modules/locate-path": {
"version": "6.0.0", "version": "6.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
@@ -7344,6 +7716,12 @@
"node": ">= 0.4" "node": ">= 0.4"
} }
}, },
"node_modules/media-engine": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/media-engine/-/media-engine-1.0.3.tgz",
"integrity": "sha512-aa5tG6sDoK+k70B9iEX1NeyfT8ObCKhNDs6lJVpwF6r8vhUfuKMslIcirq6HIUYuuUYLefcEQOn9bSBOvawtwg==",
"license": "MIT"
},
"node_modules/media-typer": { "node_modules/media-typer": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz",
@@ -7971,6 +8349,15 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/normalize-svg-path": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/normalize-svg-path/-/normalize-svg-path-1.1.0.tgz",
"integrity": "sha512-r9KHKG2UUeB5LoTouwDzBy2VxXlHsiM6fyLQvnJa0S5hrhzqElH/CH7TUGhT1fVvIYBIKf3OpY4YJ4CK+iaqHg==",
"license": "MIT",
"dependencies": {
"svg-arc-to-cubic-bezier": "^3.0.0"
}
},
"node_modules/object-assign": { "node_modules/object-assign": {
"version": "4.1.1", "version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
@@ -8190,6 +8577,12 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/pako": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
"license": "(MIT AND Zlib)"
},
"node_modules/parent-module": { "node_modules/parent-module": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
@@ -8203,6 +8596,12 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/parse-svg-path": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/parse-svg-path/-/parse-svg-path-0.1.2.tgz",
"integrity": "sha512-JyPSBnkTJ0AI8GGJLfMXvKq42cj5c006fnLz6fXy6zfoVjJizi8BNTpu8on8ziI1cKy9d9DGNuY17Ce7wuejpQ==",
"license": "MIT"
},
"node_modules/parseurl": { "node_modules/parseurl": {
"version": "1.3.3", "version": "1.3.3",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
@@ -8337,6 +8736,14 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/png-js": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/png-js/-/png-js-2.0.0.tgz",
"integrity": "sha512-GdzJuUMc6ZSpxFJWVxtOH1bzYHym+TOnveqUjb+VJIbZWbZzyiRGFiKhbiielfpYbgMlhHVhsJ0FTazfuRFkMA==",
"dependencies": {
"fflate": "^0.8.2"
}
},
"node_modules/point-in-polygon-hao": { "node_modules/point-in-polygon-hao": {
"version": "1.2.4", "version": "1.2.4",
"resolved": "https://registry.npmjs.org/point-in-polygon-hao/-/point-in-polygon-hao-1.2.4.tgz", "resolved": "https://registry.npmjs.org/point-in-polygon-hao/-/point-in-polygon-hao-1.2.4.tgz",
@@ -8397,6 +8804,12 @@
"node": "^10 || ^12 || >=14" "node": "^10 || ^12 || >=14"
} }
}, },
"node_modules/postcss-value-parser": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
"license": "MIT"
},
"node_modules/preact": { "node_modules/preact": {
"version": "10.29.3", "version": "10.29.3",
"resolved": "https://registry.npmjs.org/preact/-/preact-10.29.3.tgz", "resolved": "https://registry.npmjs.org/preact/-/preact-10.29.3.tgz",
@@ -8529,6 +8942,15 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/queue": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/queue/-/queue-6.0.2.tgz",
"integrity": "sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==",
"license": "MIT",
"dependencies": {
"inherits": "~2.0.3"
}
},
"node_modules/queue-microtask": { "node_modules/queue-microtask": {
"version": "1.2.3", "version": "1.2.3",
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
@@ -8859,6 +9281,15 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/require-from-string": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
"integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/reselect": { "node_modules/reselect": {
"version": "5.2.0", "version": "5.2.0",
"resolved": "https://registry.npmjs.org/reselect/-/reselect-5.2.0.tgz", "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.2.0.tgz",
@@ -8909,6 +9340,12 @@
"url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
} }
}, },
"node_modules/restructure": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/restructure/-/restructure-3.0.2.tgz",
"integrity": "sha512-gSfoiOEA0VPE6Tukkrr7I0RBdE0s7H1eFCDBk05l1KIQT1UIKNc5JZy6jdyW6eYH3aR3g5b3PuL77rq0hvwtAw==",
"license": "MIT"
},
"node_modules/reusify": { "node_modules/reusify": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
@@ -9729,6 +10166,12 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/svg-arc-to-cubic-bezier": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/svg-arc-to-cubic-bezier/-/svg-arc-to-cubic-bezier-3.2.0.tgz",
"integrity": "sha512-djbJ/vZKZO+gPoSDThGNpKDO+o+bAeA4XQKovvkNCqnIS2t+S4qnLAGQhyyrulhCFRl1WWzAp0wUDV8PpTVU3g==",
"license": "ISC"
},
"node_modules/swr": { "node_modules/swr": {
"version": "2.4.2", "version": "2.4.2",
"resolved": "https://registry.npmjs.org/swr/-/swr-2.4.2.tgz", "resolved": "https://registry.npmjs.org/swr/-/swr-2.4.2.tgz",
@@ -9942,6 +10385,12 @@
"three": ">=0.154" "three": ">=0.154"
} }
}, },
"node_modules/tiny-inflate": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz",
"integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==",
"license": "MIT"
},
"node_modules/tiny-invariant": { "node_modules/tiny-invariant": {
"version": "1.3.3", "version": "1.3.3",
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
@@ -10315,6 +10764,32 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/unicode-properties": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/unicode-properties/-/unicode-properties-1.4.1.tgz",
"integrity": "sha512-CLjCCLQ6UuMxWnbIylkisbRj31qxHPAurvena/0iwSVbQ2G1VY5/HjV0IRabOEbDHlzZlRdCrD4NhB0JtU40Pg==",
"license": "MIT",
"dependencies": {
"base64-js": "^1.3.0",
"unicode-trie": "^2.0.0"
}
},
"node_modules/unicode-trie": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/unicode-trie/-/unicode-trie-2.0.0.tgz",
"integrity": "sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==",
"license": "MIT",
"dependencies": {
"pako": "^0.2.5",
"tiny-inflate": "^1.0.0"
}
},
"node_modules/unicode-trie/node_modules/pako": {
"version": "0.2.9",
"resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz",
"integrity": "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==",
"license": "MIT"
},
"node_modules/unpipe": { "node_modules/unpipe": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
@@ -10449,6 +10924,20 @@
"d3-timer": "^3.0.1" "d3-timer": "^3.0.1"
} }
}, },
"node_modules/vite-compatible-readable-stream": {
"version": "3.6.1",
"resolved": "https://registry.npmjs.org/vite-compatible-readable-stream/-/vite-compatible-readable-stream-3.6.1.tgz",
"integrity": "sha512-t20zYkrSf868+j/p31cRIGN28Phrjm3nRSLR2fyc2tiWi4cZGVdv68yNlwnIINTkMTmPoMiSlc0OadaO7DXZaQ==",
"license": "MIT",
"dependencies": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
"util-deprecate": "^1.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/webidl-conversions": { "node_modules/webidl-conversions": {
"version": "7.0.0", "version": "7.0.0",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
@@ -10688,6 +11177,12 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/yoga-layout": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/yoga-layout/-/yoga-layout-3.2.1.tgz",
"integrity": "sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ==",
"license": "MIT"
},
"node_modules/zip-stream": { "node_modules/zip-stream": {
"version": "7.0.5", "version": "7.0.5",
"resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-7.0.5.tgz", "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-7.0.5.tgz",
+1 -1
View File
@@ -6,7 +6,7 @@
"node": ">=18.0.0" "node": ">=18.0.0"
}, },
"scripts": { "scripts": {
"dev": "concurrently --names \"MONGO,NEXT,BACKEND,PROXY\" --prefix-colors \"magenta,cyan,green,yellow\" \"node scripts/start-mongo.js\" \"next dev\" \"node backend/server.js\" \"node proxy/index.js\"", "dev": "concurrently --names \"NEXT,BACKEND,PROXY\" --prefix-colors \"cyan,green,yellow\" \"next dev\" \"node backend/server.js\" \"node proxy/index.js\"",
"dev:central": "concurrently --names \"NEXT,BACKEND\" --prefix-colors \"cyan,green\" \"next dev\" \"node backend/server.js\"", "dev:central": "concurrently --names \"NEXT,BACKEND\" --prefix-colors \"cyan,green\" \"next dev\" \"node backend/server.js\"",
"dev:next": "next dev", "dev:next": "next dev",
"dev:backend": "node backend/server.js", "dev:backend": "node backend/server.js",
+6 -1
View File
@@ -19,7 +19,12 @@ const scheduler = require('./scheduler');
const routes = require('./routes'); const routes = require('./routes');
const PORT = parseInt(process.env.PROXY_PORT || '4000'); const PORT = parseInt(process.env.PROXY_PORT || '4000');
const MONGODB_URI = process.env.MONGODB_URI || 'mongodb://127.0.0.1:27017/backone_dpi'; if (!process.env.MONGODB_URI) {
console.error('[MongoDB] ❌ CRITICAL ERROR: MONGODB_URI environment variable is missing.');
console.error('[MongoDB] Local database is disabled. Connection to central database backone_dpi is required.');
process.exit(1);
}
const MONGODB_URI = process.env.MONGODB_URI;
// Connect to MongoDB with automated retries // Connect to MongoDB with automated retries
async function connectDB() { async function connectDB() {
+3 -3
View File
@@ -111,7 +111,7 @@ const POST_DEPLOY_COMMANDS = [
// Copy Next.js static assets to standalone directory (required for standalone mode) // Copy Next.js static assets to standalone directory (required for standalone mode)
// Fix standalone directory structure for Next.js // Fix standalone directory structure for Next.js
'mkdir -p /home/adminbackend/web/demoplace.my.id/public_html/.next/standalone/.next/', 'mkdir -p /home/adminbackend/web/demoplace.my.id/public_html/.next/standalone/.next/',
'cp -r /home/adminbackend/web/demoplace.my.id/public_html/.next/static /home/adminbackend/web/demoplace.my.id/public_html/.next/standalone/.next/static && echo "Static assets copied to standalone"', 'rm -rf /home/adminbackend/web/demoplace.my.id/public_html/.next/standalone/.next/static && cp -r /home/adminbackend/web/demoplace.my.id/public_html/.next/static /home/adminbackend/web/demoplace.my.id/public_html/.next/standalone/.next/static && echo "Static assets copied to standalone"',
'cp -r /home/adminbackend/web/demoplace.my.id/public_html/public /home/adminbackend/web/demoplace.my.id/public_html/.next/standalone/public 2>/dev/null || true', 'cp -r /home/adminbackend/web/demoplace.my.id/public_html/public /home/adminbackend/web/demoplace.my.id/public_html/.next/standalone/public 2>/dev/null || true',
'ln -sfn /home/adminbackend/web/demoplace.my.id/public_html/.next/standalone/.next /home/adminbackend/web/demoplace.my.id/public_html/_next', 'ln -sfn /home/adminbackend/web/demoplace.my.id/public_html/.next/standalone/.next /home/adminbackend/web/demoplace.my.id/public_html/_next',
'ln -sfn /home/adminbackend/web/demoplace.my.id/public_html/.next/standalone/.next/static /home/adminbackend/web/demoplace.my.id/public_html/.next/static', 'ln -sfn /home/adminbackend/web/demoplace.my.id/public_html/.next/standalone/.next/static /home/adminbackend/web/demoplace.my.id/public_html/.next/static',
@@ -239,7 +239,7 @@ function runSshCommands(commands) {
port: CONFIG.port, port: CONFIG.port,
username: CONFIG.username, username: CONFIG.username,
password: CONFIG.password, password: CONFIG.password,
readyTimeout: 30000, readyTimeout: 90000,
}); });
}); });
} }
@@ -280,7 +280,7 @@ async function deploy() {
port: CONFIG.port, port: CONFIG.port,
username: CONFIG.username, username: CONFIG.username,
password: CONFIG.password, password: CONFIG.password,
readyTimeout: 30000, readyTimeout: 90000,
}); });
console.log('[SFTP] Connected!\n'); console.log('[SFTP] Connected!\n');
+55
View File
@@ -0,0 +1,55 @@
// scripts/diagnose-labeling-bug.js
const { Client } = require('ssh2');
const CONFIG = {
host: '103.185.47.52',
port: 2222,
username: 'adminbackend',
password: 'htEo7x6LsBQiEHHH',
};
function execCmd(ssh, cmd) {
return new Promise((resolve) => {
console.log(`\n========================================`);
console.log(`EXEC: ${cmd}`);
console.log(`========================================`);
ssh.exec(cmd, (err, stream) => {
if (err) {
console.error('Exec error:', err.message);
return resolve('');
}
let out = '';
stream.on('data', (d) => { out += d; process.stdout.write(d); });
stream.stderr.on('data', (d) => { out += d; process.stderr.write(d); });
stream.on('close', () => resolve(out));
});
});
}
async function main() {
const ssh = new Client();
ssh.on('ready', async () => {
console.log('[SSH] Connected for targetted diagnosis...');
const pm2Path = '/home/adminbackend/.npm-global/bin/pm2';
// 1. List PM2 processes
await execCmd(ssh, `${pm2Path} list`);
// 2. Fetch last 50 error logs of frontend
await execCmd(ssh, `${pm2Path} logs backone-frontend --lines 50 --err --raw`);
// 3. Fetch last 50 output logs of frontend
await execCmd(ssh, `${pm2Path} logs backone-frontend --lines 50 --out --raw`);
// 4. Check pm2 logs backone-backend or other backend services
await execCmd(ssh, `${pm2Path} logs backone-backend --lines 50 --err --raw`);
// 5. Check if the directory next standalone actually contains device-labeling folder
await execCmd(ssh, `find /home/adminbackend/web/demoplace.my.id/public_html/.next/standalone/.next/server/app -name "device-labeling" || echo "Not found"`);
await execCmd(ssh, `ls -la /home/adminbackend/web/demoplace.my.id/public_html/.next/standalone/.next/server/app/ || echo "Failed to list app dir"`);
ssh.end();
}).connect({ host: CONFIG.host, port: CONFIG.port, username: CONFIG.username, password: CONFIG.password });
}
main().catch(console.error);
+18 -1
View File
@@ -10,6 +10,19 @@ const CONFIG = {
const cleanProxyPhpCode = `<?php const cleanProxyPhpCode = `<?php
// proxy.php — Reverse proxy to BackOne Dashboard Engine on port 3000 // proxy.php — Reverse proxy to BackOne Dashboard Engine on port 3000
if (!function_exists('getallheaders')) {
function getallheaders() {
$headers = array();
foreach ($_SERVER as $name => $value) {
if (substr($name, 0, 5) == 'HTTP_') {
$headers[str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($name, 5)))))] = $value;
}
}
return $headers;
}
}
$target = 'http://127.0.0.1:3000' . $_SERVER['REQUEST_URI']; $target = 'http://127.0.0.1:3000' . $_SERVER['REQUEST_URI'];
$ch = curl_init($target); $ch = curl_init($target);
@@ -50,7 +63,11 @@ curl_close($ch);
http_response_code($status); http_response_code($status);
foreach (explode("\r\n", $resp_headers) as $h) { foreach (explode("\r\n", $resp_headers) as $h) {
if ($h && !str_starts_with($h, 'HTTP/') && !str_starts_with(strtolower($h), 'transfer-encoding:')) { if ($h && stripos($h, 'HTTP/') !== 0 && stripos($h, 'transfer-encoding:') !== 0) {
if (stripos($h, 'location:') === 0) {
$h = str_replace('http://127.0.0.1:3000', '', $h);
$h = str_replace('http://localhost:3000', '', $h);
}
header($h, false); header($h, false);
} }
} }
+28 -7
View File
@@ -94,15 +94,36 @@ export default function AgentsPage() {
const uptimeValues = agents.map(a => uptimeMap[a.uuid || a.serial] ?? 100); const uptimeValues = agents.map(a => uptimeMap[a.uuid || a.serial] ?? 100);
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 : 100;
// External Accounts di halaman Agents = akun eksternal teknikal/operasional saja // External Accounts di halaman Agents = akun teknikal/operasional saja
// AGENT_VIEWER = akun network agent (sudah ada di tabel agents) → BUKAN akun eksternal // AGENT_VIEWER = akun network agent (ditampilkan di tabel agents atas) → exclude
// COMPANY_* = akun customer/client → sudah ada di halaman User Accounts // COMPANY_* = akun customer/client → ada di halaman User Accounts → exclude
const EXCLUDED_ROLES = ['COMPANY_ADMIN', 'COMPANY_OPERATOR', 'COMPANY_VIEWER', 'AGENT_VIEWER']; const EXCLUDED_ROLES = ['COMPANY_ADMIN', 'COMPANY_OPERATOR', 'COMPANY_VIEWER', 'AGENT_VIEWER'];
const SIAB_UUID = '6681452d_9cae_4ff4_8ae8_0d504774265e';
const NEXUS_UUID = 'd7902405_0dc2_458b_8584_ed4d24b64f24';
const externalUsers = managedUsers.filter(u => !EXCLUDED_ROLES.includes(u.role || '')); const externalUsers = managedUsers.filter(u => !EXCLUDED_ROLES.includes(u.role || ''));
// Grouping by site — semua sudah dipastikan hanya SUPER_ADMIN, EXECUTIVE, TENANT_ADMIN, SOC_ANALYST, ENGINEER
const superadminUsers = externalUsers.filter(u => u.role === 'SUPER_ADMIN' || u.role === 'EXECUTIVE' || !u.site_uuid || u.site_uuid === 'default'); // Grouping eksplisit:
const siabUsers = externalUsers.filter(u => u.site_uuid === '6681452d_9cae_4ff4_8ae8_0d504774265e' && u.role !== 'SUPER_ADMIN' && u.role !== 'EXECUTIVE'); // - siabUsers: memiliki site_uuid = SIAB (bukan SUPER_ADMIN/EXECUTIVE)
const nexusUsers = externalUsers.filter(u => u.site_uuid === 'd7902405_0dc2_458b_8584_ed4d24b64f24' && u.role !== 'SUPER_ADMIN' && u.role !== 'EXECUTIVE'); // - nexusUsers: memiliki site_uuid = NEXUS (bukan SUPER_ADMIN/EXECUTIVE)
// - superadminUsers: SUPER_ADMIN, EXECUTIVE, atau site_uuid selain SIAB/NEXUS (termasuk null)
const siabUsers = externalUsers.filter(u =>
u.site_uuid === SIAB_UUID &&
u.role !== 'SUPER_ADMIN' &&
u.role !== 'EXECUTIVE'
);
const nexusUsers = externalUsers.filter(u =>
u.site_uuid === NEXUS_UUID &&
u.role !== 'SUPER_ADMIN' &&
u.role !== 'EXECUTIVE'
);
const superadminUsers = externalUsers.filter(u =>
u.role === 'SUPER_ADMIN' ||
u.role === 'EXECUTIVE' ||
u.site_uuid === null ||
u.site_uuid === undefined ||
u.site_uuid === 'default' ||
(u.site_uuid !== SIAB_UUID && u.site_uuid !== NEXUS_UUID)
);
return ( return (
<div className="space-y-6"> <div className="space-y-6">
+2 -2
View File
@@ -27,6 +27,8 @@ export default function DeviceLabelingPage() {
const [isDetailOpen, setIsDetailOpen] = useState(false); const [isDetailOpen, setIsDetailOpen] = useState(false);
const [detailedDevice, setDetailedDevice] = useState<DeviceLabelItem | null>(null); const [detailedDevice, setDetailedDevice] = useState<DeviceLabelItem | null>(null);
const [isExportingPdf, setIsExportingPdf] = useState(false);
const fetchDevices = async () => { const fetchDevices = async () => {
setIsLoading(true); setIsLoading(true);
setError(null); setError(null);
@@ -104,8 +106,6 @@ export default function DeviceLabelingPage() {
onMacClick: handleMacClick, onMacClick: handleMacClick,
}); });
const [isExportingPdf, setIsExportingPdf] = useState(false);
const handleExportPdf = async () => { const handleExportPdf = async () => {
if (devices.length === 0) return; if (devices.length === 0) return;
setIsExportingPdf(true); setIsExportingPdf(true);
+1 -1
View File
@@ -28,7 +28,7 @@ async function handleProxy(req: NextRequest, { params }: { params: Promise<{ rou
const url = new URL(req.url); const url = new URL(req.url);
// Force ALL traffic to go through our own Express Backend // Force ALL traffic to go through our own Express Backend
const backendPort = process.env.BACKEND_PORT || '3002'; const backendPort = process.env.BACKEND_PORT || '3001';
const localBase = process.env.NEXT_PUBLIC_API_URL || `http://127.0.0.1:${backendPort}`; const localBase = process.env.NEXT_PUBLIC_API_URL || `http://127.0.0.1:${backendPort}`;
const destinationUrl = `${localBase}/api/${route}${url.search}`; const destinationUrl = `${localBase}/api/${route}${url.search}`;
+4 -1
View File
@@ -35,7 +35,10 @@ 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 (API_KEY && API_KEY.startsWith("sk_db_")) {
const mongoose = require('mongoose'); const mongoose = require('mongoose');
const MONGODB_URI = process.env.MONGODB_URI || 'mongodb://127.0.0.1:27017/backone_dpi'; const MONGODB_URI = process.env.MONGODB_URI;
if (!MONGODB_URI) {
throw new Error('MONGODB_URI environment variable is missing. Connection to central database backone_dpi is required.');
}
try { try {
if (mongoose.connection.readyState === 0) { if (mongoose.connection.readyState === 0) {
+103
View File
@@ -0,0 +1,103 @@
# INSTRUKSI SYNC FITUR DASHBOARD (Dari Netify ke BackOne)
**UNTUK AGENT AI DI REPOSITORI: DPI-Source API BackOne (dev.demoplace.my.id)**
Dokumen ini berisi panduan langkah-demi-langkah dan instruksi implementasi untuk menyelaraskan seluruh fitur dashboard, desain mobile, keamanan, dan optimasi performa dari repositori **DPI-Source API Netify** ke repositori **DPI-Source API BackOne**.
---
## 🚨 ATURAN KRITIS (WAJIB DIPATUHI)
1. **JANGAN PERNAH MENGGANTI/MENGGANGGU SOURCE API PROXY BACKONE**:
* Repositori BackOne menggunakan endpoint custom cloud BackOne (`api0.dev.backone.cloud`). Seluruh berkas di bawah folder `proxy/` bertema `backone*` (seperti `backoneClient.js`, `backoneClientCore.js`, `backoneClientStats.js`, `backoneTelemetry.js`, `backoneAgentFetcher.js`) **TIDAK BOLEH** diganti/diubah menjadi `netify*`.
* Variabel lingkungan di `.env` (seperti `BACKONE_TOKEN`, `BACKONE_DPI_API_KEY`, `BACKONE_INFORMATICS_BASE_URL`) harus tetap dipertahankan. Jangan mengubahnya ke `NETIFY_*`.
2. **JANGAN MENGGANGGU KONFIGURASI INFRASTRUKTUR & PORT**:
* Port backend (`3011`), port proxy (`4010`), target database MongoDB (`backone_inspect_0`), serta allowed origins (`dev.demoplace.my.id`) di file `.env` repositori BackOne **TIDAK BOLEH** diubah ke konfigurasi produksi Netify.
3. **KEPATUHAN BATAS BARIS FILE (RULE 3)**:
* Setiap file baru atau hasil modifikasi yang Anda buat/sentuh **TIDAK BOLEH melebihi 256 baris kode**. Jika melebihi batas tersebut, Anda harus memecahnya menjadi file modular yang lebih kecil.
---
## Bagian 1: Daftar Fitur Baru yang Harus Diimplementasikan
### 1. Dashboard Mobile Modern (App-Native UX)
* **Bottom Navigation (5-Tab)**: Tambahkan menu navigasi melayang di bawah layar mobile dengan tab `Overview`, `Devices`, `Flows`, `Threats`, dan `Menu ☰`.
* **MobileTopBar**: Header minimalis di atas layar mobile menampilkan logo instansi, status site, dan bell notifikasi.
* **Laci Filter Melayang (Modal Drawer)**: Pindahkan semua input filter (Universal Filters dan Threat Filters) ke panel modal laci bawah layar smartphone agar tidak memakan ruang konten utama.
* **DataTable Mobile Cards**: Konversi tampilan tabel data lebar menjadi kartu list vertikal yang rapi di layar mobile.
* **Pure CSS/Tailwind Styling**: Hapus file `ViewportScaler.tsx` sepenuhnya. Semua responsivitas layout harus menggunakan Tailwind CSS murni.
### 2. Sesi, Otentikasi & Keamanan (Security)
* **Lockout Login**: Batasi kegagalan login salah password maksimal 3 kali sebelum akun dikunci selama 15 menit, lengkap dengan timer hitung mundur interaktif di login page.
* **Unlock Akun Manual**: Tambahkan tombol aksi `Unlock 🔓` di tabel `/user-accounts` yang hanya bisa diakses oleh admin/superadmin untuk melepas status lockout secara instan.
* **Inactivity Timeout (Tab-Aware)**: Pasang timeout ketidakaktifan pengguna selama 1 jam menggunakan Page Visibility API (resets otomatis jika tab aktif, memberikan warning hanya di 2 menit terakhir).
* **Session Cookie**: Atur token JWT auth sebagai session cookie saja (tanpa parameter `maxAge`) agar langsung terhapus saat tab browser ditutup.
### 3. Ekspor Laporan PDF (Custom Labeling)
* **Integrasi Label Pemilik**: Tampilkan label nama pemilik kustom perangkat (*Owner / Custom Label*) di setiap file PDF ekspor (PDF riwayat MAC Address, PDF detail perangkat, PDF daftar aset, dll).
* **Cetak PDF Tambahan**: Buat dokumen cetak PDF khusus untuk direktori aset perangkat dan modal riwayat detail MAC perangkat.
### 4. Mode Impersonasi ("View-As")
* **View-As Banner**: Tampilkan banner status melayang di bagian atas dashboard saat admin sedang mensimulasikan login sebagai pengguna lain.
* **Audit & Isolasi**: Amankan kueri backend View-As agar data yang diakses 100% terisolasi sesuai profil target user dan simpan log aktivitasnya ke collection `ViewAsLog`.
### 5. Multi-Tier Role Perusahaan & Kuota
* **3 Tier Role Perusahaan**: Implementasikan isolasi peran kueri database untuk `COMPANY_ADMIN`, `COMPANY_OPERATOR`, dan `COMPANY_VIEWER`.
* **Kuota Akun**: Batasi maksimal 5 akun pengguna per perusahaan lengkap dengan *Quota Tracker* visual.
---
## Bagian 2: Panduan Sinkronisasi File-by-File
### A. Salin/Buat File Baru dari Netify ke BackOne
Berikut adalah file baru di Netify yang harus Anda buat dan tempatkan di lokasi yang sama di folder BackOne:
1. **Komponen Mobile & Layout**:
* `src/components/layout/MobileBottomBar.tsx` (Navigasi bawah mobile)
* `src/components/layout/MobileTopBar.tsx` (Header top mobile)
* `src/components/layout/SessionTimeoutModal.tsx` (Pop-up timeout aktivitas)
* `src/components/layout/ViewAsBanner.tsx` (Banner mode impersonasi)
* `src/components/ui/DataTableDesktopView.tsx` (Pecahan tabel desktop untuk Rule 3)
* `src/components/ui/DataTableMobileCards.tsx` (Pecahan tabel kartu mobile)
2. **Ekspor Dokumen PDF**:
* `src/components/admin/DeviceLabelingPdfDocument.tsx` (Template cetak PDF direktori aset)
* `src/components/admin/DeviceMacPdfDocument.tsx` (Template cetak PDF detail MAC)
3. **Backend Kueri Labeling Modular (Rule 3)**:
* `backend/routes/dashboard/deviceLabeling/getLabeling.js` (Mengambil list labeling)
* `backend/routes/dashboard/deviceLabeling/getMacDetails.js` (Mengambil detail MAC)
* `backend/routes/dashboard/deviceLabeling/updateLabel.js` (Memperbarui label)
* `backend/scripts/seed_device_labels.js` (Script generator random seeder)
4. **Pembantu Kueri Terpisah**:
* `src/app/(dashboard)/flows/flowColumns.tsx` (Ekstraksi kolom flow untuk Rule 3)
* `src/lib/actions/agentsMongo.ts` (Ekstraksi query database agen untuk Rule 3)
### B. Modifikasi / Timpa (Overwrite) File yang Berbeda
Timpa isi kode file berikut di folder BackOne menggunakan kode dari Netify, **tetapi pastikan untuk tidak mengubah parameter port/database dev BackOne** di konfigurasi lingkungan:
1. **Gaya CSS & Tata Letak (Hard-Lock Dark Theme & Spacing)**:
* `src/app/globals.css`, `src/app/variables.css` (Menghapus selector tema light, hard-lock dark mode)
* `src/app/layout.tsx` (Pemasangan Viewport meta murni dan routing modal)
* `src/components/layout/DashboardLayout.tsx` (Menghubungkan Bottom Navigation mobile dan Topbar)
2. **Filter & Pencarian (Drawer Filters)**:
* `src/components/ui/UniversalFilters.tsx` (Mengubah filter inline menjadi Pop-up Drawer Modal)
* `src/components/threats/ThreatFilters.tsx` (Mengubah filter ancaman menjadi Pop-up Modal)
3. **Komponen Tabel & Halaman**:
* `src/components/ui/DataTable.tsx` (Menghubungkan view desktop & mobile cards modular)
* `src/app/(dashboard)/user-accounts/page.tsx` & `columns.tsx` (Menyertakan quota tracker dan tombol Unlock)
* `src/app/(dashboard)/device-labeling/page.tsx` (Perbaikan hidrasi React Hooks dan tombol ekspor PDF)
* `src/components/admin/DeviceMacDetailsModal.tsx` (Menampilkan asosiasi IP Address, formatting date-time, dan tombol PDF)
4. **Backend Router & Middleware (RBAC & Keamanan)**:
* `backend/routes/dashboard/deviceLabeling.js` (Harus diganti menjadi versi modular 24 baris yang mengimpor handler dari subfolder `deviceLabeling/`)
* `backend/middleware/auth.js` (Sinkronisasi validasi session timeout dan enkripsi role)
* `backend/routes/auth/viewAs.js` (Aktivasi pencatatan log impersonasi)
* `src/lib/actions/agents.ts` (Sinkronisasi pemanggilan `agentsMongo.ts` untuk performa loading cepat)
---
## Bagian 3: Instruksi QA & Uji Integritas Build
Setelah melakukan seluruh pemindahan dan penggabungan kode di atas:
1. **Jalankan Uji Kompilasi**: Jalankan `npm run build` lokal di folder BackOne untuk memverifikasi tidak ada kesalahan tipe TypeScript (0 errors).
2. **Uji Fungsional**:
* Buka dashboard di browser, perkecil resolusi layar ke ukuran smartphone, pastikan Bottom Navigation Bar muncul dan form filter berubah menjadi tombol Drawer.
* Uji coba login salah password 3 kali dan pastikan countdown timer berjalan.
* Uji mode View-As dan pastikan banner merah impersonasi muncul di atas layar.
+2 -2
View File
@@ -85,7 +85,7 @@ console.log('');
console.log('[GROUP 4] Docker Compose — 2 Container Groups + 2 Networks'); console.log('[GROUP 4] Docker Compose — 2 Container Groups + 2 Networks');
const composeCode = readFile('docker-compose.yml'); const composeCode = readFile('docker-compose.yml');
test('docker-compose.yml punya service: mongodb', composeCode.includes('mongodb:')); test('docker-compose.yml tidak punya service: mongodb', !composeCode.includes('mongodb:'));
test('docker-compose.yml punya service: proxy', composeCode.includes('proxy:')); 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: backend', composeCode.includes('backend:'));
test('docker-compose.yml punya service: frontend', composeCode.includes('frontend:')); test('docker-compose.yml punya service: frontend', composeCode.includes('frontend:'));
@@ -93,7 +93,7 @@ test('docker-compose.yml punya network: backone-infra', composeCode.includes('b
test('docker-compose.yml punya network: backone-app', composeCode.includes('backone-app')); 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('proxy connect ke backone-infra (Container 1)', composeCode.includes('backone-infra'));
test('backend connect ke backone-app (Container 2)', composeCode.includes('backone-app')); test('backend connect ke backone-app (Container 2)', composeCode.includes('backone-app'));
test('proxy depends on mongodb', composeCode.includes('service_healthy')); test('proxy tidak depends on mongodb', !composeCode.includes('service_healthy'));
test('env PROXY_COLLECT_MODE di docker-compose', composeCode.includes('PROXY_COLLECT_MODE')); 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('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')); test('PROXY_URL dikirim ke backend', composeCode.includes('PROXY_URL=http://proxy:4000'));
+9 -4
View File
@@ -5,7 +5,12 @@ const path = require('path');
// Load environment variables // Load environment variables
require('dotenv').config({ path: path.join(__dirname, '../.env.local') }); 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 MONGODB_URI = process.env.MONGODB_URI;
if (!MONGODB_URI) {
console.error('✗ ERROR: MONGODB_URI environment variable is missing.');
console.error('Local database is disabled. Connection to central database backone_dpi is required.');
process.exit(1);
}
const User = require('../backend/models/User'); const User = require('../backend/models/User');
const { TenantConfig } = require('../backend/models/Schemas'); const { TenantConfig } = require('../backend/models/Schemas');
@@ -76,10 +81,10 @@ async function runTests() {
assert('Nexus brand_logo should be /nexus-logo.png', nexusConfig.brand_logo === '/nexus-logo.png'); 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) // 4. Validate Agent listing isolation per site_uuid (agent_registry collection)
const db = mongoose.connection.db; const db = mongoose.connection.db;
const siabAgents = await db.collection('summaries').distinct('agent_uuid', { site_uuid: '6681452d_9cae_4ff4_8ae8_0d504774265e' }); const siabAgents = await db.collection('agent_registry').distinct('uuid', { site_uuid: '6681452d_9cae_4ff4_8ae8_0d504774265e' });
const nexusAgents = await db.collection('summaries').distinct('agent_uuid', { site_uuid: 'd7902405_0dc2_458b_8584_ed4d24b64f24' }); const nexusAgents = await db.collection('agent_registry').distinct('uuid', { site_uuid: 'd7902405_0dc2_458b_8584_ed4d24b64f24' });
assert('SIAB agents count should be greater than 0', siabAgents.length > 0); assert('SIAB agents count should be greater than 0', siabAgents.length > 0);
assert('Nexus agents count should be greater than 0', nexusAgents.length > 0); assert('Nexus agents count should be greater than 0', nexusAgents.length > 0);