From 93d4002b2763f2740496affe58137b9861f9f507 Mon Sep 17 00:00:00 2001 From: vanne Date: Wed, 1 Jul 2026 14:02:27 +0700 Subject: [PATCH] feat: implement password changes, agent headers, MAC routing tags, GeoIP caching lookups, port descriptions, and network infra agent isolation --- backend/database.js | 635 ++++++++++++++++++++++-- backend/routes/auth.js | 135 ++++- backend/routes/dashboard.js | 90 ++-- src/app/(dashboard)/agents/page.tsx | 24 +- src/app/(dashboard)/devices/page.tsx | 13 +- src/app/(dashboard)/flows/page.tsx | 71 ++- src/components/layout/Header.tsx | 30 ++ src/components/layout/Sidebar.tsx | 185 ++++++- src/components/ui/DeviceDetailModal.tsx | 94 ++-- src/components/ui/IpDetails.tsx | 68 +++ 10 files changed, 1206 insertions(+), 139 deletions(-) create mode 100644 src/components/ui/IpDetails.tsx diff --git a/backend/database.js b/backend/database.js index 7db80b5..99c0d53 100644 --- a/backend/database.js +++ b/backend/database.js @@ -6,6 +6,40 @@ const bcrypt = require('bcryptjs'); const DB_PATH = path.join(__dirname, 'netify_data.db'); let db; +// Agent UUID mappings to MAC addresses and numeric interface IDs +const AGENT_MAC_MAP = { + '2F-TF-1D-GK': ['60:be:b4:1f:05:96'], + '8A-V3-PB-85': ['04:f4:1c:ce:c2:e6'], + 'F6-2V-DT-8A': [ + '2c:7b:a0:d8:86:91', '16:11:ac:73:34:1d', 'bc:45:5b:ca:d5:be', + 'de:ed:cc:57:58:34', 'f4:6d:3f:ef:01:a0', '60:be:b4:29:d3:36', + '70:85:6c:81:50:d4', '70:85:6c:6d:f7:17', '60:be:b4:29:d3:33', + '60:be:b4:26:4c:d6', '60:be:b4:29:d3:32' + ], +}; + +const AGENT_NUMERIC_IDS = { + '2F-TF-1D-GK': ['4894730147'], + '8A-V3-PB-85': ['4895530456'], + 'F6-2V-DT-8A': ['4895853843', '4897042839'], +}; + +function getAgentTrafficRatio(agentUuid) { + const d = getDB(); + const macs = AGENT_MAC_MAP[agentUuid]; + if (!macs || macs.length === 0) return 0; + + const latest = d.prepare("SELECT MAX(fetched_at) as t FROM mac_bandwidth").get(); + if (!latest?.t) return 0; + + const siteTotal = d.prepare("SELECT SUM(total) as val FROM mac_bandwidth WHERE fetched_at = ?").get(latest.t)?.val || 1; + + const placeholders = macs.map(() => '?').join(','); + const agentTotal = d.prepare(`SELECT SUM(total) as val FROM mac_bandwidth WHERE fetched_at = ? AND mac_address IN (${placeholders})`).get(latest.t, ...macs)?.val || 0; + + return siteTotal > 0 ? (agentTotal / siteTotal) : 0; +} + function getDB() { if (!db) { db = new Database(DB_PATH); @@ -26,9 +60,17 @@ function initSchema() { password_hash TEXT NOT NULL, role TEXT NOT NULL DEFAULT 'AGENT_VIEWER', site_uuid TEXT DEFAULT NULL, + agent_uuid TEXT DEFAULT NULL, created_at TEXT DEFAULT CURRENT_TIMESTAMP )`); + // Alter users table to add agent_uuid if it was created on an older schema + try { + d.exec("ALTER TABLE users ADD COLUMN agent_uuid TEXT DEFAULT NULL"); + } catch (e) { + // Column already exists, safe to ignore + } + const adminExists = d.prepare("SELECT count(*) as count FROM users WHERE username = 'admin'").get(); if (adminExists.count === 0) { const hash = bcrypt.hashSync('admin123', 10); @@ -39,11 +81,32 @@ function initSchema() { const agentExists = d.prepare("SELECT count(*) as count FROM users WHERE username = 'agent1'").get(); if (agentExists.count === 0) { const hash = bcrypt.hashSync('agent123', 10); - const site_uuid = process.env.NETIFY_SITE_UUID || 'dummy_site_uuid'; - d.prepare("INSERT INTO users (username, password_hash, role, site_uuid) VALUES (?, ?, ?, ?)").run('agent1', hash, 'AGENT_VIEWER', site_uuid); + const site_uuid = process.env.NETIFY_SITE_UUID || '6681452d_9cae_4ff4_8ae8_0d504774265e'; + d.prepare("INSERT INTO users (username, password_hash, role, site_uuid, agent_uuid) VALUES (?, ?, ?, ?, ?)").run('agent1', hash, 'AGENT_VIEWER', site_uuid, '8A-V3-PB-85'); console.log('[DB] Created default agent user (agent1 / agent123)'); } + // Seed specific agent accounts for testing isolation + const agentJrpExists = d.prepare("SELECT count(*) as count FROM users WHERE username = 'agent_jrp'").get(); + if (agentJrpExists.count === 0) { + const hash = bcrypt.hashSync('agent123', 10); + d.prepare("INSERT INTO users (username, password_hash, role, site_uuid, agent_uuid) VALUES (?, ?, ?, ?, ?)").run('agent_jrp', hash, 'AGENT_VIEWER', '6681452d_9cae_4ff4_8ae8_0d504774265e', '2F-TF-1D-GK'); + console.log('[DB] Created JRP Cibubur agent user (agent_jrp / agent123)'); + } + + const agentIfgExists = d.prepare("SELECT count(*) as count FROM users WHERE username = 'agent_ifg'").get(); + if (agentIfgExists.count === 0) { + const hash = bcrypt.hashSync('agent123', 10); + d.prepare("INSERT INTO users (username, password_hash, role, site_uuid, agent_uuid) VALUES (?, ?, ?, ?, ?)").run('agent_ifg', hash, 'AGENT_VIEWER', '6681452d_9cae_4ff4_8ae8_0d504774265e', '8A-V3-PB-85'); + console.log('[DB] Created IFG LT.18 agent user (agent_ifg / agent123)'); + } + + const agentCpiExists = d.prepare("SELECT count(*) as count FROM users WHERE username = 'agent_cpi'").get(); + if (agentCpiExists.count === 0) { + const hash = bcrypt.hashSync('agent123', 10); + d.prepare("INSERT INTO users (username, password_hash, role, site_uuid, agent_uuid) VALUES (?, ?, ?, ?, ?)").run('agent_cpi', hash, 'AGENT_VIEWER', '6681452d_9cae_4ff4_8ae8_0d504774265e', 'F6-2V-DT-8A'); + console.log('[DB] Created CPI Balaraja agent user (agent_cpi / agent123)'); + } d.exec(`CREATE TABLE IF NOT EXISTS tls_versions ( id INTEGER PRIMARY KEY AUTOINCREMENT, @@ -490,6 +553,15 @@ function initSchema() { total_flows INTEGER DEFAULT 0, active_devices INTEGER DEFAULT 0 )`); + d.exec(`CREATE TABLE IF NOT EXISTS geoip_cache ( + ip_address TEXT PRIMARY KEY, + isp TEXT, + country TEXT, + city TEXT, + as_org TEXT, + created_at TEXT DEFAULT CURRENT_TIMESTAMP + )`); + console.log('[DB] Schema siap.'); } @@ -698,86 +770,441 @@ function insertBandwidthTimeline(summary, fetchedAt, siteUuid) { // ─── QUERY FUNCTIONS ────────────────────────────────────────────────────────── -function getLatestBandwidthApps(limit = 20, siteUuid = null) { - const d = getDB(); - const latest = d.prepare(`SELECT MAX(fetched_at) as t FROM bandwidth_apps`).get(); - if (!latest?.t) return []; - return d.prepare(` - SELECT * FROM bandwidth_apps WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid) AND fetched_at = @fetched_at ORDER BY download DESC LIMIT @limit - `).all({ fetched_at: latest.t, limit, siteUuid }); -} - -function getLatestDevices(limit = 100, siteUuid = null) { - const d = getDB(); - const latest = d.prepare(`SELECT MAX(fetched_at) as t FROM devices`).get(); - if (!latest?.t) return []; - return d.prepare(` - SELECT * FROM devices WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid) AND fetched_at = @fetched_at ORDER BY download DESC LIMIT @limit - `).all({ fetched_at: latest.t, limit, siteUuid }); -} - -function getLatestFlows(limit = 100, siteUuid = null) { +function getLatestBandwidthApps(limit = 20, siteUuid = null, agentUuid = null) { const d = getDB(); const latest = d.prepare(`SELECT MAX(fetched_at) as t FROM flows`).get(); if (!latest?.t) return []; + + if (agentUuid && AGENT_MAC_MAP[agentUuid]) { + const macs = AGENT_MAC_MAP[agentUuid]; + const placeholders = macs.map(() => '?').join(','); + return d.prepare(` + SELECT app_label, SUM(bytes_download) AS download, SUM(bytes_upload) AS upload, + (SUM(bytes_download) + SUM(bytes_upload)) AS total, COUNT(*) AS flow_count + FROM flows + WHERE src_mac IN (${placeholders}) AND fetched_at = ? AND app_label IS NOT NULL + GROUP BY app_label + ORDER BY download DESC + LIMIT ? + `).all(...macs, latest.t, limit); + } + + const appsLatest = d.prepare(`SELECT MAX(fetched_at) as t FROM bandwidth_apps`).get(); + if (!appsLatest?.t) return []; + return d.prepare(` + SELECT * FROM bandwidth_apps WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid) AND fetched_at = @fetched_at ORDER BY download DESC LIMIT @limit + `).all({ fetched_at: appsLatest.t, limit, siteUuid }); +} + +function resolveDeviceMetadata(ip, mac, dbLabel, dbManufacturer, dbType) { + let label = dbLabel || ip; + let manufacturer = dbManufacturer || 'Unknown'; + let type = dbType || 'Generic Client'; + let os = 'Unknown'; + + if (manufacturer.includes('Routerboard') || manufacturer.includes('MikroTik')) { + manufacturer = 'MikroTik'; + type = 'Router/Network'; + os = 'RouterOS'; + } else if (manufacturer.includes('Fortinet')) { + manufacturer = 'Fortinet'; + type = 'Firewall/Network'; + os = 'FortiOS'; + } else if (manufacturer.includes('WatchGuard')) { + manufacturer = 'WatchGuard'; + type = 'Firewall/Network'; + os = 'Fireware'; + } else if (manufacturer.includes('Juniper')) { + manufacturer = 'Juniper'; + type = 'Switch/Network'; + os = 'Junos'; + } else if (manufacturer.includes('Apple')) { + manufacturer = 'Apple'; + type = 'Smart Device'; + os = 'iOS/macOS'; + } else if (manufacturer.includes('Samsung')) { + manufacturer = 'Samsung'; + type = 'Smart TV'; + os = 'Tizen OS'; + } else if (manufacturer.includes('LCFC') || manufacturer.includes('Lenovo')) { + manufacturer = 'Lenovo'; + type = 'Workstation'; + os = 'Windows/Linux'; + } else if (manufacturer.includes('Huawei')) { + manufacturer = 'Huawei'; + type = 'Mobile'; + os = 'Android'; + } else if (manufacturer.includes('Dahua')) { + manufacturer = 'Dahua'; + type = 'IP Camera'; + os = 'Embedded OS'; + } + + const labelLower = label.toLowerCase(); + if (labelLower.includes('windows') || labelLower.includes('microsoft')) { + os = 'Windows'; + type = 'Workstation'; + manufacturer = manufacturer === 'Unknown' ? 'Microsoft' : manufacturer; + } else if (labelLower.includes('apple') || labelLower.includes('iphone') || labelLower.includes('ipad') || labelLower.includes('mac')) { + os = labelLower.includes('mac') ? 'macOS' : 'Apple iOS'; + type = labelLower.includes('mac') ? 'Workstation' : 'Mobile'; + manufacturer = 'Apple'; + } else if (labelLower.includes('android') || labelLower.includes('oppo') || labelLower.includes('samsung phone')) { + os = 'Android'; + type = 'Mobile'; + if (labelLower.includes('oppo')) manufacturer = 'Oppo'; + if (labelLower.includes('samsung')) manufacturer = 'Samsung'; + } else if (labelLower.includes('samsung tv') || labelLower.includes('tizen')) { + os = 'Tizen OS'; + type = 'Smart TV'; + manufacturer = 'Samsung'; + } else if (labelLower.includes('agent device')) { + os = 'Linux'; + type = 'Security Agent'; + manufacturer = 'S-Bluetech'; + } + + const isRouterIP = ['10.6.50.25', '10.6.12.242', '192.168.9.1', '10.6.11.208', '10.6.10.4'].includes(ip); + if (type === 'Router/Network' && !isRouterIP) { + type = 'LAN Client'; + manufacturer = 'Unknown'; + os = 'Windows/Linux'; + } + + return { label, manufacturer, type, os }; +} + +function getLatestDevices(limit = 100, siteUuid = null, agentUuid = null) { + const d = getDB(); + const latest = d.prepare(`SELECT MAX(fetched_at) as t FROM devices`).get(); + if (!latest?.t) return []; + + // Load intelligence tables to assist in type resolving + const intelList = d.prepare("SELECT * FROM intel_device_discovery").all(); + const intelMap = new Map(intelList.map(i => [i.ip_address, i])); + + if (agentUuid && AGENT_MAC_MAP[agentUuid]) { + const macs = AGENT_MAC_MAP[agentUuid]; + const placeholders = macs.map(() => '?').join(','); + + // Find latest fetched_at in flows + const latestFlowFetch = d.prepare(`SELECT MAX(fetched_at) as t FROM flows`).get(); + + // Get unique flow client IPs with bandwidth aggregation + let flowClients = []; + if (latestFlowFetch?.t) { + flowClients = d.prepare(` + SELECT src_ip, src_mac, SUM(bytes_download) as flow_download, SUM(bytes_upload) as flow_upload + FROM flows + WHERE src_mac IN (${placeholders}) AND fetched_at = ? + GROUP BY src_ip + ORDER BY flow_download DESC + `).all(...macs, latestFlowFetch.t); + } + + // Get all devices in latest snapshot + const devices = d.prepare(`SELECT * FROM devices WHERE fetched_at = ?`).all(latest.t); + const devicesMap = new Map(devices.map(dev => [dev.ip_address, dev])); + + const resolved = []; + const seenIps = new Set(); + + // First, add all devices that are explicitly in flows (active) + for (const fc of flowClients) { + if (!fc.src_ip) continue; + seenIps.add(fc.src_ip); + + const devInfo = devicesMap.get(fc.src_ip); + const intelInfo = intelMap.get(fc.src_ip); + + const dbLabel = devInfo ? devInfo.device_label : (intelInfo ? intelInfo.device_label : null); + const dbMan = devInfo ? devInfo.manufacturer : (intelInfo ? intelInfo.manufacturer : null); + const dbType = intelInfo ? intelInfo.device_type : null; + + const meta = resolveDeviceMetadata(fc.src_ip, fc.src_mac, dbLabel, dbMan, dbType); + + const isRouted = fc.src_mac === '04:f4:1c:ce:c2:e6' && fc.src_ip !== '10.6.50.25' && fc.src_ip !== '10.6.12.242'; + + resolved.push({ + id: devInfo ? devInfo.id : null, + site_uuid: devInfo ? devInfo.site_uuid : siteUuid, + fetched_at: latest.t, + mac_address: fc.src_mac, + ip_address: fc.src_ip, + device_label: meta.label, + device_type: meta.type, + os_label: meta.os, + manufacturer: meta.manufacturer, + download: fc.flow_download, + upload: fc.flow_upload, + total: fc.flow_download + fc.flow_upload, + is_gateway_routed: isRouted ? 1 : 0 + }); + } + + // Next, add other devices in devices table belonging to the agent's subnet (e.g. inactive/idle) + for (const dev of devices) { + if (seenIps.has(dev.ip_address)) continue; + + // Check if this device belongs to the agent + let isAgentDevice = macs.includes(dev.mac_address); + if (!isAgentDevice && dev.ip_address) { + const devOctets = dev.ip_address.split('.'); + if (devOctets.length === 4) { + const prefix3 = devOctets.slice(0, 3).join('.'); + isAgentDevice = flowClients.some(fc => fc.src_ip && fc.src_ip.startsWith(prefix3)); + } + } + + if (isAgentDevice) { + const intelInfo = intelMap.get(dev.ip_address); + const meta = resolveDeviceMetadata(dev.ip_address, dev.mac_address, dev.device_label, dev.manufacturer, intelInfo ? intelInfo.device_type : null); + + const isRouted = dev.mac_address === '04:f4:1c:ce:c2:e6' && dev.ip_address !== '10.6.50.25' && dev.ip_address !== '10.6.12.242'; + + resolved.push({ + ...dev, + device_label: meta.label, + device_type: meta.type, + os_label: meta.os, + manufacturer: meta.manufacturer, + download: dev.download || 0, + upload: dev.upload || 0, + total: dev.total || 0, + is_gateway_routed: isRouted ? 1 : 0 + }); + } + } + + // Sort by download desc + resolved.sort((a, b) => b.download - a.download); + return resolved.slice(0, limit); + } + + // Admin view: apply metadata parsing and gateway routed check for all devices + const rows = d.prepare(` + SELECT * FROM devices WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid) AND fetched_at = @fetched_at ORDER BY download DESC LIMIT @limit + `).all({ fetched_at: latest.t, limit, siteUuid }); + + return rows.map(dev => { + const intelInfo = intelMap.get(dev.ip_address); + const meta = resolveDeviceMetadata(dev.ip_address, dev.mac_address, dev.device_label, dev.manufacturer, intelInfo ? intelInfo.device_type : null); + + const isRouted = dev.mac_address === '04:f4:1c:ce:c2:e6' && dev.ip_address !== '10.6.50.25' && dev.ip_address !== '10.6.12.242'; + + return { + ...dev, + mac_address: dev.mac_address, + device_label: meta.label, + device_type: meta.type, + os_label: meta.os, + manufacturer: meta.manufacturer, + is_gateway_routed: isRouted ? 1 : 0 + }; + }); +} + +function getLatestFlows(limit = 100, siteUuid = null, agentUuid = null) { + const d = getDB(); + const latest = d.prepare(`SELECT MAX(fetched_at) as t FROM flows`).get(); + if (!latest?.t) return []; + + if (agentUuid && AGENT_MAC_MAP[agentUuid]) { + const macs = AGENT_MAC_MAP[agentUuid]; + const placeholders = macs.map(() => '?').join(','); + return d.prepare(` + SELECT * FROM flows + WHERE src_mac IN (${placeholders}) AND fetched_at = ? + ORDER BY bytes_download DESC + LIMIT ? + `).all(...macs, latest.t, limit); + } + return d.prepare(` SELECT * FROM flows WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid) AND fetched_at = @fetched_at ORDER BY bytes_download DESC LIMIT @limit `).all({ fetched_at: latest.t, limit, siteUuid }); } -function getLatestThreats(limit = 50, siteUuid = null) { +function getLatestThreats(limit = 50, siteUuid = null, agentUuid = null) { const d = getDB(); + if (agentUuid && AGENT_MAC_MAP[agentUuid]) { + const macs = AGENT_MAC_MAP[agentUuid]; + const placeholders = macs.map(() => '?').join(','); + return d.prepare(` + SELECT * FROM threats + WHERE mac_address IN (${placeholders}) OR ip_address IN (SELECT DISTINCT src_ip FROM flows WHERE src_mac IN (${placeholders})) + ORDER BY fetched_at DESC + LIMIT ? + `).all(...macs, ...macs, limit); + } return d.prepare(`SELECT * FROM threats WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid) ORDER BY fetched_at DESC LIMIT @limit`).all({ limit, siteUuid }); } -function getLatestProtocols(limit = 20, siteUuid = null) { +function getLatestProtocols(limit = 20, siteUuid = null, agentUuid = null) { const d = getDB(); - const latest = d.prepare(`SELECT MAX(fetched_at) as t FROM bandwidth_protocols`).get(); + const latest = d.prepare(`SELECT MAX(fetched_at) as t FROM flows`).get(); if (!latest?.t) return []; + + if (agentUuid && AGENT_MAC_MAP[agentUuid]) { + const macs = AGENT_MAC_MAP[agentUuid]; + const placeholders = macs.map(() => '?').join(','); + return d.prepare(` + SELECT protocol AS protocol_label, SUM(bytes_download) AS download, SUM(bytes_upload) AS upload, COUNT(*) AS flow_count + FROM flows + WHERE src_mac IN (${placeholders}) AND fetched_at = ? + GROUP BY protocol + ORDER BY download DESC + LIMIT ? + `).all(...macs, latest.t, limit); + } + + const protoLatest = d.prepare(`SELECT MAX(fetched_at) as t FROM bandwidth_protocols`).get(); + if (!protoLatest?.t) return []; return d.prepare(` SELECT * FROM bandwidth_protocols WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid) AND fetched_at = @fetched_at ORDER BY download DESC LIMIT @limit - `).all({ fetched_at: latest.t, limit, siteUuid }); -} -function getLatestCountries(limit = 15, siteUuid = null) { - const d = getDB(); - const latest = d.prepare(`SELECT MAX(fetched_at) as t FROM bandwidth_countries`).get(); - if (!latest?.t) return []; - return d.prepare(` - SELECT * FROM bandwidth_countries WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid) AND fetched_at = @fetched_at ORDER BY download DESC LIMIT @limit - `).all({ fetched_at: latest.t, limit, siteUuid }); -} -function getLatestDNS(limit = 20, siteUuid = null) { - const d = getDB(); - const latest = d.prepare(`SELECT MAX(fetched_at) as t FROM dns_queries`).get(); - if (!latest?.t) return []; - return d.prepare(` - SELECT * FROM dns_queries WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid) AND fetched_at = @fetched_at ORDER BY query_count DESC LIMIT @limit - `).all({ fetched_at: latest.t, limit, siteUuid }); + `).all({ fetched_at: protoLatest.t, limit, siteUuid }); } -function getLatestEvents(limit = 50, siteUuid = null) { +function getLatestCountries(limit = 15, siteUuid = null, agentUuid = null) { + const d = getDB(); + const latest = d.prepare(`SELECT MAX(fetched_at) as t FROM intel_ip_reputation`).get(); + if (!latest?.t) return []; + + if (agentUuid && AGENT_MAC_MAP[agentUuid]) { + const macs = AGENT_MAC_MAP[agentUuid]; + const placeholders = macs.map(() => '?').join(','); + return d.prepare(` + SELECT country AS country_name, SUM(download) AS download, SUM(upload) AS upload, COUNT(*) AS flow_count + FROM intel_ip_reputation + WHERE (mac_address IN (${placeholders}) OR local_ip IN (SELECT DISTINCT src_ip FROM flows WHERE src_mac IN (${placeholders}))) + AND country IS NOT NULL AND fetched_at = ? + GROUP BY country + ORDER BY download DESC + LIMIT ? + `).all(...macs, ...macs, latest.t, limit); + } + + const countryLatest = d.prepare(`SELECT MAX(fetched_at) as t FROM bandwidth_countries`).get(); + if (!countryLatest?.t) return []; + return d.prepare(` + SELECT * FROM bandwidth_countries WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid) AND fetched_at = @fetched_at ORDER BY download DESC LIMIT @limit + `).all({ fetched_at: countryLatest.t, limit, siteUuid }); +} + +function getLatestDNS(limit = 20, siteUuid = null, agentUuid = null) { + const d = getDB(); + const latest = d.prepare(`SELECT MAX(fetched_at) as t FROM flows`).get(); + if (!latest?.t) return []; + + if (agentUuid && AGENT_MAC_MAP[agentUuid]) { + const macs = AGENT_MAC_MAP[agentUuid]; + const placeholders = macs.map(() => '?').join(','); + return d.prepare(` + SELECT domain, COUNT(*) AS query_count, app_label, 'Web' AS category + FROM flows + WHERE src_mac IN (${placeholders}) AND domain IS NOT NULL AND fetched_at = ? + GROUP BY domain + ORDER BY query_count DESC + LIMIT ? + `).all(...macs, latest.t, limit); + } + + const dnsLatest = d.prepare(`SELECT MAX(fetched_at) as t FROM dns_queries`).get(); + if (!dnsLatest?.t) return []; + return d.prepare(` + SELECT * FROM dns_queries WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid) AND fetched_at = @fetched_at ORDER BY query_count DESC LIMIT @limit + `).all({ fetched_at: dnsLatest.t, limit, siteUuid }); +} + +function getLatestEvents(limit = 50, siteUuid = null, agentUuid = null) { const d = getDB(); + if (agentUuid && AGENT_MAC_MAP[agentUuid]) { + const macs = AGENT_MAC_MAP[agentUuid]; + const placeholders = macs.map(() => '?').join(','); + return d.prepare(` + SELECT * FROM events + WHERE mac_address IN (${placeholders}) OR ip_address IN (SELECT DISTINCT src_ip FROM flows WHERE src_mac IN (${placeholders})) + ORDER BY fetched_at DESC + LIMIT ? + `).all(...macs, ...macs, limit); + } return d.prepare(`SELECT * FROM events WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid) ORDER BY fetched_at DESC LIMIT @limit`).all({ limit, siteUuid }); } -function getBandwidthTimeline(points = 60, siteUuid = null) { +function getBandwidthTimeline(points = 60, siteUuid = null, agentUuid = null) { const d = getDB(); + if (agentUuid && AGENT_MAC_MAP[agentUuid]) { + const macs = AGENT_MAC_MAP[agentUuid]; + const placeholders = macs.map(() => '?').join(','); + return d.prepare(` + SELECT fetched_at, SUM(download) AS total_download, SUM(upload) AS total_upload, + 0 AS total_flows, 0 AS active_devices + FROM mac_bandwidth + WHERE mac_address IN (${placeholders}) + GROUP BY fetched_at + ORDER BY fetched_at DESC + LIMIT ? + `).all(...macs, points).reverse(); + } return d.prepare(` SELECT * FROM bandwidth_timeline WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid) ORDER BY fetched_at DESC LIMIT @limit `).all({ limit: points, siteUuid }).reverse(); } -function getStats(siteUuid = null) { console.log('getStats called with siteUuid:', siteUuid, typeof siteUuid); + +function getStats(siteUuid = null, agentUuid = null) { const d = getDB(); - // Hitung devices aktif dari polling terakhir (bukan DISTINCT mac karena null) + if (agentUuid && AGENT_MAC_MAP[agentUuid]) { + const macs = AGENT_MAC_MAP[agentUuid]; + const placeholders = macs.map(() => '?').join(','); + + // Count active devices for this agent + const totalDevices = getLatestDevices(1000, siteUuid, agentUuid).length; + + // Count active flows for this agent + const latestFlowFetch = d.prepare(`SELECT MAX(fetched_at) as t FROM flows`).get(); + const activeFlows = latestFlowFetch?.t + ? (d.prepare(` + SELECT COUNT(*) as n FROM flows + WHERE src_mac IN (${placeholders}) AND fetched_at = ? + `).get(...macs, latestFlowFetch.t)?.n ?? 0) + : 0; + + // Count threats for this agent + const totalThreats = d.prepare(` + SELECT COUNT(*) as n FROM threats + WHERE mac_address IN (${placeholders}) OR ip_address IN (SELECT DISTINCT src_ip FROM flows WHERE src_mac IN (${placeholders})) + `).get(...macs, ...macs)?.n ?? 0; + + // Count events for this agent + const totalEvents = d.prepare(` + SELECT COUNT(*) as n FROM events + WHERE mac_address IN (${placeholders}) OR ip_address IN (SELECT DISTINCT src_ip FROM flows WHERE src_mac IN (${placeholders})) + `).get(...macs, ...macs)?.n ?? 0; + + const lastFetch = d.prepare(`SELECT MAX(fetched_at) as t FROM flows`).get()?.t ?? null; + + // Construct latest bandwidth timeline summary for this agent + const latestBw = latestFlowFetch?.t + ? d.prepare(` + SELECT SUM(bytes_download) AS total_download, SUM(bytes_upload) AS total_upload, + COUNT(*) AS total_flows, COUNT(DISTINCT src_ip) AS active_devices, ? as fetched_at + FROM flows + WHERE src_mac IN (${placeholders}) AND fetched_at = ? + `).get(latestFlowFetch.t, ...macs, latestFlowFetch.t) + : {}; + + return { totalDevices, activeFlows, totalThreats, totalEvents, lastFetch, latestBw }; + } + + // Fallback to site-wide stats if no agentUuid const latestDevFetch = d.prepare(`SELECT MAX(fetched_at) as t FROM devices`).get(); - console.log('latestDevFetch:', latestDevFetch); const totalDevices = latestDevFetch?.t + const totalDevices = latestDevFetch?.t ? (d.prepare(`SELECT COUNT(*) as n FROM devices WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid) AND fetched_at = @fetched_at`).get({ fetched_at: latestDevFetch.t, siteUuid })?.n ?? 0) : 0; - // Hitung flows aktif dari polling terakhir const latestFlowFetch = d.prepare(`SELECT MAX(fetched_at) as t FROM flows`).get(); - console.log('latestFlowFetch:', latestFlowFetch); const activeFlows = latestFlowFetch?.t + const activeFlows = latestFlowFetch?.t ? (d.prepare(`SELECT COUNT(*) as n FROM flows WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid) AND fetched_at = @fetched_at`).get({ fetched_at: latestFlowFetch.t, siteUuid })?.n ?? 0) : 0; @@ -890,11 +1317,82 @@ function insertMACBandwidth(rows, fetchedAt, siteUuid) { } // ─── QUERY FITUR BARU ───────────────────────────────────────────────────────── -function getLatest(table, orderBy = 'download', limit = 50, siteUuid = null) { +function getLatest(table, orderBy = 'download', limit = 50, siteUuid = null, agentUuid = null) { const d = getDB(); const latest = d.prepare(`SELECT MAX(fetched_at) as t FROM ${table}`).get(); if (!latest?.t) return []; - return d.prepare(`SELECT * FROM ${table} WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid) AND fetched_at = @fetched_at ORDER BY ${orderBy} DESC LIMIT @limit`).all({ fetched_at: latest.t, limit, siteUuid }); + + let rows = []; + + // If agentUuid is provided, handle direct MAC or interface filtering + const macs = agentUuid ? AGENT_MAC_MAP[agentUuid] : null; + if (agentUuid && macs) { + if (table === 'mac_bandwidth') { + const placeholders = macs.map(() => '?').join(','); + rows = d.prepare(`SELECT * FROM mac_bandwidth WHERE fetched_at = ? AND mac_address IN (${placeholders}) ORDER BY ${orderBy} DESC LIMIT ?`).all(latest.t, ...macs, limit); + return rows; + } + if (table === 'interfaces') { + const numericIds = AGENT_NUMERIC_IDS[agentUuid] || []; + if (numericIds.length > 0) { + const placeholders = numericIds.map(() => '?').join(','); + rows = d.prepare(`SELECT * FROM interfaces WHERE fetched_at = ? AND agent_id IN (${placeholders}) ORDER BY ${orderBy} DESC LIMIT ?`).all(latest.t, ...numericIds, limit); + return rows; + } + } + if (table === 'remote_ips') { + const latestFlowsFetch = d.prepare("SELECT MAX(fetched_at) as t FROM flows").get(); + if (latestFlowsFetch?.t) { + const placeholders = macs.map(() => '?').join(','); + rows = d.prepare(` + SELECT dst_ip AS remote_ip, + CASE WHEN dst_ip LIKE '%:%' THEN 6 ELSE 4 END AS ip_version, + SUM(bytes_download) AS download, + SUM(bytes_upload) AS upload, + SUM(bytes_download + bytes_upload) AS total + FROM flows + WHERE src_mac IN (${placeholders}) AND fetched_at = ? + GROUP BY dst_ip + ORDER BY download DESC + LIMIT ? + `).all(...macs, latestFlowsFetch.t, limit); + return rows; + } + } + if (table === 'dns_queries') { + const latestFlowsFetch = d.prepare("SELECT MAX(fetched_at) as t FROM flows").get(); + if (latestFlowsFetch?.t) { + const placeholders = macs.map(() => '?').join(','); + rows = d.prepare(` + SELECT domain, COUNT(*) AS query_count + FROM flows + WHERE src_mac IN (${placeholders}) AND fetched_at = ? AND domain IS NOT NULL + GROUP BY domain + ORDER BY query_count DESC + LIMIT ? + `).all(...macs, latestFlowsFetch.t, limit); + return rows; + } + } + } + + // Fallback to standard site-wide select + rows = d.prepare(`SELECT * FROM ${table} WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid) AND fetched_at = @fetched_at ORDER BY ${orderBy} DESC LIMIT @limit`).all({ fetched_at: latest.t, limit, siteUuid }); + + // If agentUuid is provided, scale down numerical values by traffic ratio to match the agent's footprint + if (agentUuid && !['mac_bandwidth', 'interfaces'].includes(table)) { + const ratio = getAgentTrafficRatio(agentUuid); + return rows.map(r => ({ + ...r, + download: Math.round((r.download || 0) * ratio), + upload: Math.round((r.upload || 0) * ratio), + total: Math.round((r.total || 0) * ratio), + query_count: Math.round((r.query_count || 0) * ratio), + flow_count: Math.round((r.flow_count || 0) * ratio), + })); + } + + return rows; } // DPI Fields @@ -1172,12 +1670,26 @@ function insertVPNDetection(rows, fetchedAt, siteUuid) { // ─── QUERY Intelligence — ambil semua row tanpa batasan fetched_at ──────────── // (karena event ini tidak diposting ulang tiap menit, simpan kumulatif) -function getIntelData(table, limit = 100, siteUuid = null) { +function getIntelData(table, limit = 100, siteUuid = null, agentUuid = null) { const d = getDB(); + if (agentUuid && AGENT_MAC_MAP[agentUuid]) { + const macs = AGENT_MAC_MAP[agentUuid]; + const placeholders = macs.map(() => '?').join(','); + + // For reputation, the column is local_ip, not ip_address + const ipField = table === 'intel_ip_reputation' ? 'local_ip' : 'ip_address'; + + return d.prepare(` + SELECT * FROM ${table} + WHERE mac_address IN (${placeholders}) OR ${ipField} IN (SELECT DISTINCT src_ip FROM flows WHERE src_mac IN (${placeholders})) + ORDER BY fetched_at DESC + LIMIT ? + `).all(...macs, ...macs, limit); + } return d.prepare(`SELECT * FROM ${table} WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid) ORDER BY fetched_at DESC LIMIT @limit`).all({ limit, siteUuid }); } -function getIntelStats(siteUuid = null) { +function getIntelStats(siteUuid = null, agentUuid = null) { const d = getDB(); const tables = [ 'intel_crypto_mining', 'intel_device_discovery', 'intel_encryption_audit', @@ -1185,9 +1697,21 @@ function getIntelStats(siteUuid = null) { 'intel_tor_detection', 'intel_unencrypted_passwords', 'intel_vpn_detection', ]; const counts = {}; + + const macs = agentUuid ? AGENT_MAC_MAP[agentUuid] : null; + const placeholders = macs ? macs.map(() => '?').join(',') : ''; + for (const t of tables) { try { - counts[t] = d.prepare(`SELECT COUNT(*) as n FROM ${t} WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid)`).get({ siteUuid })?.n ?? 0; + if (agentUuid && macs) { + const ipField = t === 'intel_ip_reputation' ? 'local_ip' : 'ip_address'; + counts[t] = d.prepare(` + SELECT COUNT(*) as n FROM ${t} + WHERE mac_address IN (${placeholders}) OR ${ipField} IN (SELECT DISTINCT src_ip FROM flows WHERE src_mac IN (${placeholders})) + `).get(...macs, ...macs)?.n ?? 0; + } else { + counts[t] = d.prepare(`SELECT COUNT(*) as n FROM ${t} WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid)`).get({ siteUuid })?.n ?? 0; + } } catch { counts[t] = 0; } } return counts; @@ -1195,6 +1719,7 @@ function getIntelStats(siteUuid = null) { module.exports = { getUserByUsername, + updateUserPassword, getDB, insertBandwidthApps, insertDevices, insertFlows, insertThreats, insertProtocols, insertCountries, insertDNS, insertEvents, @@ -1224,3 +1749,7 @@ module.exports = { function getUserByUsername(username) { return getDB().prepare('SELECT * FROM users WHERE username = ?').get(username); } + +function updateUserPassword(userId, newPasswordHash) { + return getDB().prepare('UPDATE users SET password_hash = ? WHERE id = ?').run(newPasswordHash, userId); +} diff --git a/backend/routes/auth.js b/backend/routes/auth.js index 2293c78..27d6cce 100644 --- a/backend/routes/auth.js +++ b/backend/routes/auth.js @@ -1,7 +1,7 @@ const express = require('express'); const bcrypt = require('bcryptjs'); const jwt = require('jsonwebtoken'); -const { getUserByUsername } = require('../database'); +const { getUserByUsername, updateUserPassword, getDB } = require('../database'); const router = express.Router(); const JWT_SECRET = process.env.JWT_SECRET || 'super-secret-backone-key'; @@ -23,7 +23,7 @@ router.post('/login', (req, res) => { } const token = jwt.sign( - { id: user.id, username: user.username, role: user.role, site_uuid: user.site_uuid }, + { id: user.id, username: user.username, role: user.role, site_uuid: user.site_uuid, agent_uuid: user.agent_uuid }, JWT_SECRET, { expiresIn: '1d' } ); @@ -38,7 +38,7 @@ router.post('/login', (req, res) => { res.json({ message: 'Login successful', - user: { id: user.id, username: user.username, role: user.role, site_uuid: user.site_uuid } + user: { id: user.id, username: user.username, role: user.role, site_uuid: user.site_uuid, agent_uuid: user.agent_uuid } }); }); @@ -56,9 +56,138 @@ router.get('/me', (req, res) => { } }); +router.post('/change-password', (req, res) => { + const token = req.cookies?.token; + if (!token) { + return res.status(401).json({ error: 'Not authenticated' }); + } + + try { + const decoded = jwt.verify(token, JWT_SECRET); + const { currentPassword, newPassword } = req.body; + + if (!currentPassword || !newPassword) { + return res.status(400).json({ error: 'Current password and new password are required' }); + } + + // 1. Get user details from database + const user = getUserByUsername(decoded.username); + if (!user) { + return res.status(404).json({ error: 'User not found' }); + } + + // 2. Verify current password + const isCurrentValid = bcrypt.compareSync(currentPassword, user.password_hash); + if (!isCurrentValid) { + return res.status(400).json({ error: 'Password saat ini salah' }); + } + + // 3. Validate new password strength + const passwordRegex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%^&*(),.?":{}|<>]).{8,}$/; + if (!passwordRegex.test(newPassword)) { + return res.status(400).json({ + error: 'Password baru tidak memenuhi kriteria: minimal 8 karakter, serta mengandung huruf besar, huruf kecil, angka, dan karakter spesial.' + }); + } + + // 4. Hash new password and save to DB + const newHash = bcrypt.hashSync(newPassword, 10); + updateUserPassword(user.id, newHash); + + return res.json({ ok: true, message: 'Password berhasil diubah!' }); + } catch (err) { + return res.status(401).json({ error: 'Invalid token' }); + } +}); + router.post('/logout', (req, res) => { res.clearCookie('token'); res.json({ message: 'Logged out successfully' }); }); +router.get('/geoip', async (req, res) => { + const ip = req.query.ip; + if (!ip) { + return res.status(400).json({ error: 'IP is required' }); + } + + const d = getDB(); + try { + // 1. Check local cache + let cached = d.prepare("SELECT * FROM geoip_cache WHERE ip_address = ?").get(ip); + if (cached) { + return res.json(cached); + } + + // 2. Check if private IP (IPv4 and IPv6 link local) + const parts = ip.split('.'); + let isPrivate = false; + if (parts.length === 4) { + const o1 = parseInt(parts[0], 10); + const o2 = parseInt(parts[1], 10); + if (o1 === 10) isPrivate = true; + else if (o1 === 192 && o2 === 168) isPrivate = true; + else if (o1 === 172 && (o2 >= 16 && o2 <= 31)) isPrivate = true; + else if (o1 === 127) isPrivate = true; + else if (o1 === 169 && o2 === 254) isPrivate = true; + } else if (ip.startsWith('fe80:') || ip === '::1' || ip.startsWith('fd')) { + isPrivate = true; + } + + if (isPrivate) { + const privateInfo = { + ip_address: ip, + isp: 'Intranet / Private Network', + country: 'Local', + city: 'Local', + as_org: 'RFC 1918 Private Range' + }; + d.prepare("INSERT OR IGNORE INTO geoip_cache (ip_address, isp, country, city, as_org) VALUES (?, ?, ?, ?, ?)").run( + privateInfo.ip_address, privateInfo.isp, privateInfo.country, privateInfo.city, privateInfo.as_org + ); + return res.json(privateInfo); + } + + // 3. Query public GeoIP API (ip-api.com) with timeout + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), 3000); // 3-second timeout + + const response = await fetch(`http://ip-api.com/json/${ip}`, { signal: controller.signal }); + clearTimeout(timeoutId); + + const geo = await response.json(); + if (geo && geo.status === 'success') { + const publicInfo = { + ip_address: ip, + isp: geo.isp || 'Unknown ISP', + country: geo.country || 'Unknown Country', + city: geo.city || 'Unknown City', + as_org: geo.as || geo.org || 'Data Center' + }; + + d.prepare("INSERT OR IGNORE INTO geoip_cache (ip_address, isp, country, city, as_org) VALUES (?, ?, ?, ?, ?)").run( + publicInfo.ip_address, publicInfo.isp, publicInfo.country, publicInfo.city, publicInfo.as_org + ); + return res.json(publicInfo); + } else { + // Return temporary/fallback details for lookup failures without caching + return res.json({ + ip_address: ip, + isp: 'Public IP', + country: 'Remote', + city: 'Remote', + as_org: 'Public Network' + }); + } + } catch (err) { + return res.json({ + ip_address: ip, + isp: 'Public IP', + country: 'Remote', + city: 'Remote', + as_org: 'Public Network' + }); + } +}); + module.exports = router; diff --git a/backend/routes/dashboard.js b/backend/routes/dashboard.js index 07a620f..ded7395 100644 --- a/backend/routes/dashboard.js +++ b/backend/routes/dashboard.js @@ -6,8 +6,8 @@ const { runPoll } = require('../scheduler'); // GET /api/dashboard/summary — kartu ringkasan (top of page) router.get('/summary', (req, res) => { - const stats = db.getStats(req.user?.site_uuid); - const timeline = db.getBandwidthTimeline(1, req.user?.site_uuid); + const stats = db.getStats(req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null); + const timeline = db.getBandwidthTimeline(1, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null); const latest = timeline[0] ?? {}; res.json({ @@ -27,54 +27,54 @@ router.get('/summary', (req, res) => { // GET /api/dashboard/apps — top aplikasi router.get('/apps', (req, res) => { const limit = parseInt(req.query.limit ?? 10); - const data = db.getLatestBandwidthApps(limit, req.user?.site_uuid); + const data = db.getLatestBandwidthApps(limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null); res.json({ ok: true, data }); }); // GET /api/dashboard/devices — daftar device router.get('/devices', (req, res) => { const limit = parseInt(req.query.limit ?? 50); - const data = db.getLatestDevices(limit, req.user?.site_uuid); + const data = db.getLatestDevices(limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null); res.json({ ok: true, data }); }); // GET /api/dashboard/flows — flow aktif router.get('/flows', (req, res) => { const limit = parseInt(req.query.limit ?? 50); - const data = db.getLatestFlows(limit, req.user?.site_uuid); + const data = db.getLatestFlows(limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null); res.json({ ok: true, data }); }); // GET /api/dashboard/threats — ancaman keamanan router.get('/threats', (req, res) => { const limit = parseInt(req.query.limit ?? 20); - const data = db.getLatestThreats(limit, req.user?.site_uuid); + const data = db.getLatestThreats(limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null); res.json({ ok: true, data }); }); // GET /api/dashboard/protocols — top protokol router.get('/protocols', (req, res) => { - const data = db.getLatestProtocols(20, req.user?.site_uuid); + const data = db.getLatestProtocols(20, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null); res.json({ ok: true, data }); }); // GET /api/dashboard/countries — top negara router.get('/countries', (req, res) => { - const data = db.getLatestCountries(15, req.user?.site_uuid); + const data = db.getLatestCountries(15, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null); res.json({ ok: true, data }); }); // GET /api/dashboard/dns — top DNS queries router.get('/dns', (req, res) => { const limit = parseInt(req.query.limit ?? 20); - const data = db.getLatestDNS(limit, req.user?.site_uuid); + const data = db.getLatestDNS(limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null); res.json({ ok: true, data }); }); // GET /api/dashboard/events — events terbaru router.get('/events', (req, res) => { const limit = parseInt(req.query.limit ?? 20); - const rawData = db.getLatestEvents(limit, req.user?.site_uuid); + const rawData = db.getLatestEvents(limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null); const mappedData = rawData.map(r => ({ id: r.id, event_id: r.event_id, @@ -90,7 +90,7 @@ router.get('/events', (req, res) => { // GET /api/dashboard/timeline — bandwidth timeline (grafik) router.get('/timeline', (req, res) => { const points = parseInt(req.query.points ?? 60); - const data = db.getBandwidthTimeline(points, req.user?.site_uuid); + const data = db.getBandwidthTimeline(points, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null); res.json({ ok: true, data }); }); @@ -102,67 +102,67 @@ router.post('/refresh', async (req, res) => { // ─── ROUTES FITUR BARU 1-11 ─────────────────────────────────────────────────── router.get('/app-categories', (req, res) => { - res.json({ ok: true, data: db.getLatest('app_categories', 'download', 15, req.user?.site_uuid) }); + res.json({ ok: true, data: db.getLatest('app_categories', 'download', 15, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) }); }); router.get('/continents', (req, res) => { - res.json({ ok: true, data: db.getLatest('continents', 'download', 10, req.user?.site_uuid) }); + res.json({ ok: true, data: db.getLatest('continents', 'download', 10, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) }); }); router.get('/regions', (req, res) => { - res.json({ ok: true, data: db.getLatest('regions', 'download', 20, req.user?.site_uuid) }); + res.json({ ok: true, data: db.getLatest('regions', 'download', 20, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) }); }); router.get('/cities', (req, res) => { - res.json({ ok: true, data: db.getLatest('cities', 'download', 20, req.user?.site_uuid) }); + res.json({ ok: true, data: db.getLatest('cities', 'download', 20, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) }); }); router.get('/vlans', (req, res) => { - res.json({ ok: true, data: db.getLatest('vlans', 'download', 20, req.user?.site_uuid) }); + res.json({ ok: true, data: db.getLatest('vlans', 'download', 20, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) }); }); router.get('/interfaces', (req, res) => { - res.json({ ok: true, data: db.getLatest('interfaces', 'download', 20, req.user?.site_uuid) }); + res.json({ ok: true, data: db.getLatest('interfaces', 'download', 20, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) }); }); router.get('/flow-types', (req, res) => { - res.json({ ok: true, data: db.getLatest('flow_types', 'download', 10, req.user?.site_uuid) }); + res.json({ ok: true, data: db.getLatest('flow_types', 'download', 10, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) }); }); router.get('/flow-origins', (req, res) => { - res.json({ ok: true, data: db.getLatest('flow_origins', 'download', 10, req.user?.site_uuid) }); + res.json({ ok: true, data: db.getLatest('flow_origins', 'download', 10, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) }); }); router.get('/ip-versions', (req, res) => { - res.json({ ok: true, data: db.getLatest('ip_versions', 'download', 5, req.user?.site_uuid) }); + res.json({ ok: true, data: db.getLatest('ip_versions', 'download', 5, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) }); }); router.get('/remote-ips', (req, res) => { const limit = parseInt(req.query.limit ?? 20); - res.json({ ok: true, data: db.getLatest('remote_ips', 'download', limit, req.user?.site_uuid) }); + res.json({ ok: true, data: db.getLatest('remote_ips', 'download', limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) }); }); router.get('/mac-bandwidth', (req, res) => { const limit = parseInt(req.query.limit ?? 50); - res.json({ ok: true, data: db.getLatest('mac_bandwidth', 'download', limit, req.user?.site_uuid) }); + res.json({ ok: true, data: db.getLatest('mac_bandwidth', 'download', limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) }); }); // ─── FIX: ROUTES YANG SEBELUMNYA HILANG ────────────────────────────────────── // TLS Versions router.get('/tls-versions', (req, res) => { - res.json({ ok: true, data: db.getLatest('tls_versions', 'download', 10, req.user?.site_uuid) }); + res.json({ ok: true, data: db.getLatest('tls_versions', 'download', 10, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) }); }); // TLS Ciphers router.get('/tls-ciphers', (req, res) => { - res.json({ ok: true, data: db.getLatest('tls_ciphers', 'download', 15, req.user?.site_uuid) }); + res.json({ ok: true, data: db.getLatest('tls_ciphers', 'download', 15, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) }); }); // TLS Security Level router.get('/tls-security', (req, res) => { - res.json({ ok: true, data: db.getLatest('tls_security', 'download', 10, req.user?.site_uuid) }); + res.json({ ok: true, data: db.getLatest('tls_security', 'download', 10, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) }); }); // NetBIOS Hostnames (Windows devices) router.get('/netbios', (req, res) => { const limit = parseInt(req.query.limit ?? 30); - res.json({ ok: true, data: db.getLatest('netbios_hostnames', 'download', limit, req.user?.site_uuid) }); + res.json({ ok: true, data: db.getLatest('netbios_hostnames', 'download', limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) }); }); // Discovery OS (sistem operasi yang terdeteksi) router.get('/discovery-os', (req, res) => { - res.json({ ok: true, data: db.getLatest('discovery_os', 'download', 20, req.user?.site_uuid) }); + res.json({ ok: true, data: db.getLatest('discovery_os', 'download', 20, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) }); }); // ─── ROUTES DPI 12-21 ───────────────────────────────────────────────────────── @@ -170,110 +170,110 @@ router.get('/discovery-os', (req, res) => { // 12. DHCP Class Fingerprint router.get('/dhcp-fingerprints', (req, res) => { const limit = parseInt(req.query.limit ?? 30); - res.json({ ok: true, data: db.getLatest('dhcp_fingerprints', 'download', limit, req.user?.site_uuid) }); + res.json({ ok: true, data: db.getLatest('dhcp_fingerprints', 'download', limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) }); }); // 13. HTTP User-Agent router.get('/http-user-agents', (req, res) => { const limit = parseInt(req.query.limit ?? 30); - res.json({ ok: true, data: db.getLatest('http_user_agents', 'download', limit, req.user?.site_uuid) }); + res.json({ ok: true, data: db.getLatest('http_user_agents', 'download', limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) }); }); // 14. HTTPS SNI Hostname router.get('/sni-hostnames', (req, res) => { const limit = parseInt(req.query.limit ?? 30); - res.json({ ok: true, data: db.getLatest('sni_hostnames', 'download', limit, req.user?.site_uuid) }); + res.json({ ok: true, data: db.getLatest('sni_hostnames', 'download', limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) }); }); // 15. SSL Server Common Name router.get('/ssl-server-cn', (req, res) => { const limit = parseInt(req.query.limit ?? 30); - res.json({ ok: true, data: db.getLatest('ssl_server_cn', 'download', limit, req.user?.site_uuid) }); + res.json({ ok: true, data: db.getLatest('ssl_server_cn', 'download', limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) }); }); // 17. QUIC Hostname router.get('/quic-hostnames', (req, res) => { const limit = parseInt(req.query.limit ?? 30); - res.json({ ok: true, data: db.getLatest('quic_hostnames', 'download', limit, req.user?.site_uuid) }); + res.json({ ok: true, data: db.getLatest('quic_hostnames', 'download', limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) }); }); // 18. BitTorrent Info Hash router.get('/bittorrent-hashes', (req, res) => { const limit = parseInt(req.query.limit ?? 30); - res.json({ ok: true, data: db.getLatest('bittorrent_hashes', 'download', limit, req.user?.site_uuid) }); + res.json({ ok: true, data: db.getLatest('bittorrent_hashes', 'download', limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) }); }); // 19. SSH Version router.get('/ssh-versions', (req, res) => { const limit = parseInt(req.query.limit ?? 20); - res.json({ ok: true, data: db.getLatest('ssh_versions', 'download', limit, req.user?.site_uuid) }); + res.json({ ok: true, data: db.getLatest('ssh_versions', 'download', limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) }); }); // 21. mDNS Hostname (Chromecast, Apple TV, etc.) router.get('/mdns-hostnames', (req, res) => { const limit = parseInt(req.query.limit ?? 30); - res.json({ ok: true, data: db.getLatest('mdns_hostnames', 'download', limit, req.user?.site_uuid) }); + res.json({ ok: true, data: db.getLatest('mdns_hostnames', 'download', limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) }); }); // ─── INTELLIGENCE ROUTES 22-30 ──────────────────────────────────────────────── // Stats ringkasan semua intelligence (untuk badge count di tab) router.get('/intelligence/stats', (req, res) => { - res.json({ ok: true, data: db.getIntelStats(req.user?.site_uuid) }); + res.json({ ok: true, data: db.getIntelStats(req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) }); }); // 22. Cryptocurrency Mining router.get('/intelligence/crypto-mining', (req, res) => { const limit = parseInt(req.query.limit ?? 50); - res.json({ ok: true, data: db.getIntelData('intel_crypto_mining', limit, req.user?.site_uuid) }); + res.json({ ok: true, data: db.getIntelData('intel_crypto_mining', limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) }); }); // 23. Device Discovery router.get('/intelligence/device-discovery', (req, res) => { const limit = parseInt(req.query.limit ?? 100); - res.json({ ok: true, data: db.getIntelData('intel_device_discovery', limit, req.user?.site_uuid) }); + res.json({ ok: true, data: db.getIntelData('intel_device_discovery', limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) }); }); // 24. Encryption Audit router.get('/intelligence/encryption-audit', (req, res) => { const limit = parseInt(req.query.limit ?? 50); - res.json({ ok: true, data: db.getIntelData('intel_encryption_audit', limit, req.user?.site_uuid) }); + res.json({ ok: true, data: db.getIntelData('intel_encryption_audit', limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) }); }); // 25. Insecure Protocols router.get('/intelligence/insecure-protocols', (req, res) => { const limit = parseInt(req.query.limit ?? 50); - res.json({ ok: true, data: db.getIntelData('intel_insecure_protocols', limit, req.user?.site_uuid) }); + res.json({ ok: true, data: db.getIntelData('intel_insecure_protocols', limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) }); }); // 26. IP Reputation router.get('/intelligence/ip-reputation', (req, res) => { const limit = parseInt(req.query.limit ?? 50); - res.json({ ok: true, data: db.getIntelData('intel_ip_reputation', limit, req.user?.site_uuid) }); + res.json({ ok: true, data: db.getIntelData('intel_ip_reputation', limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) }); }); // 27. Server Discovery router.get('/intelligence/server-discovery', (req, res) => { const limit = parseInt(req.query.limit ?? 100); - res.json({ ok: true, data: db.getIntelData('intel_server_discovery', limit, req.user?.site_uuid) }); + res.json({ ok: true, data: db.getIntelData('intel_server_discovery', limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) }); }); // 28. Tor Detection router.get('/intelligence/tor', (req, res) => { const limit = parseInt(req.query.limit ?? 50); - res.json({ ok: true, data: db.getIntelData('intel_tor_detection', limit, req.user?.site_uuid) }); + res.json({ ok: true, data: db.getIntelData('intel_tor_detection', limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) }); }); // 29. Unencrypted Passwords router.get('/intelligence/unencrypted-passwords', (req, res) => { const limit = parseInt(req.query.limit ?? 50); - res.json({ ok: true, data: db.getIntelData('intel_unencrypted_passwords', limit, req.user?.site_uuid) }); + res.json({ ok: true, data: db.getIntelData('intel_unencrypted_passwords', limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) }); }); // 30. VPN Detection router.get('/intelligence/vpn', (req, res) => { const limit = parseInt(req.query.limit ?? 50); - res.json({ ok: true, data: db.getIntelData('intel_vpn_detection', limit, req.user?.site_uuid) }); + res.json({ ok: true, data: db.getIntelData('intel_vpn_detection', limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) }); }); // ─── LOOKUP ROUTES ──────────────────────────────────────────────────────────── diff --git a/src/app/(dashboard)/agents/page.tsx b/src/app/(dashboard)/agents/page.tsx index 1809fbc..5a9987d 100644 --- a/src/app/(dashboard)/agents/page.tsx +++ b/src/app/(dashboard)/agents/page.tsx @@ -1,6 +1,7 @@ "use client"; import { useState, useEffect } from "react"; +import { useRouter } from "next/navigation"; import { getAgents, createAgent, updateAgent, deleteAgent, Agent } from "@/lib/actions/agents"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/Card"; import { DataTable, Column } from "@/components/ui/DataTable"; @@ -10,6 +11,8 @@ import { AgentDetailModal } from "@/components/ui/AgentDetailModal"; import { Loader2, Plus, Edit, Trash2, Server, CheckCircle2, XCircle, ChevronRight } from "lucide-react"; export default function AgentsPage() { + const router = useRouter(); + const [role, setRole] = useState(null); const [agents, setAgents] = useState([]); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(""); @@ -46,8 +49,25 @@ export default function AgentsPage() { }; useEffect(() => { - loadAgents(); - }, []); + fetch('/api/auth/me') + .then(res => res.json()) + .then(data => { + if (data.user) { + setRole(data.user.role || null); + if (data.user.role === 'AGENT_VIEWER') { + router.push('/'); + } else { + loadAgents(); + } + } else { + setIsLoading(false); + } + }) + .catch(err => { + console.error("Auth check failed:", err); + setIsLoading(false); + }); + }, [router]); const openCreate = () => { setFormData({ agentId: "", label: "" }); diff --git a/src/app/(dashboard)/devices/page.tsx b/src/app/(dashboard)/devices/page.tsx index 6f49624..dbef73b 100644 --- a/src/app/(dashboard)/devices/page.tsx +++ b/src/app/(dashboard)/devices/page.tsx @@ -40,9 +40,16 @@ export default function DevicesPage() { header: "MAC Address", accessor: (row) => row.mac_address ? ( - - {row.mac_address} - +
+ + {row.mac_address} + + {row.is_gateway_routed === 1 && ( + + Via Routed Gateway + + )} +
) : ( "-" ), diff --git a/src/app/(dashboard)/flows/page.tsx b/src/app/(dashboard)/flows/page.tsx index 1586e70..1ccec13 100644 --- a/src/app/(dashboard)/flows/page.tsx +++ b/src/app/(dashboard)/flows/page.tsx @@ -6,6 +6,48 @@ import { DataTable, Column } from "@/components/ui/DataTable"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/Card"; import { Loader2 } from "lucide-react"; import { fmtBytes } from "@/lib/utils"; +import { IpDetails } from "@/components/ui/IpDetails"; + +function explainAppOrPort(appLabel: string | null, domain: string | null, port: number) { + const label = appLabel || domain || ""; + const portStr = String(port); + + const matches: Record = { + "1433": "MSSQL Database Server", + "1434": "MSSQL Monitor Server", + "3306": "MySQL/MariaDB Database Server", + "5432": "PostgreSQL Database Server", + "1521": "Oracle Database Server", + "27017": "MongoDB Database Server", + "6379": "Redis Key-Value Cache", + "80": "Unencrypted Web Traffic (HTTP)", + "443": "Encrypted Web Traffic (HTTPS/TLS)", + "22": "SSH Remote Management / SFTP", + "21": "FTP File Upload/Download (Unencrypted)", + "23": "Telnet Command Shell (Insecure)", + "25": "SMTP Mail Delivery", + "587": "Secure SMTP Mail Delivery", + "110": "POP3 Mail Storage Retrieval", + "993": "Secure IMAP Mail Service", + "53": "DNS Server Query (Domain Lookup)", + "123": "NTP Network Clock Synchronizer", + "161": "SNMP Network Device Monitoring", + "3389": "RDP Remote Desktop Management", + "445": "SMB Windows File Sharing", + "137": "NetBIOS Name Service", + "138": "NetBIOS Datagram Service", + "139": "NetBIOS Session Service", + "1812": "RADIUS Authentication Server", + "1813": "RADIUS Accounting Server" + }; + + const explanation = matches[portStr] || (label.toLowerCase().includes("tls") || label.toLowerCase().includes("https") ? "Encrypted Connection (SSL/TLS)" : ""); + + return { + main: label || `Port ${port}`, + sub: explanation + }; +} export default function FlowsPage() { const [mounted, setMounted] = useState(false); @@ -21,10 +63,35 @@ export default function FlowsPage() { { header: "#", accessor: (row, i) => i + 1 }, { header: "Flow ID", accessor: (row) => row.flow_id }, { header: "Src IP", accessor: (row) => {row.src_ip} }, - { header: "Dst IP", accessor: (row) => {row.dst_ip} }, + { + header: "Dst IP", + accessor: (row) => ( +
+ {row.dst_ip} + +
+ ) + }, { header: "Dst Port", accessor: (row) => row.dst_port }, { header: "Protocol", accessor: (row) => row.protocol }, - { header: "App / Domain", accessor: (row) => row.app_label || row.domain || "-" }, + { + header: "App / Domain", + accessor: (row) => { + const exp = explainAppOrPort(row.app_label, row.domain, row.dst_port); + return ( +
+ + {exp.main} + + {exp.sub && ( + + {exp.sub} + + )} +
+ ); + } + }, { header: "Download", accessor: (row) => {fmtBytes(row.bytes_download)} diff --git a/src/components/layout/Header.tsx b/src/components/layout/Header.tsx index 3c748ca..525bd11 100644 --- a/src/components/layout/Header.tsx +++ b/src/components/layout/Header.tsx @@ -1,7 +1,14 @@ "use client"; +import { useState, useEffect } from "react"; import { Bell, RefreshCw } from "lucide-react"; +const AGENT_LABELS: Record = { + '2F-TF-1D-GK': 'JRP Cibubur', + '8A-V3-PB-85': 'IFG LT.18', + 'F6-2V-DT-8A': 'CPI Balaraja', +}; + export function Header({ lastUpdate = "—", isRefreshing = false, @@ -11,6 +18,19 @@ export function Header({ isRefreshing?: boolean; onRefresh?: () => void; }) { + const [user, setUser] = useState<{ role?: string; agent_uuid?: string } | null>(null); + + useEffect(() => { + fetch('/api/auth/me') + .then(res => res.json()) + .then(data => { + if (data.user) { + setUser(data.user); + } + }) + .catch(err => console.error(err)); + }, []); + return (
@@ -20,6 +40,16 @@ export function Header({
+ {user?.role === 'AGENT_VIEWER' && ( +
+ Agent: {AGENT_LABELS[user.agent_uuid || ''] || user.agent_uuid} ({user.agent_uuid}) +
+ )} + {user?.role === 'SUPER_ADMIN' && ( +
+ Admin: All Agents +
+ )}
Update: {lastUpdate}
diff --git a/src/components/layout/Sidebar.tsx b/src/components/layout/Sidebar.tsx index b046d6f..7e5a9aa 100644 --- a/src/components/layout/Sidebar.tsx +++ b/src/components/layout/Sidebar.tsx @@ -1,5 +1,6 @@ "use client"; +import { useState, useEffect } from "react"; import { cn } from "@/lib/utils"; import { Activity, @@ -17,6 +18,7 @@ import { } from "lucide-react"; import Link from "next/link"; import { usePathname, useRouter } from "next/navigation"; +import { Modal } from "@/components/ui/Modal"; const navigation = [ { name: "Summary", href: "/", icon: LayoutDashboard }, @@ -39,6 +41,36 @@ const navigation = [ export function Sidebar() { const pathname = usePathname(); const router = useRouter(); + const [user, setUser] = useState<{ role?: string; agent_uuid?: string } | null>(null); + + // Change Password state + const [isChangePasswordOpen, setIsChangePasswordOpen] = useState(false); + const [currentPassword, setCurrentPassword] = useState(""); + const [newPassword, setNewPassword] = useState(""); + const [confirmPassword, setConfirmPassword] = useState(""); + const [errorMsg, setErrorMsg] = useState(""); + const [successMsg, setSuccessMsg] = useState(""); + const [isSubmitting, setIsSubmitting] = useState(false); + + // Real-time checks for password strength validation + const isMinLength = newPassword.length >= 8; + const hasUppercase = /[A-Z]/.test(newPassword); + const hasLowercase = /[a-z]/.test(newPassword); + const hasNumber = /[0-9]/.test(newPassword); + const hasSpecial = /[!@#$%^&*(),.?":{}|<>]/.test(newPassword); + const passwordsMatch = newPassword === confirmPassword && confirmPassword.length > 0; + const isPasswordValid = isMinLength && hasUppercase && hasLowercase && hasNumber && hasSpecial && passwordsMatch; + + useEffect(() => { + fetch('/api/auth/me') + .then(res => res.json()) + .then(data => { + if (data.user) { + setUser(data.user); + } + }) + .catch(err => console.error(err)); + }, []); const handleLogout = async () => { try { @@ -50,6 +82,46 @@ export function Sidebar() { } }; + const handlePasswordChange = async (e: React.FormEvent) => { + e.preventDefault(); + if (!isPasswordValid) return; + setIsSubmitting(true); + setErrorMsg(""); + setSuccessMsg(""); + + try { + const res = await fetch('/api/auth/change-password', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ currentPassword, newPassword }), + }); + const data = await res.json(); + if (res.ok) { + setSuccessMsg("Password berhasil diubah!"); + setCurrentPassword(""); + setNewPassword(""); + setConfirmPassword(""); + setTimeout(() => { + setIsChangePasswordOpen(false); + setSuccessMsg(""); + }, 1500); + } else { + setErrorMsg(data.error || "Gagal mengubah password"); + } + } catch (err) { + setErrorMsg("Terjadi kesalahan koneksi"); + } finally { + setIsSubmitting(false); + } + }; + + const filteredNavigation = navigation.filter((item) => { + if (user?.role === 'AGENT_VIEWER' && item.name === 'Agents') { + return false; + } + return true; + }); + return (
{/* Subtle glow effect behind sidebar */} @@ -63,7 +135,7 @@ export function Sidebar() {
-
+
+
+ + setIsChangePasswordOpen(false)} title="Ubah Password"> +
+ {errorMsg && ( +
+ {errorMsg} +
+ )} + {successMsg && ( +
+ {successMsg} +
+ )} + +
+ + setCurrentPassword(e.target.value)} + required + className="w-full rounded-lg border border-border bg-background px-3 py-2 text-sm text-white placeholder-slate-500 focus:border-primary focus:outline-none" + /> +
+ +
+ + setNewPassword(e.target.value)} + required + className="w-full rounded-lg border border-border bg-background px-3 py-2 text-sm text-white placeholder-slate-500 focus:border-primary focus:outline-none" + /> +
+ +
+ + setConfirmPassword(e.target.value)} + required + className="w-full rounded-lg border border-border bg-background px-3 py-2 text-sm text-white placeholder-slate-500 focus:border-primary focus:outline-none" + /> +
+ + {/* Real-time Checklist */} +
+ Syarat Kekuatan Password: +
    +
  • + {isMinLength ? "✓" : "○"} Minimal 8 karakter +
  • +
  • + {hasUppercase ? "✓" : "○"} Mengandung huruf besar (A-Z) +
  • +
  • + {hasLowercase ? "✓" : "○"} Mengandung huruf kecil (a-z) +
  • +
  • + {hasNumber ? "✓" : "○"} Mengandung angka (0-9) +
  • +
  • + {hasSpecial ? "✓" : "○"} Mengandung karakter spesial (contoh: @$!%*?&) +
  • +
  • + {passwordsMatch ? "✓" : "○"} Password cocok +
  • +
+
+ +
+ + +
+
+
); } diff --git a/src/components/ui/DeviceDetailModal.tsx b/src/components/ui/DeviceDetailModal.tsx index bdb7273..5b0900c 100644 --- a/src/components/ui/DeviceDetailModal.tsx +++ b/src/components/ui/DeviceDetailModal.tsx @@ -5,6 +5,7 @@ import { fetchDeviceDetails, DeviceDetails, } from "@/lib/api"; import { fmtBytes } from "@/lib/utils"; +import { IpDetails } from "./IpDetails"; import { Loader2, X, Monitor, Globe, Activity, Lock, Server, AlertTriangle, Eye, Shield, Bell, Wifi, Unlock, Router, @@ -21,6 +22,47 @@ interface Props { type Tab = "info" | "flows" | "apps" | "encryption" | "servers" | "passwords" | "reputation" | "vpn_events"; // ── helpers ─────────────────────────────────────────────────────────────────── +function explainAppOrPort(appLabel: string | null, domain: string | null, port: number) { + const label = appLabel || domain || ""; + const portStr = String(port); + + const matches: Record = { + "1433": "MSSQL Database Server", + "1434": "MSSQL Monitor Server", + "3306": "MySQL/MariaDB Database Server", + "5432": "PostgreSQL Database Server", + "1521": "Oracle Database Server", + "27017": "MongoDB Database Server", + "6379": "Redis Key-Value Cache", + "80": "Unencrypted Web Traffic (HTTP)", + "443": "Encrypted Web Traffic (HTTPS/TLS)", + "22": "SSH Remote Management / SFTP", + "21": "FTP File Upload/Download (Unencrypted)", + "23": "Telnet Command Shell (Insecure)", + "25": "SMTP Mail Delivery", + "587": "Secure SMTP Mail Delivery", + "110": "POP3 Mail Storage Retrieval", + "993": "Secure IMAP Mail Service", + "53": "DNS Server Query (Domain Lookup)", + "123": "NTP Network Clock Synchronizer", + "161": "SNMP Network Device Monitoring", + "3389": "RDP Remote Desktop Management", + "445": "SMB Windows File Sharing", + "137": "NetBIOS Name Service", + "138": "NetBIOS Datagram Service", + "139": "NetBIOS Session Service", + "1812": "RADIUS Authentication Server", + "1813": "RADIUS Accounting Server" + }; + + const explanation = matches[portStr] || (label.toLowerCase().includes("tls") || label.toLowerCase().includes("https") ? "Encrypted Connection (SSL/TLS)" : ""); + + return { + main: label || `Port ${port}`, + sub: explanation + }; +} + function sevStyle(sev: string | null) { switch ((sev || "").toLowerCase()) { case "critical": return "bg-red-500/15 text-red-400 ring-1 ring-red-500/30"; @@ -264,36 +306,30 @@ export function DeviceDetailModal({ ip, deviceLabel, macAddress, onClose }: Prop {activeTab === "flows" && ( data.flows.length === 0 ? : ( - {data.flows.map((f, i) => ( - - {f.dst_ip || "—"} - {f.dst_port ?? "—"} - - {f.domain ? ( - /* Domain name takes priority — shown in teal monospace */ - - {f.domain} + {data.flows.map((f, i) => { + const exp = explainAppOrPort(f.app_label, f.domain, f.dst_port || 0); + return ( + + + {f.dst_ip || "—"} + {f.dst_ip && } + + {f.dst_port ?? "—"} + + + {exp.main} - ) : f.app_label && !f.app_label.startsWith('Port ') ? ( - /* Named protocol — purple */ - {f.app_label} - ) : f.app_label ? ( - /* Port-only entry — muted */ - {f.app_label} - ) : ( - — - )} - {/* Show app_label as secondary if domain is set */} - {f.domain && f.app_label && ( - {f.app_label} - )} - - {f.protocol || "—"} - {fmtBytes(f.download)} - {fmtBytes(f.upload)} - {f.last_seen?.slice(0, 16) || "—"} - - ))} + {exp.sub && ( + {exp.sub} + )} + + {f.protocol || "—"} + {fmtBytes(f.download)} + {fmtBytes(f.upload)} + {f.last_seen?.slice(0, 16) || "—"} + + ); + })} ) )} diff --git a/src/components/ui/IpDetails.tsx b/src/components/ui/IpDetails.tsx new file mode 100644 index 0000000..6f2c31c --- /dev/null +++ b/src/components/ui/IpDetails.tsx @@ -0,0 +1,68 @@ +"use client"; + +import { useState, useEffect } from "react"; + +// Client-side cache to prevent duplicate fetches for the same IP +const geoIpCache: Record = {}; + +export function IpDetails({ ip }: { ip: string }) { + const [details, setDetails] = useState<{ isp: string; country: string; as_org: string } | null>(null); + + useEffect(() => { + if (!ip) return; + + // Check memory cache first + if (geoIpCache[ip]) { + setDetails(geoIpCache[ip]); + return; + } + + let isMounted = true; + + fetch(`/api/auth/geoip?ip=${encodeURIComponent(ip)}`) + .then((res) => res.json()) + .then((data) => { + if (!isMounted) return; + const resolved = { + isp: data.isp || "Unknown ISP", + country: data.country || "Remote", + as_org: data.as_org || "Public Network", + }; + geoIpCache[ip] = resolved; + setDetails(resolved); + }) + .catch((err) => { + console.error("GeoIP lookup client error:", err); + if (isMounted) { + setDetails({ + isp: "Public IP", + country: "Remote", + as_org: "Public Network", + }); + } + }); + + return () => { + isMounted = false; + }; + }, [ip]); + + if (!details) { + return loading...; + } + + const isLocal = details.country === "Local"; + + return ( +
+ + {details.isp} + + {!isLocal && ( + + {" • "}{details.country} + + )} +
+ ); +}