feat: complete deployment codebase for BackOne DPI dashboard with dynamic telemetry, rebranding, database size tracking, and modular design

This commit is contained in:
Rafif-Riqullah-Siregar committed 2026-07-10 11:52:58 +07:00
1 parent 6cf3c6c7e5
commit c316f3171b
226 files changed
+21152 -8397

No files matched your search

+172
View File
@@ -0,0 +1,172 @@
// backend/routes/auth/core.js
const express = require('express');
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');
const User = require('../../models/User');
const { makeToken, setCookieToken, requireAuth, JWT_SECRET } = require('./helpers');
const router = express.Router();
// ─── Auto-seed SUPER_ADMIN dan SOC_ANALYST 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 analystCount = await User.countDocuments({ role: 'SOC_ANALYST' });
if (analystCount === 0) {
const hash = bcrypt.hashSync('analyst', 10);
await User.create({
username: 'analyst',
password_hash: hash,
account_name: 'BackOne SOC Analyst',
role: 'SOC_ANALYST',
site_uuid: process.env.NETIFY_SITE_UUID || null,
agent_uuid: null,
});
console.log('[Auth] ✓ Default SOC_ANALYST created: analyst / analyst');
}
} catch (err) {
console.warn('[Auth] Seed skipped (MongoDB not ready yet):', err.message);
}
})();
// ─── POST /api/auth/login ─────────────────────────────────────────────────────
router.post('/login', async (req, res) => {
try {
const { username, password } = req.body;
if (!username || !password) {
return res.status(400).json({ error: 'Username and password are required' });
}
const user = await User.findOne({ username, is_active: true }).select('+password_hash');
if (!user) return res.status(401).json({ error: 'Invalid credentials' });
const isValid = bcrypt.compareSync(password, user.password_hash);
if (!isValid) return res.status(401).json({ error: 'Invalid credentials' });
const token = makeToken(user);
setCookieToken(res, token);
res.json({
message: 'Login successful',
user: {
id: user._id.toString(),
username: user.username,
account_name: user.account_name,
profile_picture: user.profile_picture,
role: user.role,
site_uuid: user.site_uuid,
agent_uuid: user.agent_uuid,
}
});
} catch (err) {
res.status(500).json({ error: err.message });
}
});
// ─── POST /api/auth/renew ─────────────────────────────────────────────────────
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 token = makeToken(user);
setCookieToken(res, token);
const decoded = jwt.verify(token, JWT_SECRET);
res.json({
ok: true,
message: 'Sesi berhasil diperpanjang',
user: {
id: user._id.toString(),
username: user.username,
account_name: user.account_name,
profile_picture: user.profile_picture,
role: user.role,
site_uuid: user.site_uuid,
agent_uuid: user.agent_uuid,
iat: decoded.iat,
exp: decoded.exp,
}
});
} catch (err) {
res.status(500).json({ error: err.message });
}
});
// ─── GET /api/auth/me ─────────────────────────────────────────────────────────
router.get('/me', requireAuth, async (req, res) => {
try {
const user = await User.findById(req.user.id);
if (!user) return res.json({ user: req.user });
const isViewAs = req.user._viewAsMode;
res.json({
user: {
id: user._id.toString(),
username: user.username,
account_name: isViewAs ? req.user.agent_label : user.account_name,
profile_picture: user.profile_picture,
role: isViewAs ? 'AGENT_VIEWER' : user.role,
site_uuid: user.site_uuid,
agent_uuid: isViewAs ? req.user.agent_uuid : user.agent_uuid,
_originalRole: isViewAs ? 'SUPER_ADMIN' : undefined,
iat: req.user.iat,
exp: req.user.exp,
}
});
} catch (err) {
res.status(500).json({ error: err.message });
}
});
// ─── POST /api/auth/logout ────────────────────────────────────────────────────
router.post('/logout', (req, res) => {
res.clearCookie('token');
res.json({ message: 'Logged out successfully' });
});
// ─── GET /api/auth/geoip?ip=x.x.x.x ─────────────────────────────────────────
router.get('/geoip', async (req, res) => {
const ip = req.query.ip;
if (!ip) return res.status(400).json({ error: 'IP is required' });
const parts = ip.split('.');
if (parts.length === 4) {
const [o1, o2] = parts.map(Number);
if (o1 === 10 || (o1 === 192 && o2 === 168) || (o1 === 172 && o2 >= 16 && o2 <= 31) || o1 === 127 || (o1 === 169 && o2 === 254)) {
return res.json({ ip_address: ip, isp: 'Intranet / Private Network', country: 'Local', city: 'Local', as_org: 'RFC 1918 Private Range' });
}
} else if (ip.startsWith('fe80:') || ip === '::1' || ip.startsWith('fd')) {
return res.json({ ip_address: ip, isp: 'Intranet / Private Network', country: 'Local', city: 'Local', as_org: 'IPv6 Link-Local' });
}
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 3000);
const response = await fetch(`http://ip-api.com/json/${ip}`, { signal: controller.signal });
clearTimeout(timeoutId);
const geo = await response.json();
if (geo?.status === 'success') {
return res.json({ ip_address: ip, isp: geo.isp || 'Unknown ISP', country: geo.country || 'Unknown', city: geo.city || 'Unknown', as_org: geo.as || geo.org || 'Unknown' });
}
} catch (e) { /* timeout or network error — fallback */ }
res.json({ ip_address: ip, isp: 'Public IP', country: 'Remote', city: 'Remote', as_org: 'Public Network' });
});
module.exports = router;
+100
View File
@@ -0,0 +1,100 @@
// backend/routes/auth/helpers.js
const jwt = require('jsonwebtoken');
const multer = require('multer');
const path = require('path');
const fs = require('fs');
const JWT_SECRET = process.env.JWT_SECRET || 'super-secret-backone-key';
function makeToken(user) {
return jwt.sign(
{
id: user._id.toString(),
username: user.username,
account_name: user.account_name,
profile_picture: user.profile_picture,
role: user.role,
site_uuid: user.site_uuid,
agent_uuid: user.agent_uuid,
},
JWT_SECRET,
{ expiresIn: '1d' }
);
}
function setCookieToken(res, token) {
res.cookie('token', token, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
maxAge: 24 * 60 * 60 * 1000,
});
}
function requireAuth(req, res, next) {
const token = req.cookies?.token;
if (!token) return res.status(401).json({ error: 'Not authenticated' });
try {
req.user = jwt.verify(token, JWT_SECRET);
// ── VIEW-AS MODE ──────────────────────────────────────────────────────────
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 {
res.status(401).json({ error: 'Token tidak valid' });
}
}
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);
if (decoded.role !== 'SUPER_ADMIN' && decoded.role !== 'TENANT_ADMIN' && decoded.role !== 'SOC_ANALYST') {
return res.status(403).json({ error: 'Role Anda tidak memiliki izin untuk melakukan aksi ini (Hanya Administrator / Analyst)' });
}
req.adminUser = decoded;
next();
} catch {
res.status(401).json({ error: 'Token tidak valid' });
}
}
const storage = multer.diskStorage({
destination: (req, file, cb) => {
const dir = path.join(__dirname, '..', '..', 'uploads');
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
cb(null, dir);
},
filename: (req, file, cb) => {
const uniqueSuffix = `${Date.now()}-${Math.round(Math.random() * 1e9)}`;
cb(null, `profile-${uniqueSuffix}${path.extname(file.originalname)}`);
}
});
const upload = multer({ storage });
module.exports = {
JWT_SECRET,
makeToken,
setCookieToken,
requireAuth,
requireAdmin,
upload
};
+147
View File
@@ -0,0 +1,147 @@
// backend/routes/auth/settings.js
const express = require('express');
const bcrypt = require('bcryptjs');
const path = require('path');
const fs = require('fs');
const User = require('../../models/User');
const { requireAuth, makeToken, setCookieToken, upload } = require('./helpers');
const router = express.Router();
// ─── POST /api/auth/change-password ──────────────────────────────────────────
router.post('/change-password', requireAuth, async (req, res) => {
try {
const { currentPassword, newPassword } = req.body;
if (!currentPassword || !newPassword) {
return res.status(400).json({ error: 'Current password and new password are required' });
}
const user = await User.findById(req.user.id).select('+password_hash');
if (!user) return res.status(404).json({ error: 'User not found' });
if (!bcrypt.compareSync(currentPassword, user.password_hash)) {
return res.status(400).json({ error: 'Password saat ini salah' });
}
const passwordRegex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{6,}$/;
if (!passwordRegex.test(newPassword)) {
return res.status(400).json({ error: 'Password baru tidak memenuhi kriteria: minimal 6 karakter, serta mengandung huruf besar, huruf kecil, dan angka.' });
}
user.password_hash = bcrypt.hashSync(newPassword, 10);
await user.save();
res.json({ ok: true, message: 'Password berhasil diubah!' });
} catch (err) {
res.status(500).json({ error: err.message });
}
});
// ─── POST /api/auth/change-username ──────────────────────────────────────────
router.post('/change-username', requireAuth, async (req, res) => {
try {
const { currentPassword, newUsername } = req.body;
if (!currentPassword || !newUsername) {
return res.status(400).json({ error: 'Current password and new username are required' });
}
if (newUsername.length < 4 || /[^a-zA-Z0-9_]/.test(newUsername)) {
return res.status(400).json({ error: 'Username baru tidak valid (minimal 4 karakter, hanya huruf, angka, dan underscore).' });
}
const user = await User.findById(req.user.id).select('+password_hash');
if (!user) return res.status(404).json({ error: 'User not found' });
if (!bcrypt.compareSync(currentPassword, user.password_hash)) {
return res.status(400).json({ error: 'Password saat ini salah' });
}
const existing = await User.findOne({ username: newUsername });
if (existing) return res.status(400).json({ error: 'Username sudah digunakan oleh akun lain' });
user.username = newUsername;
await user.save();
const newToken = makeToken(user);
setCookieToken(res, newToken);
res.json({ ok: true, message: 'Username berhasil diubah!', newUsername });
} catch (err) {
res.status(500).json({ error: err.message });
}
});
// ─── POST /api/auth/change-account-name ──────────────────────────────────────
router.post('/change-account-name', requireAuth, async (req, res) => {
try {
const { currentPassword, newAccountName } = req.body;
if (!currentPassword || newAccountName == null) {
return res.status(400).json({ error: 'Current password and new account name are required' });
}
if (!newAccountName.trim()) {
return res.status(400).json({ error: 'Nama akun tidak boleh kosong' });
}
const user = await User.findById(req.user.id).select('+password_hash');
if (!user) return res.status(404).json({ error: 'User not found' });
if (!bcrypt.compareSync(currentPassword, user.password_hash)) {
return res.status(400).json({ error: 'Password saat ini salah' });
}
user.account_name = newAccountName.trim();
await user.save();
const newToken = makeToken(user);
setCookieToken(res, newToken);
res.json({ ok: true, message: 'Nama akun berhasil diubah!', newAccountName: user.account_name });
} catch (err) {
res.status(500).json({ error: err.message });
}
});
// ─── POST /api/auth/upload-profile-picture ───────────────────────────────────
router.post('/upload-profile-picture', requireAuth, upload.single('profile_picture'), async (req, res) => {
try {
if (!req.file) return res.status(400).json({ error: 'No image uploaded' });
const user = await User.findById(req.user.id);
if (!user) return res.status(404).json({ error: 'User not found' });
user.profile_picture = req.file.filename;
await user.save();
const newToken = makeToken(user);
setCookieToken(res, newToken);
res.json({ ok: true, message: 'Foto profil berhasil diperbarui', profile_picture: req.file.filename });
} catch (err) {
console.error('[Upload Error]', err);
res.status(500).json({ error: err.message });
}
});
// ─── POST /api/auth/remove-profile-picture ───────────────────────────────────
router.post('/remove-profile-picture', requireAuth, async (req, res) => {
try {
const user = await User.findById(req.user.id);
if (!user) return res.status(404).json({ error: 'User not found' });
if (user.profile_picture) {
const filePath = path.join(__dirname, '..', '..', 'uploads', user.profile_picture);
if (fs.existsSync(filePath)) fs.unlinkSync(filePath);
}
user.profile_picture = null;
await user.save();
const newToken = makeToken(user);
setCookieToken(res, newToken);
res.json({ ok: true, message: 'Foto profil berhasil dihapus' });
} catch (err) {
res.status(500).json({ error: err.message });
}
});
module.exports = router;
+118
View File
@@ -0,0 +1,118 @@
// backend/routes/auth/users.js
const express = require('express');
const bcrypt = require('bcryptjs');
const User = require('../../models/User');
const { requireAdmin, upload } = require('./helpers');
const router = express.Router();
// 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/admin/users — daftar semua users (admin & analyst)
router.get('/admin/users', requireAdmin, async (req, res) => {
try {
const users = await User.find({}, '-password_hash').sort({ created_at: 1 });
const data = users.map(u => ({
id: u._id.toString(),
username: u.username,
account_name: u.account_name,
profile_picture: u.profile_picture,
role: u.role,
site_uuid: u.site_uuid,
agent_uuid: u.agent_uuid,
is_active: u.is_active,
}));
res.json({ ok: true, data });
} 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 {
const { username, password, account_name, agent_uuid } = req.body;
if (!username || !password) {
return res.status(400).json({ ok: false, error: 'Username dan password wajib diisi' });
}
const passwordHash = bcrypt.hashSync(password, 10);
const siteUuid = process.env.BACKONE_SITE_UUID || process.env.NETIFY_SITE_UUID || null;
const newUser = await User.create({
username: username.trim(),
password_hash: passwordHash,
account_name: account_name?.trim() || null,
agent_uuid: agent_uuid?.trim() || null,
role: 'AGENT_VIEWER',
site_uuid: siteUuid,
});
res.json({ ok: true, message: 'Akun Network Agent berhasil dibuat', userId: newUser._id.toString() });
} catch (err) {
const msg = err.code === 11000 ? 'Username sudah digunakan' : err.message;
res.status(400).json({ ok: false, error: msg });
}
});
// POST /api/auth/admin/update-agent-user — update akun Network Agent
router.post('/admin/update-agent-user', requireAdmin, blockAnalyst, upload.single('profile_picture'), async (req, res) => {
try {
const { user_id, username, password, account_name, agent_uuid } = req.body;
if (!user_id) return res.status(400).json({ ok: false, error: 'user_id wajib diisi' });
const target = await User.findById(user_id).select('+password_hash');
if (!target) return res.status(404).json({ ok: false, error: 'User tidak ditemukan' });
if (target.role === 'SUPER_ADMIN') return res.status(403).json({ ok: false, error: 'Tidak bisa mengubah akun SUPER_ADMIN dari sini' });
if (username?.trim()) {
const existing = await User.findOne({ username: username.trim(), _id: { $ne: user_id } });
if (existing) return res.status(400).json({ ok: false, error: 'Username sudah digunakan' });
target.username = username.trim();
}
if (password) target.password_hash = bcrypt.hashSync(password, 10);
if (account_name != null) target.account_name = account_name?.trim() || null;
if (agent_uuid != null) target.agent_uuid = agent_uuid?.trim() || null;
if (req.file) target.profile_picture = req.file.filename;
await target.save();
const updated = await User.findById(user_id, '-password_hash');
res.json({ ok: true, message: 'Akun berhasil diperbarui', user: updated });
} catch (err) {
res.status(500).json({ ok: false, error: err.message });
}
});
// DELETE /api/auth/admin/delete-agent-user/:id — hapus akun Network Agent
router.delete('/admin/delete-agent-user/:id', requireAdmin, blockAnalyst, async (req, res) => {
try {
const target = await User.findById(req.params.id);
if (!target) return res.status(404).json({ ok: false, error: 'User tidak ditemukan' });
if (target.role === 'SUPER_ADMIN') return res.status(403).json({ ok: false, error: 'Tidak bisa menghapus SUPER_ADMIN' });
await User.findByIdAndDelete(req.params.id);
res.json({ ok: true, message: 'Akun berhasil dihapus' });
} catch (err) {
res.status(400).json({ ok: false, error: err.message });
}
});
// POST /api/auth/admin/upload-agent-picture/:id — upload foto profil agent oleh admin
router.post('/admin/upload-agent-picture/:id', requireAdmin, blockAnalyst, upload.single('profile_picture'), async (req, res) => {
try {
if (!req.file) return res.status(400).json({ ok: false, error: 'File gambar wajib diupload' });
const target = await User.findById(req.params.id);
if (!target) return res.status(404).json({ ok: false, error: 'User tidak ditemukan' });
target.profile_picture = req.file.filename;
await target.save();
res.json({ ok: true, message: 'Foto profil berhasil diperbarui', filename: req.file.filename });
} catch (err) {
res.status(500).json({ ok: false, error: err.message });
}
});
module.exports = router;
+113
View File
@@ -0,0 +1,113 @@
// backend/routes/auth/viewAs.js
const express = require('express');
const jwt = require('jsonwebtoken');
const mongoose = require('mongoose');
const { requireAdmin, requireAuth, JWT_SECRET } = require('./helpers');
const router = express.Router();
// Helper to block SOC_ANALYST from starting view-as sessions
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();
}
// POST /api/auth/admin/view-as — admin masuk mode "lihat sebagai agent"
router.post('/admin/view-as', requireAdmin, blockAnalyst, async (req, res) => {
const { agent_uuid, agent_label } = req.body;
if (!agent_uuid) return res.status(400).json({ ok: false, error: 'agent_uuid wajib diisi' });
try {
const viewToken = jwt.sign(
{
adminId: req.adminUser.id,
adminUsername: req.adminUser.username,
viewAs: agent_uuid,
viewAsLabel: agent_label || agent_uuid,
type: 'view-as'
},
JWT_SECRET,
{ expiresIn: '8h' }
);
// Simpan log audit ke MongoDB
const ViewAsLog = mongoose.model('ViewAsLog');
await new ViewAsLog({
admin_id: req.adminUser.id,
admin_username: req.adminUser.username,
admin_role: req.adminUser.role, // Save role!
agent_uuid,
agent_label: agent_label || agent_uuid
}).save();
res.json({
ok: true,
message: `Sekarang melihat sebagai ${agent_label || agent_uuid}`,
view_token: viewToken,
agent_uuid,
agent_label: agent_label || agent_uuid
});
} catch (err) {
res.status(500).json({ ok: false, error: err.message });
}
});
// GET /api/auth/admin/view-as/logs — ambil riwayat audit view-as
router.get('/admin/view-as/logs', requireAdmin, async (req, res) => {
try {
const ViewAsLog = mongoose.model('ViewAsLog');
// Role-based visibility logic:
// If the requesting user is a SOC_ANALYST, filter out logs performed by a SUPER_ADMIN.
const query = {};
if (req.adminUser.role === 'SOC_ANALYST') {
query.admin_role = { $ne: 'SUPER_ADMIN' };
query.admin_username = { $ne: 'admin' };
}
const logs = await ViewAsLog.find(query).sort({ timestamp: -1 }).lean();
res.json({ ok: true, data: logs });
} catch (err) {
res.status(500).json({ ok: false, error: err.message });
}
});
// DELETE /api/auth/admin/view-as — keluar dari mode view-as
router.delete('/admin/view-as', requireAdmin, async (req, res) => {
try {
const ViewAsLog = mongoose.model('ViewAsLog');
const latestLog = await ViewAsLog.findOne({
admin_id: req.adminUser.id,
end_timestamp: { $exists: false }
}).sort({ timestamp: -1 });
if (latestLog) {
latestLog.end_timestamp = new Date();
const diffMs = latestLog.end_timestamp.getTime() - latestLog.timestamp.getTime();
latestLog.duration = Math.round(diffMs / 1000); // durasi dalam detik
await latestLog.save();
}
} catch (err) {
console.error("Gagal menyimpan durasi sesi view-as:", err.message);
}
res.clearCookie('view_as_token');
res.json({ ok: true, message: 'Kembali ke tampilan admin' });
});
// GET /api/auth/view-as — cek status view-as
router.get('/view-as', requireAuth, (req, res) => {
const viewToken = req.cookies?.view_as_token;
if (!viewToken) return res.json({ active: false });
try {
const decoded = jwt.verify(viewToken, JWT_SECRET);
res.json({ active: true, agent_uuid: decoded.viewAs, agent_label: decoded.viewAsLabel });
} catch {
res.clearCookie('view_as_token');
res.json({ active: false });
}
});
module.exports = router;