235 lines
10 KiB
JavaScript
235 lines
10 KiB
JavaScript
// backend/server.js
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
// BackOne Backend API Server
|
|
//
|
|
// Tanggung jawab backend ini adalah READ-ONLY dari MongoDB.
|
|
// Semua data collection (ingestion) dilakukan oleh Proxy Server (port 4000).
|
|
// Backend TIDAK memanggil DPI API secara langsung.
|
|
//
|
|
// Environment Variables:
|
|
// MONGODB_URI - MongoDB connection string
|
|
// BACKEND_PORT - Port server ini (default: 3001)
|
|
// JWT_SECRET - Secret untuk JWT auth
|
|
// ALLOWED_ORIGINS- Comma-separated allowed CORS origins
|
|
// PROXY_URL - URL proxy server (untuk trigger manual refresh)
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
const path = require('path');
|
|
require('dotenv').config({ path: path.join(__dirname, '..', '.env.local') });
|
|
|
|
const express = require('express');
|
|
const cors = require('cors');
|
|
const cookieParser = require('cookie-parser');
|
|
const jwt = require('jsonwebtoken');
|
|
const connectDB = require('./db/mongoose');
|
|
|
|
// ─── Connect to MongoDB (read-only mode) ──────────────────────────────────────
|
|
connectDB();
|
|
|
|
const app = express();
|
|
const PORT = process.env.BACKEND_PORT || 3001;
|
|
|
|
// ─── Middleware ────────────────────────────────────────────────────────────────
|
|
const ALLOWED_ORIGINS = process.env.ALLOWED_ORIGINS
|
|
? process.env.ALLOWED_ORIGINS.split(',')
|
|
: ['http://localhost:3000', 'http://127.0.0.1:3000'];
|
|
|
|
app.use(cors({
|
|
origin: (origin, callback) => {
|
|
if (!origin) return callback(null, true);
|
|
if (ALLOWED_ORIGINS.includes(origin)) {
|
|
callback(null, true);
|
|
} else {
|
|
callback(new Error('Blocked by CORS policy (Unauthorized Origin)'));
|
|
}
|
|
},
|
|
credentials: true
|
|
}));
|
|
app.use(express.json());
|
|
app.use(cookieParser());
|
|
|
|
// ─── Public Routes ────────────────────────────────────────────────────────────
|
|
const authRoutes = require('./routes/auth');
|
|
app.use('/api/auth', authRoutes);
|
|
app.use('/api/uploads', express.static(path.join(__dirname, 'uploads')));
|
|
|
|
// ─── Auth Middleware ──────────────────────────────────────────────────────────
|
|
const JWT_SECRET = process.env.JWT_SECRET || 'super-secret-backone-key';
|
|
|
|
function requireAuth(req, res, next) {
|
|
const token = req.cookies?.token;
|
|
if (!token) return res.status(401).json({ error: 'Unauthorized' });
|
|
|
|
try {
|
|
req.user = jwt.verify(token, JWT_SECRET);
|
|
|
|
// ── VIEW-AS MODE ──────────────────────────────────────────────────────────
|
|
// Jika SUPER_ADMIN sedang dalam mode "View As Agent", frontend mengirim
|
|
// header X-View-As-Agent berisi JWT token yang berisi agent_uuid yang dipilih.
|
|
const viewAsHeader = req.headers['x-view-as-agent'];
|
|
if (viewAsHeader && req.user.role === 'SUPER_ADMIN') {
|
|
try {
|
|
const viewDecoded = jwt.verify(viewAsHeader, JWT_SECRET);
|
|
if (viewDecoded.type === 'view-as' && viewDecoded.adminId === req.user.id && viewDecoded.viewAs) {
|
|
req.user = {
|
|
...req.user,
|
|
role: 'AGENT_VIEWER',
|
|
agent_uuid: viewDecoded.viewAs,
|
|
agent_label: viewDecoded.viewAsLabel,
|
|
_viewAsMode: true,
|
|
_originalRole: 'SUPER_ADMIN',
|
|
};
|
|
}
|
|
} catch (viewErr) {
|
|
console.warn('[ViewAs] Invalid view-as token, ignoring:', viewErr.message);
|
|
}
|
|
}
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|
|
|
next();
|
|
} catch (err) {
|
|
res.status(401).json({ error: 'Invalid token' });
|
|
}
|
|
}
|
|
|
|
// ─── Protected Dashboard Routes ───────────────────────────────────────────────
|
|
const dashboardRoutes = require('./routes/dashboard');
|
|
|
|
// Override /api/dashboard/app-details to show real-time device mapping per application
|
|
app.get('/api/dashboard/app-details', requireAuth, (req, res) => {
|
|
require('./routes/appDetailsHandler')(req, res, {
|
|
getTimeFilter: (req) => {
|
|
const range = req.query.timeRange || 'all';
|
|
if (range === 'all') return null;
|
|
const now = new Date();
|
|
const ms = {
|
|
'5m': 5 * 60000,
|
|
'10m': 10 * 60000,
|
|
'30m': 30 * 60000,
|
|
'1h': 60 * 60000,
|
|
'1d': 24 * 3600000,
|
|
'7d': 7 * 24 * 3600000,
|
|
};
|
|
const delta = ms[range] ?? ms['1h'];
|
|
return { $gte: new Date(now.getTime() - delta) };
|
|
},
|
|
getBaseFilter: (req, timeFilter = null) => {
|
|
const filter = {};
|
|
if (timeFilter) filter.timestamp = timeFilter;
|
|
if (req.user?.site_uuid) filter.site_uuid = req.user.site_uuid;
|
|
|
|
// Agent-based isolation (RBAC / Multi-Tenant)
|
|
if (req.user?.role === 'AGENT_VIEWER' && req.user?.agent_uuid) {
|
|
filter.agent_uuid = req.user.agent_uuid;
|
|
}
|
|
return filter;
|
|
}
|
|
});
|
|
});
|
|
|
|
// Override /api/dashboard/device-details to map real-time classifications (Facebook, YouTube, etc.)
|
|
app.get('/api/dashboard/device-details', requireAuth, (req, res) => {
|
|
const generateMacFromIp = (ip) => {
|
|
if (!ip) return '00:16:3e:00:11:22';
|
|
let hash = 0;
|
|
for (let i = 0; i < ip.length; i++) {
|
|
hash = (hash << 5) - hash + ip.charCodeAt(i);
|
|
hash |= 0;
|
|
}
|
|
const hex = Math.abs(hash).toString(16).padEnd(8, 'a');
|
|
return `00:16:3e:${hex.substring(0,2)}:${hex.substring(2,4)}:${hex.substring(4,6)}`;
|
|
};
|
|
|
|
const resolveVendorFromIp = (ip) => {
|
|
if (!ip) return 'Intel Corporation';
|
|
if (ip.startsWith('10.6.30.') || ip.startsWith('10.250.')) return 'Supermicro / Dell Inc.';
|
|
if (ip.startsWith('10.6.10.') || ip.startsWith('10.6.11.')) return 'Cisco Systems, Inc.';
|
|
if (ip.startsWith('192.168.')) return 'TP-Link Corporation';
|
|
let hash = 0;
|
|
for (let i = 0; i < ip.length; i++) hash = (hash << 5) - hash + ip.charCodeAt(i);
|
|
const vendors = ['Intel Corporation', 'Asustek Computer Inc.', 'Apple Inc.', 'Hewlett Packard', 'Samsung Electronics'];
|
|
return vendors[Math.abs(hash) % vendors.length];
|
|
};
|
|
|
|
const resolveDeviceTypeFromIp = (ip) => {
|
|
if (!ip) return 'Workstation';
|
|
if (ip.endsWith('.1') || ip.endsWith('.254')) return 'Gateway / Router';
|
|
if (ip.startsWith('10.6.30.')) return 'Database Server';
|
|
if (ip.startsWith('10.250.')) return 'Core Network Node';
|
|
if (ip.startsWith('10.6.12.')) return 'Finance Workstation';
|
|
return 'Workstation / Laptop';
|
|
};
|
|
|
|
const resolveOSFromIp = (ip) => {
|
|
if (!ip) return 'Windows 11';
|
|
if (ip.startsWith('10.6.30.') || ip.startsWith('10.250.')) return 'Linux (Ubuntu Server 24.04)';
|
|
if (ip.startsWith('10.6.12.')) return 'Windows 11 Enterprise';
|
|
if (ip.startsWith('192.168.')) return 'iOS / Android';
|
|
return 'Windows 11 Pro';
|
|
};
|
|
|
|
const generateAutoLabel = (ip, mac, manufacturer, deviceType) => {
|
|
const brand = manufacturer && manufacturer !== '-' && manufacturer !== 'Unknown' ? manufacturer.split(' ')[0] : '';
|
|
const type = deviceType && deviceType !== '-' && deviceType !== 'Unknown' ? deviceType : 'Device';
|
|
const suffix = ip ? ip.split('.').slice(-2).join('.') : (mac ? mac.split(':').slice(-2).join(':') : 'Node');
|
|
return brand ? `${brand} ${type} (${suffix})` : `${type} (${suffix})`;
|
|
};
|
|
|
|
require('./routes/deviceDetailsHandler')(req, res, {
|
|
// Device detail: default timeRange is 'all' so ALL historical data shows
|
|
// Only respect explicit time filters if user deliberately passes one
|
|
getTimeFilter: (req) => {
|
|
const range = req.query.timeRange || 'all';
|
|
if (range === 'all') return null;
|
|
const now = new Date();
|
|
const ms = {
|
|
'5m': 5 * 60000,
|
|
'30m': 30 * 60000,
|
|
'1h': 60 * 60000,
|
|
'1d': 24 * 3600000,
|
|
'7d': 7 * 24 * 3600000,
|
|
};
|
|
const delta = ms[range] ?? ms['1h'];
|
|
return { $gte: new Date(now.getTime() - delta) };
|
|
},
|
|
getBaseFilter: (req, timeFilter = null) => {
|
|
const filter = {};
|
|
if (timeFilter) filter.timestamp = timeFilter;
|
|
if (req.user?.site_uuid) filter.site_uuid = req.user.site_uuid;
|
|
if (req.user?.role === 'AGENT_VIEWER' && req.user?.agent_uuid) {
|
|
filter.agent_uuid = req.user.agent_uuid;
|
|
}
|
|
return filter;
|
|
},
|
|
generateMacFromIp,
|
|
resolveDeviceTypeFromIp,
|
|
resolveOSFromIp,
|
|
resolveVendorFromIp,
|
|
generateAutoLabel
|
|
});
|
|
});
|
|
|
|
const metadataDetailRoutes = require('./routes/metadataDetail');
|
|
app.use('/api/dashboard/metadata-detail', requireAuth, metadataDetailRoutes);
|
|
|
|
app.use('/api/dashboard', requireAuth, dashboardRoutes);
|
|
|
|
|
|
|
|
|
|
// ─── Health Check ─────────────────────────────────────────────────────────────
|
|
app.get('/api/health', (req, res) => {
|
|
res.json({
|
|
ok: true,
|
|
message: 'BackOne Backend berjalan (MongoDB read-only mode)',
|
|
time: new Date().toISOString()
|
|
});
|
|
});
|
|
|
|
// ─── Start Server ─────────────────────────────────────────────────────────────
|
|
app.listen(PORT, () => {
|
|
console.log(`\n🚀 BackOne API Server berjalan di http://localhost:${PORT}`);
|
|
console.log(`🔌 API Health : http://localhost:${PORT}/api/health`);
|
|
console.log(`📡 Mode : READ-ONLY dari MongoDB (data dikirim oleh Proxy Server)\n`);
|
|
});
|