style(sidebar): match active item highlight and header buttons 100% with demoplace.my.id
This commit is contained in:
1 parent
7f133a287b
commit
960322e30a
49 files changed
+3226
-1503
No files matched your search
+2
-2
@@ -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
|
||||
|
||||
@@ -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}`) |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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' });
|
||||
}
|
||||
|
||||
@@ -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 ─────────────────────────────
|
||||
|
||||
@@ -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);
|
||||
@@ -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' }
|
||||
});
|
||||
|
||||
@@ -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([
|
||||
|
||||
@@ -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`);
|
||||
|
||||
@@ -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
@@ -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({
|
||||
@@ -187,8 +90,19 @@ router.post('/renew', requireAuth, async (req, res) => {
|
||||
try {
|
||||
const user = await User.findById(req.user.id);
|
||||
if (!user) return res.status(404).json({ error: 'User tidak ditemukan' });
|
||||
|
||||
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);
|
||||
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' });
|
||||
});
|
||||
|
||||
@@ -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' }
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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*`,
|
||||
},
|
||||
];
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
Generated
+1860
-1186
File diff suppressed because it is too large.
Load diff
+2
-2
@@ -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",
|
||||
|
||||
@@ -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))"
|
||||
|
||||
|
||||
@@ -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); });
|
||||
@@ -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
@@ -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
|
||||
};
|
||||
@@ -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())) {
|
||||
isViolation = true;
|
||||
} else if (f.app_label && blacklistedDomains.has(f.app_label.toLowerCase())) {
|
||||
isViolation = true;
|
||||
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;
|
||||
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);
|
||||
|
||||
@@ -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); });
|
||||
@@ -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); });
|
||||
@@ -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 ─────────────────────────────
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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).
|
||||
@@ -59,14 +58,6 @@ async function runCollection() {
|
||||
const { logCapacityStats } = require('./db/capacityTracker');
|
||||
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) {
|
||||
@@ -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));
|
||||
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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); });
|
||||
@@ -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
|
||||
@@ -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();
|
||||
@@ -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'
|
||||
});
|
||||
@@ -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!");
|
||||
|
||||
@@ -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();
|
||||
@@ -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'
|
||||
});
|
||||
@@ -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();
|
||||
|
||||
@@ -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"
|
||||
)}
|
||||
>
|
||||
|
||||
Reference in new issue
Block a user