style(sidebar): match active item highlight and header buttons 100% with demoplace.my.id

This commit is contained in:
rafif committed 2026-07-21 13:52:30 +07:00
1 parent 7f133a287b
commit 960322e30a
49 files changed
+3225 -1502

No files matched your search

+2 -2
View File
@@ -1,10 +1,10 @@
FROM node:24-alpine AS base
FROM node:18-alpine AS base
# Install dependencies only when needed
FROM base AS deps
WORKDIR /app
COPY package.json package-lock.json* ./
RUN npm ci --legacy-peer-deps
RUN npm ci
# Rebuild the source code only when needed
FROM base AS builder
+2 -15
View File
@@ -41,7 +41,7 @@ Produk BackOne oleh **PT. Data Bisnis Solusi** — Dashboard monitoring jaringan
## 📡 Proxy — 2 Mode Pengambilan Data
Proxy server (port 4000) mendukung 3 mode yang dikontrol via environment variable:
Proxy server (port 4000) mendukung 2 mode yang dikontrol via environment variable:
### Mode 1: Semua Network Agent (Admin BackOne)
@@ -62,26 +62,14 @@ PROXY_AGENT_UUID=2F-TF-1D-GK # UUID Network Agent CPI Balaraja
Proxy hanya mengambil data dari **satu Network Agent spesifik** (berdasarkan UUID). Data agent lain tidak pernah masuk ke database. Cocok untuk deployment di sisi client (Pihak A, B, C) agar mereka hanya punya data milik mereka sendiri.
### Mode 3: Beberapa Agent Spesifik (Multi-Agent)
```env
# .env.local
PROXY_COLLECT_MODE=agents
PROXY_AGENT_UUIDS=UUID-AGENT-A,UUID-AGENT-B,UUID-AGENT-C # comma-separated list
PROXY_AGENT_DELAY_MS=5000 # delay antar agent (default: 5000ms)
```
Proxy mengambil data dari **beberapa Network Agent spesifik** (berdasarkan daftar UUID yang dipisahkan koma). Data agent di luar daftar tidak pernah masuk ke database. Delay antar agent dapat diatur dengan `PROXY_AGENT_DELAY_MS` untuk menghindari rate-limit.
### Contoh Multi-Tenant Deployment
| Deployment | PROXY_COLLECT_MODE | PROXY_AGENT_UUID / PROXY_AGENT_UUIDS | Data yang disimpan |
| Deployment | PROXY_COLLECT_MODE | PROXY_AGENT_UUID | Data yang disimpan |
|---|---|---|---|
| Kantor BackOne (Admin) | `all` | _(kosong)_ | Semua agent |
| Pihak A | `agent` | `UUID-AGENT-A` | Hanya data Pihak A |
| Pihak B | `agent` | `UUID-AGENT-B` | Hanya data Pihak B |
| Pihak C | `agent` | `UUID-AGENT-C` | Hanya data Pihak C |
| Multi-Client | `agents` | `UUID-A,UUID-B` | Data Pihak A dan B |
---
@@ -94,7 +82,6 @@ Proxy mengambil data dari **beberapa Network Agent spesifik** (berdasarkan dafta
| GET | `/agents` | List semua agent UUID yang ada di MongoDB |
| POST | `/collect/all` | Trigger manual — kumpulkan semua agent |
| POST | `/collect/:uuid` | Trigger manual — kumpulkan agent spesifik |
| POST | `/collect/agents` | Trigger manual — kumpulkan multiple agents (body: `{"uuids": [...], "delay_ms": 5000}`) |
---
+26 -1
View File
@@ -1,5 +1,6 @@
const jwt = require('jsonwebtoken');
const User = require('../models/User');
const Session = require('../models/Session');
const { Summary } = require('../models/Schemas');
const JWT_SECRET = process.env.JWT_SECRET || 'super-secret-backone-key';
@@ -11,6 +12,18 @@ async function requireAuth(req, res, next) {
try {
req.user = jwt.verify(token, JWT_SECRET);
// Verify session status in MongoDB
if (req.user.session_id) {
const activeSession = await Session.findById(req.user.session_id);
if (!activeSession) {
res.clearCookie('token');
return res.status(401).json({ error: 'Sesi login telah dinonaktifkan atau kedaluwarsa.' });
}
// Update last active
activeSession.last_active = new Date();
await activeSession.save();
}
// ── VIEW-AS MODE ──────────────────────────────────────────────────────────
const viewAsHeader = req.headers['x-view-as-agent'];
if (viewAsHeader && (req.user.role === 'SUPER_ADMIN' || req.user.role === 'TENANT_ADMIN')) {
@@ -50,11 +63,23 @@ async function requireAuth(req, res, next) {
}
}
function requireAdmin(req, res, next) {
async function requireAdmin(req, res, next) {
const token = req.cookies?.token;
if (!token) return res.status(401).json({ error: 'Not authenticated' });
try {
const decoded = jwt.verify(token, JWT_SECRET);
// Verify session status in MongoDB
if (decoded.session_id) {
const activeSession = await Session.findById(decoded.session_id);
if (!activeSession) {
res.clearCookie('token');
return res.status(401).json({ error: 'Sesi login telah dinonaktifkan atau kedaluwarsa.' });
}
activeSession.last_active = new Date();
await activeSession.save();
}
if (decoded.role !== 'SUPER_ADMIN' && decoded.role !== 'TENANT_ADMIN' && decoded.role !== 'SOC_ANALYST') {
return res.status(403).json({ error: 'Forbidden' });
}
+2
View File
@@ -107,6 +107,7 @@ const ThreatSchema = new mongoose.Schema({
protocol: String,
description: String,
event_at: String,
flow_id: { type: String, index: true },
}, baseOptions);
// ─── App Categories (per agent) ───────────────────────────────────────────────
@@ -133,6 +134,7 @@ const EventSchema = new mongoose.Schema({
ip_address: String,
mac_address: String,
event_at: Date,
flow_id: { type: String, index: true },
}, baseOptions);
// ─── Compound Indexes for common dashboard queries ─────────────────────────────
+22
View File
@@ -0,0 +1,22 @@
// backend/models/Session.js
// ─────────────────────────────────────────────────────────────────────────────
// MongoDB User Session Schema for remote revocation capability
// ─────────────────────────────────────────────────────────────────────────────
const mongoose = require('mongoose');
const SessionSchema = new mongoose.Schema({
user_id: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true, index: true },
ip_address: { type: String, default: 'Unknown' },
user_agent: { type: String, default: 'Unknown' },
session_token: { type: String, required: true, unique: true }, // JWT JTI or unique token hash
last_active: { type: Date, default: Date.now },
expires_at: { type: Date, required: true, index: true }, // MongoDB TTL Index will auto-expire sessions
}, {
timestamps: { createdAt: 'created_at', updatedAt: 'updated_at' }
});
// TTL index to automatically remove expired sessions from MongoDB
SessionSchema.index({ expires_at: 1 }, { expireAfterSeconds: 0 });
module.exports = mongoose.model('Session', SessionSchema);
+2
View File
@@ -19,6 +19,8 @@ const UserSchema = new mongoose.Schema({
agent_uuid: { type: String, default: null },
created_by: { type: String, default: null, index: true },
is_active: { type: Boolean, default: true },
login_attempts: { type: Number, default: 0 },
lockout_until: { type: Date, default: null },
}, {
timestamps: { createdAt: 'created_at', updatedAt: 'updated_at' }
});
+6 -10
View File
@@ -44,25 +44,21 @@ async function populateAppCache(BASE_URL, token, siteUuid) {
const headers = { 'x-api-key': token, 'Accept': 'application/json' };
if (siteUuid) headers['x-net-site'] = siteUuid;
const res = await axios.get(`${BASE_URL}/lookup/applications`, {
headers, params: { settings_limit: 10000 }, timeout: 20000
headers, params: { settings_limit: 2000 }, timeout: 8000
});
appLookupCache = {};
if (res.data && Array.isArray(res.data.data)) {
res.data.data.forEach(a => {
if (!a.id) return;
const label = (a.label || '').trim();
if (!label) return;
if (!a.label || !a.id) return;
let domain = null;
if (a.home_page?.url) {
domain = a.home_page.url.replace(/^https?:\/\/(www\.)?/, '').split('/')[0];
} else if (a.domain_list?.length > 0) {
domain = a.domain_list[0].label;
} else {
domain = label.toLowerCase();
domain = a.label.toLowerCase();
}
const entry = { id: a.id, label, domain };
appLookupCache[label.toLowerCase()] = entry;
if (a.tag) appLookupCache[a.tag.toLowerCase()] = entry;
appLookupCache[a.label.toLowerCase()] = { id: a.id, label: a.label, domain };
});
}
console.log(`[AppDetailsDpiHelper] App cache: ${Object.keys(appLookupCache).length} apps`);
@@ -96,12 +92,12 @@ async function fetchFromDpiApi(label, agentUuid, timeRange, token, siteUuid) {
const params = {
filter_interval: timeRangeToMinutes(timeRange),
filter_applications: `[${appInfo.id}]`,
filter_applications: `["${appInfo.id}"]`,
settings_limit: 10000
};
if (agentUuid && agentMapCache?.[agentUuid]) {
params.filter_agents = `["${agentMapCache[agentUuid]}"]`;
params.filter_agents = `[${agentMapCache[agentUuid]}]`;
}
const [dlRes, ulRes] = await Promise.all([
+2 -2
View File
@@ -40,7 +40,7 @@ module.exports = async function appDetailsHandler(req, res, helpers) {
if (deviceApps.length > 0) {
// Pre-load application lookup to resolve default domains
const BASE_URL = process.env.NETIFY_INFORMATICS_BASE_URL || 'https://informatics.netify.ai/api/v1';
if (token) {
if (token && SITE_UUID) {
await populateAppCache(BASE_URL, token, SITE_UUID).catch(e => console.warn('[AppDetails] Cache error:', e.message));
}
const appMeta = getAppLookupCache()?.[label.toLowerCase()];
@@ -96,7 +96,7 @@ module.exports = async function appDetailsHandler(req, res, helpers) {
}
// ── Step 2: Fall back to DPI API only if MongoDB has ZERO data ────────────
if (token) {
if (token && SITE_UUID) {
const dpiResult = await fetchFromDpiApi(label, agentUuid, req.query.timeRange, token, SITE_UUID);
if (dpiResult) {
console.log(`[AppDetails] DPI fallback: label=${label} time=${Date.now()-t0}ms`);
+2
View File
@@ -11,10 +11,12 @@ const coreRoutes = require('./auth/core');
const settingsRoutes = require('./auth/settings');
const usersRoutes = require('./auth/users');
const viewAsRoutes = require('./auth/viewAs');
const sessionsRoutes = require('./auth/sessions');
router.use('/', coreRoutes);
router.use('/', settingsRoutes);
router.use('/', usersRoutes);
router.use('/', viewAsRoutes);
router.use('/', sessionsRoutes);
module.exports = router;
+66 -143
View File
@@ -9,144 +9,9 @@ const { TenantConfig, CustomAgentLocation } = require('../../models/Schemas');
const router = express.Router();
// ─── Auto-seed SUPER_ADMIN, SOC_ANALYST, dan TENANT_ADMIN jika belum ada ─────────
(async () => {
try {
const count = await User.countDocuments({ role: 'SUPER_ADMIN' });
if (count === 0) {
const hash = bcrypt.hashSync('admin', 10);
await User.create({
username: 'admin',
password_hash: hash,
account_name: 'BackOne Administrator',
role: 'SUPER_ADMIN',
site_uuid: process.env.NETIFY_SITE_UUID || null,
agent_uuid: null,
});
console.log('[Auth] ✓ Default SUPER_ADMIN created: admin / admin');
console.log('[Auth] ⚠ GANTI PASSWORD INI SEGERA DI PRODUCTION!');
}
const siabCount = await User.countDocuments({ username: 'siab' });
if (siabCount === 0) {
const hash = bcrypt.hashSync('siab', 10);
await User.create({
username: 'siab',
password_hash: hash,
account_name: 'SIAB Administrator',
role: 'TENANT_ADMIN',
site_uuid: '6681452d_9cae_4ff4_8ae8_0d504774265e',
agent_uuid: null,
});
console.log('[Auth] ✓ Default SIAB Tenant created: siab / siab');
}
const nexusCount = await User.countDocuments({ username: 'nexus' });
if (nexusCount === 0) {
const hash = bcrypt.hashSync('nexus', 10);
await User.create({
username: 'nexus',
password_hash: hash,
account_name: 'Nexus Administrator',
role: 'TENANT_ADMIN',
site_uuid: 'd7902405_0dc2_458b_8584_ed4d24b64f24',
agent_uuid: null,
});
console.log('[Auth] ✓ Default Nexus Tenant created: nexus / nexus');
}
// Repair/Migration: Ensure legacy users have appropriate created_by values
try {
const missingCreatedBy = await User.find({ $or: [{ created_by: { $exists: false } }, { created_by: null }] });
if (missingCreatedBy.length > 0) {
console.log(`[Auth] Migrating ${missingCreatedBy.length} legacy users to set created_by...`);
for (const u of missingCreatedBy) {
if (u.username === 'admin') {
u.created_by = 'admin';
} else if (u.site_uuid === '6681452d_9cae_4ff4_8ae8_0d504774265e') {
u.created_by = 'siab';
} else if (u.site_uuid === 'd7902405_0dc2_458b_8584_ed4d24b64f24') {
u.created_by = 'nexus';
} else {
u.created_by = 'admin';
}
await u.save();
}
console.log(`[Auth] Migration complete.`);
}
} catch (migrateErr) {
console.error('[Auth] Migration failed:', migrateErr.message);
}
const defaultConfigs = [
{
site_uuid: 'default',
brand_name: 'BackOne',
brand_logo: '/backone-logo.png',
footer_copyright: 'PT. Data Bisnis Solusi',
primary_color: '#E11D48',
},
{
site_uuid: '6681452d_9cae_4ff4_8ae8_0d504774265e',
brand_name: 'SIAB',
brand_logo: '/siab-logo.png',
footer_copyright: 'PT. SIAB Indonesia',
primary_color: '#3B82F6',
},
{
site_uuid: 'd7902405_0dc2_458b_8584_ed4d24b64f24',
brand_name: 'Nexus',
brand_logo: '/nexus-logo.png',
footer_copyright: 'PT. Nexus Solusi',
primary_color: '#8B5CF6',
}
];
for (const config of defaultConfigs) {
const existing = await TenantConfig.findOne({ site_uuid: config.site_uuid });
if (!existing) {
await TenantConfig.create(config);
console.log(`[Auth] ✓ Seeded TenantConfig for: ${config.brand_name}`);
}
}
// Seed default agent locations
const defaultLocations = [
{
agent_uuid: 'F6-2V-DT-8A',
site_uuid: '6681452d_9cae_4ff4_8ae8_0d504774265e',
latitude: -6.2263304,
longitude: 106.4247322,
label: 'CPI Balaraja Agent Office'
},
{
agent_uuid: '2F-TF-1D-GK',
site_uuid: '6681452d_9cae_4ff4_8ae8_0d504774265e',
latitude: -6.3763318,
longitude: 106.8983017,
label: 'JRP Cibubur Agent Office'
},
{
agent_uuid: '8A-V3-PB-85',
site_uuid: '6681452d_9cae_4ff4_8ae8_0d504774265e',
latitude: -6.2253265,
longitude: 106.8061484,
label: 'IFG LT.18 Agent HQ'
}
];
for (const loc of defaultLocations) {
const existing = await CustomAgentLocation.findOne({ agent_uuid: loc.agent_uuid });
if (!existing) {
await CustomAgentLocation.create(loc);
console.log(`[Auth] ✓ Seeded CustomAgentLocation for: ${loc.agent_uuid}`);
}
}
} catch (err) {
console.warn('[Auth] Seed skipped (MongoDB not ready yet):', err.message);
}
})();
// ─── Auto-seed database records if empty ──────────────────────────────────────
const seedAuth = require('./seed');
seedAuth();
// ─── POST /api/auth/login ─────────────────────────────────────────────────────
router.post('/login', async (req, res) => {
@@ -157,12 +22,50 @@ router.post('/login', async (req, res) => {
}
const user = await User.findOne({ username, is_active: true }).select('+password_hash');
if (!user) return res.status(401).json({ error: 'Invalid credentials' });
if (!user) {
return res.status(401).json({ error: 'Username not found' });
}
// Check if account is currently locked out
if (user.lockout_until && user.lockout_until > new Date()) {
const remainingTime = Math.ceil((user.lockout_until - new Date()) / 60000);
return res.status(403).json({ error: `Account is temporarily locked. Please try again in ${remainingTime} minute(s).` });
}
const isValid = bcrypt.compareSync(password, user.password_hash);
if (!isValid) return res.status(401).json({ error: 'Invalid credentials' });
if (!isValid) {
user.login_attempts = (user.login_attempts || 0) + 1;
if (user.login_attempts >= 3) {
user.lockout_until = new Date(Date.now() + 15 * 60 * 1000); // 15 mins lockout
await user.save();
return res.status(403).json({ error: 'Account is temporarily locked. Please try again in 15 minute(s).' });
} else {
await user.save();
return res.status(401).json({ error: 'Invalid Password' });
}
}
const token = makeToken(user);
// Reset login attempts on successful login
user.login_attempts = 0;
user.lockout_until = null;
await user.save();
// Create session in MongoDB
const Session = require('../../models/Session');
const crypto = require('crypto');
const sessionToken = crypto.randomBytes(32).toString('hex');
const expiresAt = new Date();
expiresAt.setDate(expiresAt.getDate() + 1); // 1 day session lifetime
const newSession = await Session.create({
user_id: user._id,
ip_address: req.headers['x-forwarded-for'] || req.socket.remoteAddress || 'Unknown',
user_agent: req.headers['user-agent'] || 'Unknown',
session_token: sessionToken,
expires_at: expiresAt,
});
const token = makeToken(user, newSession._id);
setCookieToken(res, token);
res.json({
@@ -188,7 +91,18 @@ router.post('/renew', requireAuth, async (req, res) => {
const user = await User.findById(req.user.id);
if (!user) return res.status(404).json({ error: 'User tidak ditemukan' });
const token = makeToken(user);
const sessionId = req.user.session_id;
if (sessionId) {
const Session = require('../../models/Session');
const session = await Session.findById(sessionId);
if (session) {
// Extend session expires_at in MongoDB by another 24h
session.expires_at = new Date(Date.now() + 24 * 60 * 60 * 1000);
await session.save();
}
}
const token = makeToken(user, sessionId);
setCookieToken(res, token);
const decoded = jwt.verify(token, JWT_SECRET);
@@ -239,7 +153,16 @@ router.get('/me', requireAuth, async (req, res) => {
});
// ─── POST /api/auth/logout ────────────────────────────────────────────────────
router.post('/logout', (req, res) => {
router.post('/logout', requireAuth, async (req, res) => {
try {
const sessionId = req.user?.session_id;
if (sessionId) {
const Session = require('../../models/Session');
await Session.findByIdAndDelete(sessionId);
}
} catch (err) {
console.error('[Logout] Session deletion failed:', err.message);
}
res.clearCookie('token');
res.json({ message: 'Logged out successfully' });
});
+2 -1
View File
@@ -6,7 +6,7 @@ const fs = require('fs');
const { requireAuth, requireAdmin, JWT_SECRET } = require('../../middleware/auth');
function makeToken(user) {
function makeToken(user, sessionId) {
return jwt.sign(
{
id: user._id.toString(),
@@ -16,6 +16,7 @@ function makeToken(user) {
role: user.role,
site_uuid: user.site_uuid,
agent_uuid: user.agent_uuid,
session_id: sessionId ? sessionId.toString() : undefined,
},
JWT_SECRET,
{ expiresIn: '1d' }
+148
View File
@@ -0,0 +1,148 @@
// backend/routes/auth/seed.js
// ─────────────────────────────────────────────────────────────────────────────
// Seeding logic for default roles, site configs, and agent locations
// ─────────────────────────────────────────────────────────────────────────────
const bcrypt = require('bcryptjs');
const User = require('../../models/User');
const { TenantConfig, CustomAgentLocation } = require('../../models/Schemas');
async function seedAuth() {
try {
const count = await User.countDocuments({ role: 'SUPER_ADMIN' });
if (count === 0) {
const hash = bcrypt.hashSync('admin', 10);
await User.create({
username: 'admin',
password_hash: hash,
account_name: 'BackOne Administrator',
role: 'SUPER_ADMIN',
site_uuid: process.env.NETIFY_SITE_UUID || null,
agent_uuid: null,
});
console.log('[Auth] ✓ Default SUPER_ADMIN created: admin / admin');
console.log('[Auth] ⚠ GANTI PASSWORD INI SEGERA DI PRODUCTION!');
}
const siabCount = await User.countDocuments({ username: 'siab' });
if (siabCount === 0) {
const hash = bcrypt.hashSync('siab', 10);
await User.create({
username: 'siab',
password_hash: hash,
account_name: 'SIAB Administrator',
role: 'TENANT_ADMIN',
site_uuid: '6681452d_9cae_4ff4_8ae8_0d504774265e',
agent_uuid: null,
});
console.log('[Auth] ✓ Default SIAB Tenant created: siab / siab');
}
const nexusCount = await User.countDocuments({ username: 'nexus' });
if (nexusCount === 0) {
const hash = bcrypt.hashSync('nexus', 10);
await User.create({
username: 'nexus',
password_hash: hash,
account_name: 'Nexus Administrator',
role: 'TENANT_ADMIN',
site_uuid: 'd7902405_0dc2_458b_8584_ed4d24b64f24',
agent_uuid: null,
});
console.log('[Auth] ✓ Default Nexus Tenant created: nexus / nexus');
}
// Repair/Migration: Ensure legacy users have appropriate created_by values
try {
const missingCreatedBy = await User.find({ $or: [{ created_by: { $exists: false } }, { created_by: null }] });
if (missingCreatedBy.length > 0) {
console.log(`[Auth] Migrating ${missingCreatedBy.length} legacy users to set created_by...`);
for (const u of missingCreatedBy) {
if (u.username === 'admin') {
u.created_by = 'admin';
} else if (u.site_uuid === '6681452d_9cae_4ff4_8ae8_0d504774265e') {
u.created_by = 'siab';
} else if (u.site_uuid === 'd7902405_0dc2_458b_8584_ed4d24b64f24') {
u.created_by = 'nexus';
} else {
u.created_by = 'admin';
}
await u.save();
}
console.log(`[Auth] Migration complete.`);
}
} catch (migrateErr) {
console.error('[Auth] Migration failed:', migrateErr.message);
}
const defaultConfigs = [
{
site_uuid: 'default',
brand_name: 'BackOne',
brand_logo: '/backone-logo.png',
footer_copyright: 'PT. Data Bisnis Solusi',
primary_color: '#E11D48',
},
{
site_uuid: '6681452d_9cae_4ff4_8ae8_0d504774265e',
brand_name: 'SIAB',
brand_logo: '/siab-logo.png',
footer_copyright: 'PT. SIAB Indonesia',
primary_color: '#3B82F6',
},
{
site_uuid: 'd7902405_0dc2_458b_8584_ed4d24b64f24',
brand_name: 'Nexus',
brand_logo: '/nexus-logo.png',
footer_copyright: 'PT. Nexus Solusi',
primary_color: '#8B5CF6',
}
];
for (const config of defaultConfigs) {
const existing = await TenantConfig.findOne({ site_uuid: config.site_uuid });
if (!existing) {
await TenantConfig.create(config);
console.log(`[Auth] ✓ Seeded TenantConfig for: ${config.brand_name}`);
}
}
// Seed default agent locations
const defaultLocations = [
{
agent_uuid: 'F6-2V-DT-8A',
site_uuid: '6681452d_9cae_4ff4_8ae8_0d504774265e',
latitude: -6.2263304,
longitude: 106.4247322,
label: 'CPI Balaraja Agent Office'
},
{
agent_uuid: '2F-TF-1D-GK',
site_uuid: '6681452d_9cae_4ff4_8ae8_0d504774265e',
latitude: -6.3763318,
longitude: 106.8983017,
label: 'JRP Cibubur Agent Office'
},
{
agent_uuid: '8A-V3-PB-85',
site_uuid: '6681452d_9cae_4ff4_8ae8_0d504774265e',
latitude: -6.2253265,
longitude: 106.8061484,
label: 'IFG LT.18 Agent HQ'
}
];
for (const loc of defaultLocations) {
const existing = await CustomAgentLocation.findOne({ agent_uuid: loc.agent_uuid });
if (!existing) {
await CustomAgentLocation.create(loc);
console.log(`[Auth] ✓ Seeded CustomAgentLocation for: ${loc.agent_uuid}`);
}
}
} catch (err) {
console.warn('[Auth] Seed skipped (MongoDB not ready yet):', err.message);
}
}
module.exports = seedAuth;
+126
View File
@@ -0,0 +1,126 @@
// backend/routes/auth/sessions.js
// ─────────────────────────────────────────────────────────────────────────────
// User Session Management Routes (Active Sessions & Remote Revocation)
// ─────────────────────────────────────────────────────────────────────────────
const express = require('express');
const router = express.Router();
const User = require('../../models/User');
const Session = require('../../models/Session');
const { requireAuth, requireAdmin } = require('./helpers');
// Helper to block SOC_ANALYST from write actions
function blockAnalyst(req, res, next) {
if (req.adminUser.role === 'SOC_ANALYST') {
return res.status(403).json({ ok: false, error: 'Aksi ini tidak diizinkan untuk peran SOC Analyst' });
}
next();
}
// ─── GET /api/auth/sessions (Current User Sessions) ──────────────────────────
router.get('/sessions', requireAuth, async (req, res) => {
try {
const sessions = await Session.find({ user_id: req.user.id }).sort({ last_active: -1 });
const data = sessions.map(s => ({
id: s._id.toString(),
ip_address: s.ip_address,
user_agent: s.user_agent,
last_active: s.last_active,
created_at: s.created_at,
is_current: req.user.session_id === s._id.toString(),
}));
res.json({ ok: true, data });
} catch (err) {
res.status(500).json({ ok: false, error: err.message });
}
});
// ─── DELETE /api/auth/sessions/:id (Revoke Current User Session) ──────────────
router.delete('/sessions/:id', requireAuth, async (req, res) => {
try {
const session = await Session.findById(req.params.id);
if (!session) {
return res.status(404).json({ ok: false, error: 'Sesi tidak ditemukan' });
}
// Users can only revoke their own sessions
if (session.user_id.toString() !== req.user.id) {
return res.status(403).json({ ok: false, error: 'Aksi dilarang' });
}
await Session.findByIdAndDelete(req.params.id);
// Clear cookies if the user revokes their own current session
if (req.user.session_id === req.params.id) {
res.clearCookie('token');
}
res.json({ ok: true, message: 'Sesi berhasil diakhiri' });
} catch (err) {
res.status(500).json({ ok: false, error: err.message });
}
});
// ─── GET /api/auth/admin/sessions (Tenant/All Sessions) ──────────────────────
router.get('/admin/sessions', requireAdmin, async (req, res) => {
try {
let userQuery = {};
if (req.adminUser.role === 'TENANT_ADMIN') {
userQuery = { site_uuid: req.adminUser.site_uuid };
}
const users = await User.find(userQuery, 'username role account_name site_uuid');
const userIds = users.map(u => u._id);
const sessions = await Session.find({ user_id: { $in: userIds } })
.populate('user_id', 'username role account_name site_uuid')
.sort({ last_active: -1 });
const data = sessions.map(s => {
const u = s.user_id || {};
return {
id: s._id.toString(),
username: u.username || 'Unknown',
role: u.role || 'Unknown',
account_name: u.account_name || 'Unknown',
site_uuid: u.site_uuid || null,
ip_address: s.ip_address,
user_agent: s.user_agent,
last_active: s.last_active,
created_at: s.created_at,
is_current: req.adminUser.session_id === s._id.toString(),
};
});
res.json({ ok: true, data });
} catch (err) {
res.status(500).json({ ok: false, error: err.message });
}
});
// ─── DELETE /api/auth/admin/sessions/:id (Revoke Any Session by Admin) ───────
router.delete('/admin/sessions/:id', requireAdmin, blockAnalyst, async (req, res) => {
try {
const session = await Session.findById(req.params.id).populate('user_id');
if (!session) {
return res.status(404).json({ ok: false, error: 'Sesi tidak ditemukan' });
}
// Tenant Admin can only revoke sessions within their own site
if (req.adminUser.role !== 'SUPER_ADMIN') {
const sessionUser = session.user_id || {};
if (sessionUser.site_uuid !== req.adminUser.site_uuid) {
return res.status(403).json({ ok: false, error: 'Unauthorized: Sesi berada di luar site Anda' });
}
}
await Session.findByIdAndDelete(req.params.id);
res.json({ ok: true, message: 'Sesi berhasil diakhiri oleh administrator' });
} catch (err) {
res.status(500).json({ ok: false, error: err.message });
}
});
module.exports = router;
+29
View File
@@ -37,6 +37,8 @@ router.get('/admin/users', requireAdmin, async (req, res) => {
site_uuid: u.site_uuid,
agent_uuid: u.agent_uuid,
is_active: u.is_active,
login_attempts: u.login_attempts || 0,
lockout_until: u.lockout_until || null,
}));
res.json({ ok: true, data });
} catch (err) {
@@ -44,6 +46,33 @@ router.get('/admin/users', requireAdmin, async (req, res) => {
}
});
// POST /api/auth/admin/unlock-user — unlock akun yang terkunci
router.post('/admin/unlock-user', requireAdmin, blockAnalyst, async (req, res) => {
try {
const { user_id } = req.body;
if (!user_id) {
return res.status(400).json({ ok: false, error: 'user_id wajib diisi' });
}
const target = await User.findById(user_id);
if (!target) {
return res.status(404).json({ ok: false, error: 'User tidak ditemukan' });
}
if (req.adminUser.role !== 'SUPER_ADMIN' && target.site_uuid !== req.adminUser.site_uuid) {
return res.status(403).json({ ok: false, error: 'Unauthorized: Account does not belong to your tenant.' });
}
target.login_attempts = 0;
target.lockout_until = null;
await target.save();
res.json({ ok: true, message: 'Akun berhasil di-unlock' });
} catch (err) {
res.status(500).json({ ok: false, error: err.message });
}
});
// POST /api/auth/admin/create-agent-user — buat akun Network Agent baru
router.post('/admin/create-agent-user', requireAdmin, blockAnalyst, async (req, res) => {
try {
+6 -7
View File
@@ -1,6 +1,6 @@
const express = require('express');
const router = express.Router();
const { AppStat, ProtocolStat, AppCategoryStat, LookupApp, Flow } = require('../../models/Schemas');
const { AppStat, ProtocolStat, AppCategoryStat, LookupApp } = require('../../models/Schemas');
const { getTimeFilter, getBaseFilter } = require('./helpers');
// GET /api/dashboard/apps
@@ -44,18 +44,17 @@ router.get('/protocols', async (req, res) => {
const base = getBaseFilter(req, timeFilter);
const pipeline = [
{ $match: { ...base, protocol: { $ne: null, $ne: '' } } },
{ $match: base },
{ $group: {
_id: '$protocol',
_id: '$protocol_label',
download: { $sum: '$download' },
upload: { $sum: '$upload' },
flows: { $sum: 1 },
flows: { $sum: '$flows' },
}},
{ $sort: { download: -1 } },
{ $limit: 20 }
{ $sort: { download: -1 } }
];
const result = await Flow.aggregate(pipeline);
const result = await ProtocolStat.aggregate(pipeline);
const formatted = result.map(r => ({
protocol_label: r._id,
download: r.download || 0,
+1 -1
View File
@@ -40,7 +40,7 @@ function getBaseFilter(req, timeFilter = null) {
if (isGlobalUser && requestedSiteUuid) {
filter.site_uuid = requestedSiteUuid;
} else if (!isGlobalUser && req.user?.site_uuid) {
} else if (req.user?.site_uuid) {
filter.site_uuid = req.user.site_uuid;
}
+135
View File
@@ -0,0 +1,135 @@
const cron = require('node-cron');
const netify = require('../netify');
const { Summary, AppStat, ProtocolStat, DeviceStat, Flow, Threat } = require('../models/Schemas');
const SITE_UUID = process.env.NETIFY_SITE_UUID || process.env.BACKONE_SITE_UUID;
let isRunning = false;
async function runPoll() {
if (isRunning) return;
isRunning = true;
const timestamp = new Date();
console.log(`[Mongo-Ingestion] Started polling at ${timestamp.toISOString()}`);
try {
const agents = await netify.fetchAgents();
const agentList = agents && agents.length > 0 ? agents.map(a => a.uuid) : [null]; // null for global
for (const agentUuid of agentList) {
console.log(`[Mongo-Ingestion] Fetching data for Agent: ${agentUuid || 'Global'}`);
// 1. Summary
const summary = await netify.fetchBandwidthSummary(1440, agentUuid);
if (summary) {
await new Summary({
timestamp,
agent_uuid: agentUuid,
site_uuid: SITE_UUID,
...summary
}).save();
}
// 2. Apps
const apps = await netify.fetchTopApps(1440, 200, agentUuid); // high limit for data lake
if (apps && apps.length > 0) {
const appDocs = apps.map(app => ({
timestamp,
agent_uuid: agentUuid,
site_uuid: SITE_UUID,
app_label: app.application?.label || 'Unknown',
download: app.download || 0,
upload: app.upload || 0,
flows: app.flows || 0
}));
await AppStat.insertMany(appDocs);
}
const devices = await netify.fetchDiscoveredDevices(1440, 500, agentUuid);
if (devices && devices.length > 0) {
const devDocs = devices.map(d => ({
timestamp,
agent_uuid: agentUuid,
site_uuid: SITE_UUID,
ip_address: d.ip_address,
mac_address: d.mac_address,
device_label: d.device_label,
device_type: d.device_type,
os_label: d.os_label,
manufacturer: d.manufacturer,
download: d.download || 0,
upload: d.upload || 0,
flows: d.flows || 0,
last_seen: d.last_seen
})).filter(d => d.ip_address); // Ensure ip_address exists to avoid validation error
if (devDocs.length > 0) {
await DeviceStat.insertMany(devDocs);
}
}
// 4. Flows
const flows = await netify.fetchFlows(500, agentUuid);
if (flows && flows.length > 0) {
const flowDocs = flows.map(f => ({
timestamp,
agent_uuid: agentUuid,
site_uuid: SITE_UUID,
flow_id: f.flow_id,
src_ip: f.src_ip,
src_mac: f.src_mac,
dst_ip: f.dst_ip,
dst_port: f.dst_port,
protocol: f.protocol,
app_label: f.app_label,
domain: f.domain,
download: f.download || 0,
upload: f.upload || 0,
first_seen: f.first_seen,
last_seen: f.last_seen
})).filter(f => f.src_ip);
if (flowDocs.length > 0) {
await Flow.insertMany(flowDocs);
}
}
// 5. Threats
const threats = await netify.fetchCyberThreats(agentUuid);
if (threats && threats.length > 0) {
const threatDocs = threats.map(t => ({
timestamp,
agent_uuid: agentUuid,
site_uuid: SITE_UUID,
threat_type: t.threat_type || 'Unknown Threat',
severity: t.severity || 'Medium',
src_ip: t.src_ip,
dst_ip: t.dst_ip,
dst_port: t.dst_port,
protocol: t.protocol,
description: t.description,
event_at: t.event_at || new Date().toISOString()
}));
if (threatDocs.length > 0) {
await Threat.insertMany(threatDocs);
}
}
}
} catch (error) {
console.error('[Mongo-Ingestion] Error during polling:', error);
} finally {
isRunning = false;
}
}
function startScheduler() {
// Run every 5 minutes
cron.schedule('*/5 * * * *', () => {
runPoll();
});
console.log('[Mongo-Ingestion] Scheduler started (every 5 minutes)');
// Initial run
runPoll();
}
module.exports = { startScheduler };
Binary file not shown.

After

Width:  |  Height:  |  Size: 429 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 429 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 429 KiB

+1 -4
View File
@@ -53,12 +53,9 @@ services:
- MONGODB_URI=mongodb://mongodb:27017/backone_dpi
- PROXY_PORT=4000
# Mode 1: kumpulkan SEMUA agent (default)
# Ubah ke PROXY_COLLECT_MODE=agent dan isi PROXY_AGENT_UUID untuk mode spesifik (1 agent)
# Ubah ke PROXY_COLLECT_MODE=agents dan isi PROXY_AGENT_UUIDS untuk mode multi-agent
# Ubah ke PROXY_COLLECT_MODE=agent dan isi PROXY_AGENT_UUID untuk mode spesifik
- PROXY_COLLECT_MODE=${PROXY_COLLECT_MODE:-all}
- PROXY_AGENT_UUID=${PROXY_AGENT_UUID:-}
- PROXY_AGENT_UUIDS=${PROXY_AGENT_UUIDS:-}
- PROXY_AGENT_DELAY_MS=${PROXY_AGENT_DELAY_MS:-5000}
- PROXY_CRON_SCHEDULE=${PROXY_CRON_SCHEDULE:-*/5 * * * *}
networks:
- backone-infra
+3 -3
View File
@@ -12,8 +12,8 @@ module.exports = {
instances: 1,
exec_mode: 'fork',
watch: false,
node_args: '--max-old-space-size=256',
max_memory_restart: '300M',
node_args: '--max-old-space-size=1024',
max_memory_restart: '1200M',
restart_delay: 5000,
max_restarts: 10,
env_file: '.env.production',
@@ -51,7 +51,7 @@ module.exports = {
// ─── 3. Next.js Frontend (Standalone) ──────────────────────────────────
{
name: 'backone-frontend',
script: './.next/standalone/server.js',
script: 'start-with-env.js',
cwd: '/home/adminbackend/web/demoplace.my.id/public_html',
instances: 1,
exec_mode: 'fork',
+7 -40
View File
@@ -1,52 +1,19 @@
if (typeof globalThis.crypto === 'undefined') {
globalThis.crypto = require('crypto');
}
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
output: "standalone",
serverExternalPackages: ["mongoose"],
experimental: {
serverActions: {
allowedOrigins: [
"demoplace.my.id",
"www.demoplace.my.id",
"localhost:3000",
"127.0.0.1:3000",
],
allowedOrigins: ["demoplace.my.id", "www.demoplace.my.id"],
},
},
// Enable Turbopack explicitly (Next.js 16 default) — empty config silences the webpack warning
turbopack: {},
images: {
remotePatterns: [
{
protocol: "https",
hostname: "demoplace.my.id",
pathname: "/api/uploads/**",
},
{
protocol: "http",
hostname: "demoplace.my.id",
pathname: "/api/uploads/**",
},
{
protocol: "http",
hostname: "127.0.0.1",
port: "3001",
pathname: "/api/uploads/**",
},
{
protocol: "http",
hostname: "localhost",
port: "3001",
pathname: "/api/uploads/**",
},
],
},
async rewrites() {
return [];
return [
{
source: '/api/:path*',
destination: `${process.env.NEXT_PUBLIC_API_URL || 'http://127.0.0.1:3001'}/api/:path*`,
},
];
},
};
+1861 -1187
View File
File diff suppressed because it is too large. Load diff
+2 -2
View File
@@ -6,8 +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:mongo": "node scripts/start-mongo.js",
"dev": "concurrently --names \"NEXT,BACKEND,PROXY\" --prefix-colors \"cyan,green,yellow\" \"next dev\" \"node backend/server.js\" \"node proxy/index.js\"",
"dev:next": "next dev",
"dev:backend": "node backend/server.js",
"dev:proxy": "node proxy/index.js",
@@ -28,6 +27,7 @@
"@types/leaflet": "^1.9.21",
"axios": "^1.18.1",
"bcryptjs": "^3.0.3",
"better-sqlite3": "^12.11.1",
"clsx": "^2.1.1",
"concurrently": "^10.0.3",
"cookie": "^2.0.1",
+7 -2
View File
@@ -2,13 +2,18 @@ FROM oven/bun:1-alpine
WORKDIR /app
COPY proxy/package*.json ./
# Install dependencies first (layer caching)
# bun install is compatible with npm package.json / package-lock.json
COPY package*.json ./
RUN bun install --production
COPY proxy/ .
# Copy application source
COPY . .
# Expose proxy REST API port
EXPOSE 4000
# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=15s --retries=3 \
CMD bun -e "require('http').get('http://localhost:4000/health', r => r.statusCode === 200 ? process.exit(0) : process.exit(1)).on('error', () => process.exit(1))"
+34
View File
@@ -0,0 +1,34 @@
// check_device.js - Detailed check of 10.6.10.44 records
const path = require('path');
require('dotenv').config({ path: path.join(__dirname, '..', '.env.local') });
const mongoose = require('mongoose');
async function run() {
await mongoose.connect(process.env.MONGODB_URI || 'mongodb://localhost:27017/backone');
const db = mongoose.connection.db;
// Get all records for 10.6.10.44
const docs = await db.collection('devicestats')
.find({ ip_address: '10.6.10.44' })
.sort({ timestamp: 1 })
.toArray();
console.log(`Total docs for 10.6.10.44: ${docs.length}`);
docs.forEach((d, i) => {
console.log(`\n--- Doc ${i + 1} ---`);
console.log(' _id: ', d._id);
console.log(' agent_uuid: ', d.agent_uuid);
console.log(' timestamp: ', d.timestamp);
console.log(' created_at: ', d.created_at);
console.log(' updated_at: ', d.updated_at);
console.log(' download: ', d.download);
console.log(' device_label:', d.device_label);
});
// Check if there are different agent_uuids
const agents = [...new Set(docs.map(d => d.agent_uuid))];
console.log('\nDistinct agent_uuids for this IP:', agents);
await mongoose.disconnect();
}
run().catch(err => { console.error(err.message); process.exit(1); });
+73
View File
@@ -0,0 +1,73 @@
// proxy/clean_devicestat_duplicates.js
// ─────────────────────────────────────────────────────────────────────────────
// One-time cleanup script to deduplicate historical DeviceStat records.
// Keeps only the LATEST document per (agent_uuid, ip_address) pair,
// removing all older duplicates accumulated before the upsert fix.
//
// Usage: node proxy/clean_devicestat_duplicates.js
// ─────────────────────────────────────────────────────────────────────────────
const path = require('path');
require('dotenv').config({ path: path.join(__dirname, '..', '.env.local') });
const mongoose = require('mongoose');
const MONGODB_URI = process.env.MONGODB_URI || 'mongodb://localhost:27017/backone';
const DeviceStatSchema = new mongoose.Schema({
timestamp: { type: Date },
agent_uuid: { type: String },
site_uuid: { type: String },
ip_address: { type: String },
mac_address: { type: String },
device_label: String,
device_type: String,
os_label: String,
manufacturer: String,
download: Number,
upload: Number,
flows: Number,
last_seen: String,
}, { timestamps: { createdAt: 'created_at', updatedAt: 'updated_at' } });
const DeviceStat = mongoose.model('DeviceStat', DeviceStatSchema);
async function run() {
console.log('[Cleanup] Connecting to MongoDB...');
await mongoose.connect(MONGODB_URI);
console.log('[Cleanup] Connected.');
// Find all unique (agent_uuid, ip_address) combinations
const groups = await DeviceStat.aggregate([
{ $group: {
_id: { agent_uuid: '$agent_uuid', ip_address: '$ip_address' },
ids: { $push: '$_id' },
timestamps: { $push: '$timestamp' },
count: { $sum: 1 },
}},
{ $match: { count: { $gt: 1 } } },
]);
console.log(`[Cleanup] Found ${groups.length} (agent_uuid, ip_address) pairs with duplicates.`);
let totalDeleted = 0;
for (const group of groups) {
// Sort the ids by matching timestamps - keep the latest
const paired = group.ids.map((id, i) => ({ id, ts: group.timestamps[i] }));
paired.sort((a, b) => new Date(b.ts) - new Date(a.ts));
// Keep the first (newest), delete the rest
const toDelete = paired.slice(1).map(p => p.id);
const result = await DeviceStat.deleteMany({ _id: { $in: toDelete } });
totalDeleted += result.deletedCount;
}
const remaining = await DeviceStat.countDocuments();
console.log(`[Cleanup] Done. Deleted ${totalDeleted} duplicate DeviceStat records.`);
console.log(`[Cleanup] Remaining DeviceStat documents: ${remaining}`);
await mongoose.disconnect();
}
run().catch(err => {
console.error('[Cleanup] Fatal error:', err.message);
process.exit(1);
});
+1 -42
View File
@@ -18,8 +18,6 @@ const {
} = require('./collectorHelperDpi2');
const { Summary, AppStat } = require('./models/Schemas');
const { LookupApp } = require('./models/SchemasAux');
const { BASE_URL } = require('./netifyClientCore');
const { pruneOldData } = require('./dataRetention');
const SITE_UUIDS_STR = process.env.NETIFY_SITE_UUIDS || process.env.NETIFY_SITE_UUID;
@@ -257,47 +255,8 @@ async function collectSpecificAgents(agentUuids, siteUuid = SITE_UUIDS[0], delay
return { success: true, mode: 'specific_agents', agents_count: agentUuids.length, successful, results };
}
async function populateLookupApps(siteUuid = '') {
try {
const axios = require('axios');
const headers = { 'Accept': 'application/json' };
const res = await axios.get(`${BASE_URL}/lookup/applications`, {
headers, params: { settings_limit: 10000 }, timeout: 20000
});
const apps = res.data?.data;
if (!apps || !Array.isArray(apps)) {
console.log('[LookupApp] No application data from DPI API');
return { success: false, count: 0 };
}
let upserted = 0;
for (const a of apps) {
if (!a.id) continue;
const doc = {
id: a.id,
tag: a.tag || '',
label: a.label || '',
name: a.full_label || a.label || '',
full_name: a.full_label || '',
description: a.description || '',
favicon: a.favicon || '',
icon: a.icon || '',
logo: a.logo || '',
application_category: a.category || null
};
await LookupApp.updateOne({ id: a.id }, { $set: doc }, { upsert: true });
upserted++;
}
console.log(`[LookupApp] Upserted ${upserted} applications`);
return { success: true, count: upserted };
} catch (err) {
console.error('[LookupApp] Population error:', err.message);
return { success: false, count: 0 };
}
}
module.exports = {
collectAllAgents,
collectSpecificAgent,
collectSpecificAgents,
populateLookupApps
collectSpecificAgents
};
+40 -4
View File
@@ -115,16 +115,34 @@ async function collectFlows(agentUuid, timestamp, SITE_UUID, netify, label, ipTo
const blacklistedCategories = new Set(blacklistRules.filter(r => r.type === 'category').map(r => r.value.toLowerCase()));
const blacklistedDomains = new Set(blacklistRules.filter(r => r.type === 'domain').map(r => r.value.toLowerCase()));
const flowIdsInBatch = flowDocs.map(f => f.flow_id).filter(Boolean);
const existingFlowThreats = new Set(
await Threat.find({ flow_id: { $in: flowIdsInBatch } }).distinct('flow_id')
);
const threatDocs = [];
const eventDocs = [];
for (const f of flowDocs) {
let isViolation = false;
let categoryLabel = "";
// Check if domain is blacklisted
if (f.domain && blacklistedDomains.has(f.domain.toLowerCase())) {
for (const r of blacklistRules) {
if (r.type === 'domain') {
const val = r.value.toLowerCase();
// Direct domain match
if (f.domain && f.domain.toLowerCase().includes(val)) {
isViolation = true;
} else if (f.app_label && blacklistedDomains.has(f.app_label.toLowerCase())) {
break;
}
// Main domain part match against app label (e.g. "google" from "google.com")
const mainDomainPart = val.split('.')[0];
if (mainDomainPart && f.app_label && f.app_label.toLowerCase().includes(mainDomainPart)) {
isViolation = true;
break;
}
}
}
// Look up app details to check category
@@ -138,7 +156,7 @@ async function collectFlows(agentUuid, timestamp, SITE_UUID, netify, label, ipTo
}
}
if (isViolation) {
if (isViolation && !existingFlowThreats.has(f.flow_id)) {
threatDocs.push({
timestamp,
agent_uuid: f.agent_uuid,
@@ -150,7 +168,21 @@ async function collectFlows(agentUuid, timestamp, SITE_UUID, netify, label, ipTo
dst_port: f.dst_port,
protocol: f.protocol,
description: `Access to blacklisted app/domain: ${f.app_label} (${f.domain || 'N/A'})${categoryLabel ? ' - Category: ' + categoryLabel : ''}`,
event_at: new Date().toISOString()
event_at: new Date().toISOString(),
flow_id: f.flow_id
});
eventDocs.push({
timestamp,
agent_uuid: f.agent_uuid,
site_uuid: f.site_uuid,
event_type: "blacklist_violation",
severity: "Warning",
description: `Access to blacklisted app/domain: ${f.app_label} (${f.domain || 'N/A'})${categoryLabel ? ' - Category: ' + categoryLabel : ''}`,
ip_address: f.src_ip,
mac_address: f.src_mac,
event_at: new Date(),
flow_id: f.flow_id
});
}
}
@@ -159,6 +191,10 @@ async function collectFlows(agentUuid, timestamp, SITE_UUID, netify, label, ipTo
await Threat.insertMany(threatDocs);
console.log(`[Collector] ✓ ${threatDocs.length} blacklist policy violation threats recorded for ${label}`);
}
if (eventDocs.length > 0) {
await Event.insertMany(eventDocs);
console.log(`[Collector] ✓ ${eventDocs.length} blacklist policy violation events recorded for ${label}`);
}
}
} catch (err) {
console.error('[Collector] Blacklist detection failed:', err.message);
+44
View File
@@ -0,0 +1,44 @@
// diagnostic.js - Run: node proxy/diagnostic.js
const path = require('path');
require('dotenv').config({ path: path.join(__dirname, '..', '.env.local') });
const mongoose = require('mongoose');
const MONGODB_URI = process.env.MONGODB_URI || 'mongodb://localhost:27017/backone';
async function run() {
await mongoose.connect(MONGODB_URI);
const db = mongoose.connection.db;
// 1. Total count
const total = await db.collection('devicestats').countDocuments();
console.log('=== DeviceStat Total:', total);
// 2. Count for 10.6.10.44
const specific = await db.collection('devicestats').countDocuments({ ip_address: '10.6.10.44' });
console.log('=== Count for 10.6.10.44:', specific);
// 3. Sample doc for 10.6.10.44
const sample = await db.collection('devicestats').findOne({ ip_address: '10.6.10.44' });
console.log('=== Sample doc for 10.6.10.44:', JSON.stringify(sample, null, 2));
// 4. Duplicate groups (top 10)
const dups = await db.collection('devicestats').aggregate([
{ $group: { _id: { agent_uuid: '$agent_uuid', ip_address: '$ip_address' }, count: { $sum: 1 } } },
{ $match: { count: { $gt: 1 } } },
{ $sort: { count: -1 } },
{ $limit: 10 }
]).toArray();
console.log('=== Top duplicate groups:', JSON.stringify(dups, null, 2));
// 5. Check what collection name is actually used
const collections = await db.listCollections().toArray();
console.log('=== Collections:', collections.map(c => c.name));
// 6. Check indexes on devicestats
const indexes = await db.collection('devicestats').indexes();
console.log('=== Indexes on devicestats:', JSON.stringify(indexes, null, 2));
await mongoose.disconnect();
}
run().catch(err => { console.error('ERROR:', err.message); process.exit(1); });
+79
View File
@@ -0,0 +1,79 @@
// fix_devicestat_index.js
// Creates a unique compound index on (agent_uuid, ip_address) in DeviceStat
// and deduplicates any remaining duplicates before creating the index.
// Run: node proxy/fix_devicestat_index.js
const path = require('path');
require('dotenv').config({ path: path.join(__dirname, '..', '.env.local') });
const mongoose = require('mongoose');
const MONGODB_URI = process.env.MONGODB_URI || 'mongodb://localhost:27017/backone';
async function run() {
console.log('[Fix] Connecting to MongoDB...');
await mongoose.connect(MONGODB_URI);
const db = mongoose.connection.db;
const col = db.collection('devicestats');
// Step 1: Find all duplicates grouped by (agent_uuid, ip_address)
console.log('[Fix] Scanning for duplicates...');
const groups = await col.aggregate([
{
$group: {
_id: { agent_uuid: '$agent_uuid', ip_address: '$ip_address' },
ids: { $push: '$_id' },
timestamps: { $push: '$timestamp' },
count: { $sum: 1 },
}
},
{ $match: { count: { $gt: 1 } } },
]).toArray();
console.log(`[Fix] Found ${groups.length} duplicate groups.`);
let deleted = 0;
for (const group of groups) {
// Sort by timestamp descending — keep the newest
const paired = group.ids.map((id, i) => ({ id, ts: new Date(group.timestamps[i] || 0) }));
paired.sort((a, b) => b.ts - a.ts);
const toDelete = paired.slice(1).map(p => p.id);
const result = await col.deleteMany({ _id: { $in: toDelete } });
deleted += result.deletedCount;
}
console.log(`[Fix] Deleted ${deleted} duplicate documents.`);
const remaining = await col.countDocuments();
console.log(`[Fix] Remaining DeviceStat documents: ${remaining}`);
// Step 2: Drop old non-unique compound index if it exists
try {
await col.dropIndex('agent_uuid_1_timestamp_-1_ip_address_1');
console.log('[Fix] Dropped old compound index.');
} catch (e) {
console.log('[Fix] Old index not found or already dropped:', e.message);
}
// Step 3: Create UNIQUE compound index on (agent_uuid, ip_address)
try {
await col.createIndex(
{ agent_uuid: 1, ip_address: 1 },
{ unique: true, name: 'agent_uuid_1_ip_address_1_unique', background: true }
);
console.log('[Fix] Created unique index on (agent_uuid, ip_address).');
} catch (e) {
console.error('[Fix] Failed to create unique index:', e.message);
}
// Step 4: Verify indexes
const indexes = await col.indexes();
console.log('[Fix] Current indexes:');
indexes.forEach(idx => console.log(` - ${idx.name}: ${JSON.stringify(idx.key)} ${idx.unique ? '[UNIQUE]' : ''}`));
// Step 5: Verify 10.6.10.44
const cnt = await col.countDocuments({ ip_address: '10.6.10.44' });
console.log(`\n[Fix] Count for 10.6.10.44: ${cnt} (should be 1)`);
await mongoose.disconnect();
console.log('[Fix] Done.');
}
run().catch(err => { console.error('[Fix] Fatal:', err.message); process.exit(1); });
+2
View File
@@ -103,6 +103,7 @@ const ThreatSchema = new mongoose.Schema({
protocol: String,
description: String,
event_at: String,
flow_id: { type: String, index: true },
}, baseOptions);
// ─── App Categories (per agent) ───────────────────────────────────────────────
@@ -129,6 +130,7 @@ const EventSchema = new mongoose.Schema({
ip_address: String,
mac_address: String,
event_at: Date,
flow_id: { type: String, index: true },
}, baseOptions);
// ─── Compound indexes for common dashboard queries ─────────────────────────────
+1 -2
View File
@@ -170,8 +170,7 @@ async function fetchFlows(limit = 10000, agentUuid = null, siteUuid = null) {
const port = r.remote_port ?? null;
const portService = port ? (PORT_SERVICE_MAP[port] ?? `Port ${port}`) : null;
const appLabel = r.application?.label || portService;
const sniVal = typeof r.tls_sni === 'object' ? r.tls_sni?.label : r.tls_sni;
const domain = sniVal || r.dns_hostname || r.hostname || null;
const domain = r.tls_sni || r.dns_hostname || r.hostname || null;
return {
flow_id: String(r.flow_id ?? ''),
src_ip: r.local_ip?.address ?? null,
+2 -3
View File
@@ -111,14 +111,13 @@ async function fetchTopCountries(interval = 1440, limit = 100, agentUuid = null,
const ulMap = {};
if (ulData) {
for (const r of ulData) {
const cc = r.country?.code || r.country?.label;
const cc = r.country?.code;
if (cc) ulMap[cc] = r.upload ?? 0;
}
}
return dlData.map(r => {
const cc = r.country?.code || r.country?.label;
return {
country_code: cc || 'Unknown',
country_code: r.country?.code ?? 'Unknown',
country_name: r.country?.label ?? '',
download: r.download ?? 0,
upload: ulMap[r.country?.code] ?? 0,
+3 -12
View File
@@ -3,7 +3,7 @@
// Runs every 5 minutes, collecting data for all agents or a specific agent.
const cron = require('node-cron');
const { collectAllAgents, collectSpecificAgent, collectSpecificAgents, populateLookupApps } = require('./collector');
const { collectAllAgents, collectSpecificAgent, collectSpecificAgents } = require('./collector');
// Mode: 'all' = collect all agents, 'agent' = collect one specific agent, 'agents' = collect multiple agents
const COLLECT_MODE = process.env.PROXY_COLLECT_MODE || 'all';
@@ -21,7 +21,6 @@ let lastRunAt = null;
let lastRunResult = null;
let runCount = 0;
let lastCapacityLogAt = 0;
let lastAppLookupRefresh = 0;
/**
* Execute one collection cycle (called by cron and manual trigger).
@@ -60,14 +59,6 @@ async function runCollection() {
await logCapacityStats(`[PROXY] [MongoDB] Capacity Used after Run #${runCount}:`);
}
// Refresh application lookup catalog daily (expensive: fetches 5000+ apps)
if (now - (lastAppLookupRefresh || 0) >= CAPACITY_LOG_INTERVAL_MS) {
lastAppLookupRefresh = now;
await populateLookupApps('').catch(err =>
console.error('[Scheduler] LookupApp refresh failed:', err.message)
);
}
return lastRunResult;
} catch (err) {
console.error('[Scheduler] Unhandled error during collection:', err.message);
@@ -105,8 +96,8 @@ function startScheduler() {
// Initial run immediately on startup (async, do not block server start)
setTimeout(async () => {
// 1. Sync application lookup catalog from DPI API
populateLookupApps('').catch(err => console.error('[Scheduler] Initial LookupApp sync failed:', err.message));
// 1. Sync dictionary first
// await netifyClient.syncApplicationDictionary();
// 2. Start normal telemetry collection
runCollection().catch(err => console.error('[Scheduler] Initial run error:', err.message));
+71
View File
@@ -0,0 +1,71 @@
// test_api.js - Tests the actual metadata-detail API endpoint
// Run: node proxy/test_api.js
const http = require('http');
function request(path) {
return new Promise((resolve, reject) => {
const options = {
hostname: 'localhost',
port: 3001,
path,
method: 'GET',
};
const req = http.request(options, res => {
let body = '';
res.on('data', chunk => body += chunk);
res.on('end', () => {
try { resolve({ status: res.statusCode, data: JSON.parse(body) }); }
catch (e) { resolve({ status: res.statusCode, raw: body }); }
});
});
req.on('error', reject);
req.end();
});
}
async function main() {
// Test 1: netbios_hostname for 10.6.10.44
console.log('\n=== TEST 1: netbios_hostname=10.6.10.44 ===');
try {
const r1 = await request('/api/dashboard/metadata-detail?type=netbios_hostname&value=10.6.10.44');
console.log('Status:', r1.status);
if (r1.data) {
console.log('Count:', r1.data.count);
console.log('First 3 rows:', JSON.stringify(r1.data.data?.slice(0, 3), null, 2));
} else {
console.log('Raw:', r1.raw?.slice(0, 500));
}
} catch (e) {
console.log('ERROR (maybe backend is on different port):', e.message);
}
// Test 2: Try port 3000 (Next.js API routes)
console.log('\n=== TEST 2: via Next.js port 3000 ===');
try {
const r2 = await request('/api/dashboard/metadata-detail?type=netbios_hostname&value=10.6.10.44');
const options2 = { hostname: 'localhost', port: 3000, path: '/api/dashboard/metadata-detail?type=netbios_hostname&value=10.6.10.44', method: 'GET' };
const r3 = await new Promise((resolve, reject) => {
const req = http.request(options2, res => {
let body = '';
res.on('data', chunk => body += chunk);
res.on('end', () => {
try { resolve({ status: res.statusCode, data: JSON.parse(body) }); }
catch (e) { resolve({ status: res.statusCode, raw: body?.slice(0, 500) }); }
});
});
req.on('error', reject);
req.end();
});
console.log('Port 3000 - Status:', r3.status);
if (r3.data) {
console.log('Count:', r3.data.count);
console.log('First 3 rows:', JSON.stringify(r3.data.data?.slice(0, 3), null, 2));
} else {
console.log('Raw:', r3.raw);
}
} catch (e) {
console.log('Port 3000 ERROR:', e.message);
}
}
main().catch(console.error);
+1
View File
@@ -0,0 +1 @@
const mongoose = require('mongoose'); require('dotenv').config({ path: '../.env.local' }); const { LookupApp } = require('./models/Schemas'); async function test() { await mongoose.connect(process.env.MONGODB_URI || 'mongodb://127.0.0.1:27017/backone_dpi'); const sample = await LookupApp.findOne({ tag: /youtube/i }).lean(); console.log(JSON.stringify(sample, null, 2)); await mongoose.disconnect(); } test().catch(console.error);
+89
View File
@@ -0,0 +1,89 @@
// test_metadata_detail.js - Test after restart
// Run: node proxy/test_metadata_detail.js
const http = require('http');
function post(path, body) {
return new Promise((resolve, reject) => {
const data = JSON.stringify(body);
const options = {
hostname: 'localhost', port: 3001, path, method: 'POST',
headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(data) },
};
const req = http.request(options, res => {
let buf = '';
res.on('data', c => buf += c);
res.on('end', () => {
try { resolve({ status: res.statusCode, headers: res.headers, data: JSON.parse(buf) }); }
catch { resolve({ status: res.statusCode, raw: buf }); }
});
});
req.on('error', reject);
req.write(data);
req.end();
});
}
function get(path, cookie) {
return new Promise((resolve, reject) => {
const options = {
hostname: 'localhost', port: 3001, path, method: 'GET',
headers: cookie ? { Cookie: cookie } : {},
};
const req = http.request(options, res => {
let buf = '';
res.on('data', c => buf += c);
res.on('end', () => {
try { resolve({ status: res.statusCode, data: JSON.parse(buf) }); }
catch { resolve({ status: res.statusCode, raw: buf?.slice(0, 300) }); }
});
});
req.on('error', reject);
req.end();
});
}
async function main() {
// Step 1: Login to get cookie
console.log('=== Step 1: Login ===');
const login = await post('/api/auth/login', { username: 'admin', password: 'admin123' });
console.log('Login status:', login.status);
const setCookie = login.headers?.['set-cookie'];
let cookie = '';
if (setCookie) {
cookie = setCookie.map(c => c.split(';')[0]).join('; ');
console.log('Cookie obtained:', cookie.slice(0, 60) + '...');
} else {
console.log('No cookie received. Auth response:', JSON.stringify(login.data));
// Try with a known admin credential
}
// Step 2: Test health
console.log('\n=== Step 2: Health Check ===');
const health = await get('/api/health', cookie);
console.log('Health:', health.status, JSON.stringify(health.data));
// Step 3: Test metadata-detail netbios_hostname
console.log('\n=== Step 3: metadata-detail netbios_hostname=10.6.10.44 ===');
const r = await get('/api/dashboard/metadata-detail?type=netbios_hostname&value=10.6.10.44', cookie);
console.log('Status:', r.status);
if (r.data) {
console.log('Count (should be 1):', r.data.count);
console.log('Data:', JSON.stringify(r.data.data, null, 2));
} else {
console.log('Raw:', r.raw);
}
// Step 4: Verify DB has 1 doc for 10.6.10.44
console.log('\n=== Step 4: Direct DB verification ===');
const mongoose = require('mongoose');
const path = require('path');
require('dotenv').config({ path: path.join(__dirname, '..', '.env.local') });
await mongoose.connect(process.env.MONGODB_URI || 'mongodb://localhost:27017/backone');
const db = mongoose.connection.db;
const cnt = await db.collection('devicestats').countDocuments({ ip_address: '10.6.10.44' });
console.log('DB count for 10.6.10.44:', cnt, '(expected: 1)');
await mongoose.disconnect();
}
main().catch(console.error);
+1
View File
@@ -0,0 +1 @@
const mongoose = require('mongoose'); require('dotenv').config({ path: '../.env.local' }); const { syncApplicationDictionary } = require('./netifyClient'); const { LookupApp } = require('./models/Schemas'); async function test() { await mongoose.connect(process.env.MONGODB_URI || 'mongodb://127.0.0.1:27017/backone_dpi'); console.log('Connected to DB'); await syncApplicationDictionary(); const count = await LookupApp.countDocuments(); console.log('Total LookupApps in DB:', count); await mongoose.disconnect(); } test().catch(console.error);
+56
View File
@@ -0,0 +1,56 @@
// verify_fix.js - Directly verify the MongoDB aggregation returns correct results
// This simulates what the backend metadata-detail endpoint does after the fix.
// Run: node proxy/verify_fix.js
const path = require('path');
require('dotenv').config({ path: path.join(__dirname, '..', '.env.local') });
const mongoose = require('mongoose');
async function run() {
await mongoose.connect(process.env.MONGODB_URI || 'mongodb://localhost:27017/backone');
const db = mongoose.connection.db;
const col = db.collection('devicestats');
const testValues = ['10.6.10.44'];
// Also find other common device_labels to test
const sample = await col.find({}).limit(20).toArray();
const labels = [...new Set(sample.map(d => d.device_label).filter(Boolean))];
console.log('Sample device_labels to test:', labels.slice(0, 5));
for (const value of [...testValues, ...labels.slice(0, 3)]) {
// Count raw docs matching
const rawCount = await col.countDocuments({ device_label: value });
// Simulate the new aggregation pipeline
const aggResult = await col.aggregate([
{ $match: { device_label: value } },
{ $sort: { timestamp: -1 } },
{ $group: {
_id: '$ip_address',
mac_address: { $first: '$mac_address' },
device_label: { $first: '$device_label' },
download: { $max: '$download' },
upload: { $max: '$upload' },
}},
{ $sort: { download: -1 } },
]).toArray();
const status = rawCount > aggResult.length ? '✅ FIXED (was duplicated)' : '✓ OK';
console.log(`\ndevice_label="${value}": raw=${rawCount} rows → aggregated=${aggResult.length} unique devices ${status}`);
if (aggResult.length > 0) {
console.log(' First result:', JSON.stringify(aggResult[0], null, 2));
}
}
// Verify unique index exists
const indexes = await col.indexes();
const uniqueIdx = indexes.find(i => i.unique && i.key.agent_uuid && i.key.ip_address);
console.log('\n=== Unique Index on (agent_uuid, ip_address):', uniqueIdx ? `✅ EXISTS (${uniqueIdx.name})` : '❌ MISSING');
// Final count
const total = await col.countDocuments();
console.log('=== Total DeviceStat docs:', total);
await mongoose.disconnect();
}
run().catch(err => { console.error(err.message); process.exit(1); });
+3
View File
@@ -0,0 +1,3 @@
/home/adminbackend/web/demoplace.my.id/public_html/backend/server.js
/home/adminbackend/web/demoplace.my.id/public_html/.next/standalone/server.js
/home/adminbackend/web/demoplace.my.id/public_html/server.js
+61
View File
@@ -0,0 +1,61 @@
const https = require('https');
const PAGES = [
'/',
'/network-infrastructure',
'/agents',
'/devices',
'/flows',
'/apps',
'/network-intelligence',
'/dns',
'/geography',
'/dpi-analytics',
'/lookup',
'/intelligence',
'/threats',
'/events',
'/security-audit',
'/help'
];
function fetchPage(route) {
return new Promise((resolve) => {
https.get(`https://demoplace.my.id${route}`, (res) => {
let data = '';
res.on('data', chunk => data += chunk);
res.on('end', () => {
const titleMatch = data.match(/<title>(.*?)<\/title>/i);
const descMatch = data.match(/<meta name="description" content="(.*?)"/i);
const title = titleMatch ? titleMatch[1] : 'N/A';
const desc = descMatch ? descMatch[1] : 'N/A';
// Find logo references
const logos = [];
const logoRegex = /src="([^"]*(?:logo|icon)[^"]*)"/gi;
let match;
while ((match = logoRegex.exec(data)) !== null) {
if (!logos.includes(match[1])) logos.push(match[1]);
}
resolve({ route, statusCode: res.statusCode, title, desc, logos });
});
}).on('error', (err) => {
resolve({ route, error: err.message });
});
});
}
async function main() {
console.log('=== AUDITING DEMOPLACE.MY.ID PAGES ===\n');
for (const page of PAGES) {
const res = await fetchPage(page);
console.log(`Route: ${res.route}`);
console.log(` Title : ${res.title}`);
console.log(` Description : ${res.desc}`);
console.log(` Logos/Icons : ${res.logos ? res.logos.join(', ') : 'None'}`);
console.log('---');
}
}
main();
+57
View File
@@ -0,0 +1,57 @@
const { Client } = require('ssh2');
const fs = require('fs');
const remoteScript = `
const fs = require('fs');
const path = require('path');
const root = '/home/adminbackend/web/demoplace.my.id/public_html';
console.log('=== 1. LOGOS & IMAGES IN PUBLIC ===');
try {
const publicFiles = fs.readdirSync(path.join(root, 'public'));
console.log('Files in public/:', publicFiles);
} catch(e) { console.log('Err:', e.message); }
console.log('\\n=== 2. AUDITING NAVIGATION GROUPS IN STANDALONE CHUNKS ===');
const chunksDir = path.join(root, '.next/standalone/.next/server/chunks');
if (fs.existsSync(chunksDir)) {
const files = fs.readdirSync(chunksDir);
files.forEach(f => {
if (!f.endsWith('.js')) return;
const content = fs.readFileSync(path.join(chunksDir, f), 'utf8');
if (content.includes('Overview Dashboard') || content.includes('Network Topology')) {
console.log('Chunk File:', f);
const match = content.match(/title:\"OVERVIEW\".*?title:\"SECURITY & ALERTS\".*?\]/);
if (match) {
console.log('NAV MATCH:', match[0]);
}
}
});
}
console.log('\\n=== 3. APP ROUTE DIRECTORIES ===');
try {
const appDir = path.join(root, '.next/standalone/.next/server/app/(dashboard)');
if (fs.existsSync(appDir)) {
console.log('Dashboard routes in .next/standalone:', fs.readdirSync(appDir));
}
} catch(e) { console.log('Err app dir:', e.message); }
`;
const conn = new Client();
conn.on('ready', () => {
conn.exec(`node -e ${JSON.stringify(remoteScript)}`, (err, stream) => {
let out = '';
stream.on('data', d => out += d);
stream.on('close', () => {
console.log(out);
conn.end();
});
});
}).connect({
host: '103.185.47.52',
port: 2222,
username: 'adminbackend',
password: 'htEo7x6LsBQiEHHH'
});
+16 -18
View File
@@ -11,7 +11,7 @@ const config = {
password: 'htEo7x6LsBQiEHHH'
};
const DEPLOY_DIR = 'web/demoplace.my.id/public_html';
const DEPLOY_DIR = 'backone-production';
async function createZip() {
console.log("Packaging application...");
@@ -30,15 +30,11 @@ async function createZip() {
// Add static assets (Next.js standalone requires these copied over)
archive.directory(path.join(__dirname, '../.next/static'), '.next/static');
archive.directory(path.join(__dirname, '../public'), 'public');
archive.directory(path.join(__dirname, '../public'), false);
// Add backend and proxy
archive.directory(path.join(__dirname, '../backend'), 'backend');
archive.directory(path.join(__dirname, '../proxy'), 'proxy');
// Add PM2 ecosystem config
archive.file(path.join(__dirname, '../ecosystem.config.js'), { name: 'ecosystem.config.js' });
// Add production env
archive.file(path.join(__dirname, '../.env.production'), { name: '.env.production' });
@@ -85,24 +81,26 @@ async function deploy() {
await sftp.put(zipPath, `${targetDir}/deploy.zip`);
console.log("Extracting package on server...");
await runSSH(`cd ${targetDir} && rm -rf .next && unzip -o deploy.zip && rm deploy.zip`);
await runSSH(`cd ${targetDir} && unzip -o deploy.zip && rm deploy.zip`);
console.log("Creating symlink for static files...");
await runSSH(`cd ${targetDir} && rm -rf _next && ln -s .next _next`);
console.log("Installing production dependencies for root, backend & proxy...");
await runSSH(`. ~/.nvm/nvm.sh && nvm use 20 && cd ${targetDir} && npm install --production`);
await runSSH(`. ~/.nvm/nvm.sh && nvm use 20 && cd ${targetDir}/backend && npm install --production`);
await runSSH(`. ~/.nvm/nvm.sh && nvm use 20 && cd ${targetDir}/proxy && npm install --production`);
console.log("Installing production dependencies for backend & proxy...");
await runSSH(`cd ${targetDir}/backend && npm install --production`);
await runSSH(`cd ${targetDir}/proxy && npm install --production`);
console.log("Configuring PM2...");
// PM2 ecosystem is now packaged in deploy.zip directly, no need to generate via bash echo.
// Create ecosystem file for PM2
const ecosystem = `
module.exports = {
apps: [
{ name: 'backone-proxy', script: './proxy/index.js', env_file: '.env.production' },
{ name: 'backone-backend', script: './backend/server.js', env_file: '.env.production' },
{ name: 'backone-frontend', script: 'server.js', env_file: '.env.production' }
]
};`;
await runSSH(`cd ${targetDir} && echo "${ecosystem.replace(/\n/g, '\\n')}" > ecosystem.config.js`);
console.log("Restarting PM2 processes...");
// Clean delete old PM2 processes if they exist to apply new paths, then start fresh
await runSSH(`. ~/.nvm/nvm.sh && nvm use 20 && npx pm2 delete backone-frontend backone-backend backone-proxy || true`);
const pm2Result = await runSSH(`. ~/.nvm/nvm.sh && nvm use 20 && cd ${targetDir} && npx pm2 start ecosystem.config.js`);
const pm2Result = await runSSH(`cd ${targetDir} && pm2 start ecosystem.config.js || pm2 restart ecosystem.config.js`);
console.log(pm2Result);
console.log("Deployment completed successfully!");
+84
View File
@@ -0,0 +1,84 @@
const https = require('https');
const ROUTES = [
'/',
'/agents',
'/apps',
'/devices',
'/dns',
'/dpi-analytics',
'/encryption-audit',
'/events',
'/flows',
'/geography',
'/intelligence',
'/lookup',
'/network-infrastructure',
'/network-intelligence',
'/network-topology',
'/security-audit',
'/threat-intelligence',
'/threats',
'/traffic-categories',
'/help',
'/login'
];
function auditRoute(route) {
return new Promise((resolve) => {
https.get(`https://demoplace.my.id${route}`, (res) => {
let data = '';
res.on('data', c => data += c);
res.on('end', () => {
const title = (data.match(/<title>(.*?)<\/title>/i) || [])[1] || 'N/A';
const desc = (data.match(/<meta name="description" content="(.*?)"/i) || [])[1] || 'N/A';
const icons = [];
const iconMatches = data.matchAll(/rel="icon" href="([^"]+)"/gi);
for (const m of iconMatches) icons.push(m[1]);
const imgs = [];
const imgMatches = data.matchAll(/src="([^"]*(?:logo|brand|icon|png|svg|jpg)[^"]*)"/gi);
for (const m of imgMatches) {
if (!imgs.includes(m[1])) imgs.push(m[1]);
}
const headers = [];
const hMatches = data.matchAll(/<h[123][^>]*>(.*?)<\/h[123]>/gi);
for (const m of hMatches) {
const clean = m[1].replace(/<[^>]+>/g, '').trim();
if (clean && !headers.includes(clean)) headers.push(clean);
}
resolve({
route,
status: res.statusCode,
is404: data.includes('404: This page could not be found'),
title,
desc,
icons,
imgs,
headers
});
});
}).on('error', err => resolve({ route, error: err.message }));
});
}
async function main() {
console.log('=== DETAILED AUDIT OF DEMOPLACE.MY.ID ===\n');
for (const r of ROUTES) {
const res = await auditRoute(r);
if (res.is404) {
console.log(`❌ [404 NOT FOUND] ${res.route}`);
} else {
console.log(`✅ [200 OK] ${res.route}`);
console.log(` Title : ${res.title}`);
console.log(` Icons : ${res.icons.join(', ')}`);
console.log(` Images/Logo: ${res.imgs.join(', ')}`);
console.log(` Headers : ${res.headers.join(' | ')}`);
}
console.log('--------------------------------------------------');
}
}
main();
+18
View File
@@ -0,0 +1,18 @@
const { Client } = require('ssh2');
const conn = new Client();
conn.on('ready', () => {
conn.exec(`ls -la /home/adminbackend/web/demoplace.my.id/public_html/.next/standalone/.next/server/app/\\(dashboard\\)/ 2>/dev/null || ls -la /home/adminbackend/web/demoplace.my.id/public_html/.next/server/app/`, (err, stream) => {
let out = '';
stream.on('data', d => out += d);
stream.on('close', () => {
console.log('=== REMOTE DASHBOARD APP ROUTES ===\n' + out);
conn.end();
});
});
}).connect({
host: '103.185.47.52',
port: 2222,
username: 'adminbackend',
password: 'htEo7x6LsBQiEHHH'
});
+28
View File
@@ -0,0 +1,28 @@
if (typeof globalThis.crypto === 'undefined') {
globalThis.crypto = require('crypto');
}
const mongoose = require('mongoose');
const path = require('path');
require('dotenv').config({ path: path.join(__dirname, '..', '.env.production') });
const MONGODB_URI = process.env.MONGODB_URI;
const { collectAllAgents } = require('../proxy/collector');
async function run() {
console.log('[Trigger] Connecting to MongoDB...');
await mongoose.connect(MONGODB_URI);
console.log('[Trigger] Connected. Starting collection cycle...');
try {
const res = await collectAllAgents();
console.log('[Trigger] Collection cycle finished successfully:', res);
} catch (err) {
console.error('[Trigger] Collection cycle failed:', err);
} finally {
await mongoose.connection.close();
console.log('[Trigger] MongoDB connection closed.');
}
}
run();
+1 -1
View File
@@ -240,7 +240,7 @@ export function Sidebar() {
className={cn(
"group flex items-center gap-3 rounded-xl px-3 py-2 text-xs font-semibold tracking-wide transition-all duration-300",
isActive
? "bg-primary/10 text-primary border-l-2 border-primary font-bold shadow-md shadow-primary/5"
? "bg-primary/20 text-white font-bold border border-primary/30 shadow-lg shadow-primary/10"
: "text-slate-400 hover:bg-white/[0.02] hover:text-slate-200"
)}
>