From f1c1ba03b528f7de09a03f7c3e2ba2f512db63d4 Mon Sep 17 00:00:00 2001 From: Rafif Riqullah Siregar Date: Mon, 24 Aug 2026 14:33:51 +0700 Subject: [PATCH] feat(database): configure local to use central database directly and disable local mongo --- backend/db/mongoose.js | 7 +- backend/routes/dashboard/deviceLabeling.js | 270 +--------- .../dashboard/deviceLabeling/getLabeling.js | 151 ++++++ .../dashboard/deviceLabeling/getMacDetails.js | 72 +++ .../dashboard/deviceLabeling/updateLabel.js | 51 ++ docker-compose.prod.yml | 17 +- docker-compose.yml | 34 +- docs/feature-list.md | 5 + docs/log/2026-07-31-1150-fix-blank-screen.md | 37 ++ ...08-03-0955-adhoc-deploy-labeling-bugfix.md | 33 ++ docs/log/2026-08-24-1428-adhoc.md | 30 ++ package-lock.json | 509 +++++++++++++++++- package.json | 2 +- proxy/index.js | 7 +- scripts/deploy-sftp.js | 6 +- scripts/diagnose-labeling-bug.js | 55 ++ scripts/fix-proxy-php.js | 19 +- src/app/(dashboard)/agents/page.tsx | 35 +- src/app/(dashboard)/device-labeling/page.tsx | 4 +- src/app/api/[...route]/route.ts | 2 +- src/lib/actions/agents.ts | 5 +- sync-instruction-for-backone-agent.md | 103 ++++ test/architecture_test.js | 4 +- test/multitenant_branding_test.js | 13 +- 24 files changed, 1135 insertions(+), 336 deletions(-) create mode 100644 backend/routes/dashboard/deviceLabeling/getLabeling.js create mode 100644 backend/routes/dashboard/deviceLabeling/getMacDetails.js create mode 100644 backend/routes/dashboard/deviceLabeling/updateLabel.js create mode 100644 docs/log/2026-07-31-1150-fix-blank-screen.md create mode 100644 docs/log/2026-08-03-0955-adhoc-deploy-labeling-bugfix.md create mode 100644 docs/log/2026-08-24-1428-adhoc.md create mode 100644 scripts/diagnose-labeling-bug.js create mode 100644 sync-instruction-for-backone-agent.md diff --git a/backend/db/mongoose.js b/backend/db/mongoose.js index 73ef8e1..0807362 100644 --- a/backend/db/mongoose.js +++ b/backend/db/mongoose.js @@ -8,7 +8,12 @@ const path = require('path'); const envFile = process.env.NODE_ENV === 'production' ? '.env.production' : '.env.local'; 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() { if (mongoose.connection.readyState >= 1) return; // already connected diff --git a/backend/routes/dashboard/deviceLabeling.js b/backend/routes/dashboard/deviceLabeling.js index 8ae990f..4c6a86a 100644 --- a/backend/routes/dashboard/deviceLabeling.js +++ b/backend/routes/dashboard/deviceLabeling.js @@ -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 router = express.Router(); -const { DeviceStat, CustomDeviceLabel, Flow } = require('../../models/Schemas'); -const { getTimeFilter, getBaseFilter, getCustomLabelsMap } = require('./helpers'); -const { generateMacFromIp, resolveDeviceTypeFromIp, resolveOSFromIp, resolveVendorFromIp, generateAutoLabel } = require('../../deviceResolver'); -const User = require('../../models/User'); + +const updateLabelHandler = require('./deviceLabeling/updateLabel'); +const getLabelingHandler = require('./deviceLabeling/getLabeling'); +const getMacDetailsHandler = require('./deviceLabeling/getMacDetails'); // POST /api/dashboard/devices/update-label -router.post('/devices/update-label', async (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?._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 }); - } -}); +router.post('/devices/update-label', updateLabelHandler); // GET /api/dashboard/devices/labeling -router.get('/devices/labeling', async (req, res) => { - 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 }); - } -}); +router.get('/devices/labeling', getLabelingHandler); // GET /api/dashboard/devices/mac-details -router.get('/devices/mac-details', async (req, res) => { - 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 }); - } -}); +router.get('/devices/mac-details', getMacDetailsHandler); module.exports = router; diff --git a/backend/routes/dashboard/deviceLabeling/getLabeling.js b/backend/routes/dashboard/deviceLabeling/getLabeling.js new file mode 100644 index 0000000..c1e9a81 --- /dev/null +++ b/backend/routes/dashboard/deviceLabeling/getLabeling.js @@ -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; diff --git a/backend/routes/dashboard/deviceLabeling/getMacDetails.js b/backend/routes/dashboard/deviceLabeling/getMacDetails.js new file mode 100644 index 0000000..cd49c33 --- /dev/null +++ b/backend/routes/dashboard/deviceLabeling/getMacDetails.js @@ -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; diff --git a/backend/routes/dashboard/deviceLabeling/updateLabel.js b/backend/routes/dashboard/deviceLabeling/updateLabel.js new file mode 100644 index 0000000..9033f56 --- /dev/null +++ b/backend/routes/dashboard/deviceLabeling/updateLabel.js @@ -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; diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index f37cc6e..ee8d58b 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -1,16 +1,6 @@ version: '3.8' 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: build: context: ./backend @@ -18,12 +8,10 @@ services: restart: always # No ports exposed to the host! Completely internal. environment: - - MONGODB_URI=mongodb://mongodb:27017/backone_dpi + - MONGODB_URI=${MONGODB_URI} - BACKEND_PORT=3001 env_file: - .env.production - depends_on: - - mongodb frontend: build: @@ -51,6 +39,3 @@ services: - ./nginx/conf.d:/etc/nginx/conf.d depends_on: - frontend - -volumes: - mongodb_data_prod: diff --git a/docker-compose.yml b/docker-compose.yml index 98fd7f8..095d09b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -20,26 +20,6 @@ version: '3.8' services: # ─── 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: build: context: ./proxy @@ -50,7 +30,7 @@ services: env_file: - .env.local environment: - - MONGODB_URI=mongodb://mongodb:27017/backone_dpi + - MONGODB_URI=${MONGODB_URI} - PROXY_PORT=4000 # Mode 1: kumpulkan SEMUA agent (default) # 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 * * * *} networks: - backone-infra - depends_on: - mongodb: - condition: service_healthy # ─── Container Group 2: APP ───────────────────────────────── backend: @@ -74,14 +51,11 @@ services: env_file: - .env.local environment: - - MONGODB_URI=mongodb://mongodb:27017/backone_dpi + - MONGODB_URI=${MONGODB_URI} - BACKEND_PORT=3001 - PROXY_URL=http://proxy:4000 # untuk trigger manual refresh dari dashboard networks: - backone-app - depends_on: - mongodb: - condition: service_healthy frontend: build: @@ -106,7 +80,3 @@ networks: backone-app: driver: bridge name: backone-app - -volumes: - mongodb_data: - name: backone_mongodb_data diff --git a/docs/feature-list.md b/docs/feature-list.md index 18bdbf2..bc249ad 100644 --- a/docs/feature-list.md +++ b/docs/feature-list.md @@ -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** 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 + diff --git a/docs/log/2026-07-31-1150-fix-blank-screen.md b/docs/log/2026-07-31-1150-fix-blank-screen.md new file mode 100644 index 0000000..ebd9c5c --- /dev/null +++ b/docs/log/2026-07-31-1150-fix-blank-screen.md @@ -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. diff --git a/docs/log/2026-08-03-0955-adhoc-deploy-labeling-bugfix.md b/docs/log/2026-08-03-0955-adhoc-deploy-labeling-bugfix.md new file mode 100644 index 0000000..2375707 --- /dev/null +++ b/docs/log/2026-08-03-0955-adhoc-deploy-labeling-bugfix.md @@ -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. diff --git a/docs/log/2026-08-24-1428-adhoc.md b/docs/log/2026-08-24-1428-adhoc.md new file mode 100644 index 0000000..776e7eb --- /dev/null +++ b/docs/log/2026-08-24-1428-adhoc.md @@ -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. diff --git a/package-lock.json b/package-lock.json index 0a6b822..6bdd263 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,13 +1,15 @@ { - "name": "netify-app", - "version": "0.1.0", + "name": "backone-dpi", + "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "netify-app", - "version": "0.1.0", + "name": "backone-dpi", + "version": "1.0.0", "dependencies": { + "@react-pdf/renderer": "^4.5.1", + "@types/leaflet": "^1.9.21", "axios": "^1.18.1", "bcryptjs": "^3.0.3", "better-sqlite3": "^12.11.1", @@ -21,6 +23,7 @@ "express": "^5.2.1", "framer-motion": "^12.42.2", "jsonwebtoken": "^9.0.3", + "leaflet": "^1.9.4", "lucide-react": "^1.21.0", "mongodb-memory-server": "^11.2.0", "mongoose": "^9.7.4", @@ -55,6 +58,9 @@ "ssh2-sftp-client": "^12.1.1", "tailwindcss": "^4", "typescript": "^5" + }, + "engines": { + "node": ">=18.0.0" } }, "node_modules/@alloc/quick-lru": { @@ -1257,6 +1263,30 @@ "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": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -1305,6 +1335,183 @@ "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": { "version": "2.12.0", "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.12.0.tgz", @@ -1860,6 +2067,15 @@ "@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": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", @@ -2599,6 +2815,12 @@ "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": { "version": "2.0.0", "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_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": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", @@ -3316,6 +3547,24 @@ "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": { "version": "4.28.4", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.4.tgz", @@ -3568,6 +3817,15 @@ "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": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", @@ -3597,6 +3855,27 @@ "dev": true, "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": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -4476,6 +4755,12 @@ "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": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", @@ -4550,6 +4835,12 @@ "dev": true, "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": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", @@ -5250,7 +5541,6 @@ "version": "3.3.0", "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.8.x" @@ -5330,7 +5620,6 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, "license": "MIT" }, "node_modules/fast-fifo": { @@ -5397,7 +5686,6 @@ "version": "0.8.3", "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.3.tgz", "integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==", - "dev": true, "license": "MIT" }, "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": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", @@ -6011,6 +6316,21 @@ "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": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", @@ -6044,6 +6364,12 @@ "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": { "version": "0.7.2", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", @@ -6585,6 +6911,12 @@ "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": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", @@ -6663,6 +6995,15 @@ "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": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/jerrypick/-/jerrypick-1.1.2.tgz", @@ -6682,6 +7023,12 @@ "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": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -6933,6 +7280,12 @@ "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": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -7208,6 +7561,25 @@ "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": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -7344,6 +7716,12 @@ "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": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", @@ -7971,6 +8349,15 @@ "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": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -8190,6 +8577,12 @@ "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": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -8203,6 +8596,12 @@ "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": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -8337,6 +8736,14 @@ "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": { "version": "1.2.4", "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_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": { "version": "10.29.3", "resolved": "https://registry.npmjs.org/preact/-/preact-10.29.3.tgz", @@ -8529,6 +8942,15 @@ "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": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -8859,6 +9281,15 @@ "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": { "version": "5.2.0", "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" } }, + "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": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", @@ -9729,6 +10166,12 @@ "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": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/swr/-/swr-2.4.2.tgz", @@ -9942,6 +10385,12 @@ "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": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", @@ -10315,6 +10764,32 @@ "dev": true, "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": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -10449,6 +10924,20 @@ "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": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", @@ -10688,6 +11177,12 @@ "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": { "version": "7.0.5", "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-7.0.5.tgz", diff --git a/package.json b/package.json index 1d8cdcb..b9a714d 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "node": ">=18.0.0" }, "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:next": "next dev", "dev:backend": "node backend/server.js", diff --git a/proxy/index.js b/proxy/index.js index d5bc907..b28ed8e 100644 --- a/proxy/index.js +++ b/proxy/index.js @@ -19,7 +19,12 @@ const scheduler = require('./scheduler'); const routes = require('./routes'); 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 async function connectDB() { diff --git a/scripts/deploy-sftp.js b/scripts/deploy-sftp.js index 11da5c8..aaf2b22 100644 --- a/scripts/deploy-sftp.js +++ b/scripts/deploy-sftp.js @@ -111,7 +111,7 @@ const POST_DEPLOY_COMMANDS = [ // Copy Next.js static assets to standalone directory (required for standalone mode) // Fix standalone directory structure for Next.js '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', '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', @@ -239,7 +239,7 @@ function runSshCommands(commands) { port: CONFIG.port, username: CONFIG.username, password: CONFIG.password, - readyTimeout: 30000, + readyTimeout: 90000, }); }); } @@ -280,7 +280,7 @@ async function deploy() { port: CONFIG.port, username: CONFIG.username, password: CONFIG.password, - readyTimeout: 30000, + readyTimeout: 90000, }); console.log('[SFTP] Connected!\n'); diff --git a/scripts/diagnose-labeling-bug.js b/scripts/diagnose-labeling-bug.js new file mode 100644 index 0000000..11cf5b5 --- /dev/null +++ b/scripts/diagnose-labeling-bug.js @@ -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); diff --git a/scripts/fix-proxy-php.js b/scripts/fix-proxy-php.js index 1384d9e..a144868 100644 --- a/scripts/fix-proxy-php.js +++ b/scripts/fix-proxy-php.js @@ -10,6 +10,19 @@ const CONFIG = { const cleanProxyPhpCode = ` $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']; $ch = curl_init($target); @@ -50,7 +63,11 @@ curl_close($ch); http_response_code($status); 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); } } diff --git a/src/app/(dashboard)/agents/page.tsx b/src/app/(dashboard)/agents/page.tsx index 964ef92..68c1542 100644 --- a/src/app/(dashboard)/agents/page.tsx +++ b/src/app/(dashboard)/agents/page.tsx @@ -94,15 +94,36 @@ export default function AgentsPage() { 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; - // External Accounts di halaman Agents = akun eksternal teknikal/operasional saja - // AGENT_VIEWER = akun network agent (sudah ada di tabel agents) → BUKAN akun eksternal - // COMPANY_* = akun customer/client → sudah ada di halaman User Accounts + // External Accounts di halaman Agents = akun teknikal/operasional saja + // AGENT_VIEWER = akun network agent (ditampilkan di tabel agents atas) → exclude + // COMPANY_* = akun customer/client → ada di halaman User Accounts → exclude 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 || '')); - // 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'); - const siabUsers = externalUsers.filter(u => u.site_uuid === '6681452d_9cae_4ff4_8ae8_0d504774265e' && u.role !== 'SUPER_ADMIN' && u.role !== 'EXECUTIVE'); - const nexusUsers = externalUsers.filter(u => u.site_uuid === 'd7902405_0dc2_458b_8584_ed4d24b64f24' && u.role !== 'SUPER_ADMIN' && u.role !== 'EXECUTIVE'); + + // Grouping eksplisit: + // - siabUsers: memiliki site_uuid = SIAB (bukan SUPER_ADMIN/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 (
diff --git a/src/app/(dashboard)/device-labeling/page.tsx b/src/app/(dashboard)/device-labeling/page.tsx index 8c60ddd..1cc4776 100644 --- a/src/app/(dashboard)/device-labeling/page.tsx +++ b/src/app/(dashboard)/device-labeling/page.tsx @@ -27,6 +27,8 @@ export default function DeviceLabelingPage() { const [isDetailOpen, setIsDetailOpen] = useState(false); const [detailedDevice, setDetailedDevice] = useState(null); + const [isExportingPdf, setIsExportingPdf] = useState(false); + const fetchDevices = async () => { setIsLoading(true); setError(null); @@ -104,8 +106,6 @@ export default function DeviceLabelingPage() { onMacClick: handleMacClick, }); - const [isExportingPdf, setIsExportingPdf] = useState(false); - const handleExportPdf = async () => { if (devices.length === 0) return; setIsExportingPdf(true); diff --git a/src/app/api/[...route]/route.ts b/src/app/api/[...route]/route.ts index b5e2459..e5eca84 100644 --- a/src/app/api/[...route]/route.ts +++ b/src/app/api/[...route]/route.ts @@ -28,7 +28,7 @@ async function handleProxy(req: NextRequest, { params }: { params: Promise<{ rou const url = new URL(req.url); // 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 destinationUrl = `${localBase}/api/${route}${url.search}`; diff --git a/src/lib/actions/agents.ts b/src/lib/actions/agents.ts index 63b0cdf..a0e6a8d 100644 --- a/src/lib/actions/agents.ts +++ b/src/lib/actions/agents.ts @@ -35,7 +35,10 @@ export async function getAgents(clientSiteUuid?: string): Promise { // All agent data comes from MongoDB agent_registry (single source of truth) if (API_KEY && API_KEY.startsWith("sk_db_")) { 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 { if (mongoose.connection.readyState === 0) { diff --git a/sync-instruction-for-backone-agent.md b/sync-instruction-for-backone-agent.md new file mode 100644 index 0000000..49c9c4b --- /dev/null +++ b/sync-instruction-for-backone-agent.md @@ -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. diff --git a/test/architecture_test.js b/test/architecture_test.js index 75df6f2..a9537c0 100644 --- a/test/architecture_test.js +++ b/test/architecture_test.js @@ -85,7 +85,7 @@ console.log(''); console.log('[GROUP 4] Docker Compose — 2 Container Groups + 2 Networks'); const composeCode = readFile('docker-compose.yml'); -test('docker-compose.yml punya service: mongodb', composeCode.includes('mongodb:')); +test('docker-compose.yml tidak punya service: mongodb', !composeCode.includes('mongodb:')); test('docker-compose.yml punya service: proxy', composeCode.includes('proxy:')); test('docker-compose.yml punya service: backend', composeCode.includes('backend:')); test('docker-compose.yml punya service: frontend', composeCode.includes('frontend:')); @@ -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('proxy connect ke backone-infra (Container 1)', composeCode.includes('backone-infra')); test('backend connect ke backone-app (Container 2)', composeCode.includes('backone-app')); -test('proxy depends on mongodb', composeCode.includes('service_healthy')); +test('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_AGENT_UUID di docker-compose', composeCode.includes('PROXY_AGENT_UUID')); test('PROXY_URL dikirim ke backend', composeCode.includes('PROXY_URL=http://proxy:4000')); diff --git a/test/multitenant_branding_test.js b/test/multitenant_branding_test.js index 1988bb5..b6541b4 100644 --- a/test/multitenant_branding_test.js +++ b/test/multitenant_branding_test.js @@ -5,7 +5,12 @@ const path = require('path'); // Load environment variables require('dotenv').config({ path: path.join(__dirname, '../.env.local') }); -const MONGODB_URI = process.env.MONGODB_URI || 'mongodb://127.0.0.1:27017/backone_dpi'; +const 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 { 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'); } - // 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 siabAgents = await db.collection('summaries').distinct('agent_uuid', { site_uuid: '6681452d_9cae_4ff4_8ae8_0d504774265e' }); - const nexusAgents = await db.collection('summaries').distinct('agent_uuid', { site_uuid: 'd7902405_0dc2_458b_8584_ed4d24b64f24' }); + const siabAgents = await db.collection('agent_registry').distinct('uuid', { site_uuid: '6681452d_9cae_4ff4_8ae8_0d504774265e' }); + 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('Nexus agents count should be greater than 0', nexusAgents.length > 0);