Aplikasi billing & manajemen ISP (fiber optic) — PWA pelanggan, portal teknisi & kolektor, admin panel lengkap, multi-vendor OLT, RADIUS, WhatsApp/Telegram, pembayaran QRIS. Kredit & lisensi (ISC): - Base kode & repo: alijayanet/billing-rtrw oleh Alijaya Net — terima kasih banyak atas base kode dan repo yang bermanfaat. - Add-on: GoWA, LibreSpeed/OpenSpeedTest, Uptime Kuma, notification-forwarder. - Dikembangkan menjadi OPEN-ISP oleh oxpoe — https://github.com/oxpoe/open-isp
224 lines
6.1 KiB
JavaScript
224 lines
6.1 KiB
JavaScript
/**
|
|
* Service: Audit Trail untuk Operasi Sensitif
|
|
* Mencatat semua operasi sensitif untuk tracking dan security
|
|
*/
|
|
const db = require('../config/database');
|
|
const { logger } = require('../config/logger');
|
|
|
|
// Inisialisasi tabel audit_trail
|
|
function initAuditTrailTable() {
|
|
try {
|
|
db.exec(`
|
|
CREATE TABLE IF NOT EXISTS audit_trail (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
action TEXT NOT NULL,
|
|
entity_type TEXT NOT NULL,
|
|
entity_id TEXT,
|
|
actor_type TEXT NOT NULL,
|
|
actor_id TEXT,
|
|
actor_name TEXT,
|
|
details TEXT,
|
|
ip_address TEXT,
|
|
user_agent TEXT,
|
|
created_at DATETIME DEFAULT (NOW_LOCAL())
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_audit_actor ON audit_trail(actor_type, actor_id);
|
|
CREATE INDEX IF NOT EXISTS idx_audit_entity ON audit_trail(entity_type, entity_id);
|
|
CREATE INDEX IF NOT EXISTS idx_audit_action ON audit_trail(action);
|
|
CREATE INDEX IF NOT EXISTS idx_audit_created_at ON audit_trail(created_at);
|
|
`);
|
|
} catch (e) {
|
|
logger.error(`[Audit Trail] Gagal inisialisasi tabel: ${e.message}`);
|
|
}
|
|
}
|
|
|
|
// Inisialisasi saat module dimuat
|
|
initAuditTrailTable();
|
|
|
|
/**
|
|
* Catat audit trail
|
|
* @param {Object} data - Data audit
|
|
* @param {string} data.action - Tipe aksi (CREATE, UPDATE, DELETE, LOGIN, LOGOUT, dll)
|
|
* @param {string} data.entity_type - Tipe entity (customer, invoice, package, dll)
|
|
* @param {string} data.entity_id - ID entity
|
|
* @param {string} data.actor_type - Tipe actor (admin, cashier, technician, agent, customer, system)
|
|
* @param {string} data.actor_id - ID actor
|
|
* @param {string} data.actor_name - Nama actor
|
|
* @param {Object} data.details - Detail aksi (akan di-JSON.stringify)
|
|
* @param {string} data.ip_address - IP address
|
|
* @param {string} data.user_agent - User agent
|
|
*/
|
|
function logAuditTrail(data) {
|
|
try {
|
|
const stmt = db.prepare(`
|
|
INSERT INTO audit_trail (
|
|
action, entity_type, entity_id, actor_type, actor_id, actor_name,
|
|
details, ip_address, user_agent
|
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
`);
|
|
|
|
stmt.run(
|
|
data.action,
|
|
data.entity_type,
|
|
data.entity_id || null,
|
|
data.actor_type,
|
|
data.actor_id || null,
|
|
data.actor_name || null,
|
|
data.details ? JSON.stringify(data.details) : null,
|
|
data.ip_address || null,
|
|
data.user_agent || null
|
|
);
|
|
|
|
logger.info(`[Audit Trail] ${data.action} ${data.entity_type} by ${data.actor_type} - ${data.actor_name}`);
|
|
} catch (e) {
|
|
logger.error(`[Audit Trail] Gagal mencatat audit: ${e.message}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Ambil audit trail berdasarkan filter
|
|
* @param {Object} filters - Filter pencarian
|
|
* @param {string} filters.action - Filter berdasarkan aksi
|
|
* @param {string} filters.entity_type - Filter berdasarkan tipe entity
|
|
* @param {string} filters.actor_type - Filter berdasarkan tipe actor
|
|
* @param {string} filters.actor_id - Filter berdasarkan ID actor
|
|
* @param {number} filters.limit - Batas hasil
|
|
* @param {number} filters.offset - Offset hasil
|
|
*/
|
|
function getAuditTrail(filters = {}) {
|
|
try {
|
|
let query = 'SELECT * FROM audit_trail WHERE 1=1';
|
|
const params = [];
|
|
|
|
if (filters.action) {
|
|
query += ' AND action = ?';
|
|
params.push(filters.action);
|
|
}
|
|
|
|
if (filters.entity_type) {
|
|
query += ' AND entity_type = ?';
|
|
params.push(filters.entity_type);
|
|
}
|
|
|
|
if (filters.actor_type) {
|
|
query += ' AND actor_type = ?';
|
|
params.push(filters.actor_type);
|
|
}
|
|
|
|
if (filters.actor_id) {
|
|
query += ' AND actor_id = ?';
|
|
params.push(filters.actor_id);
|
|
}
|
|
|
|
query += ' ORDER BY created_at DESC';
|
|
|
|
if (filters.limit) {
|
|
query += ' LIMIT ?';
|
|
params.push(filters.limit);
|
|
}
|
|
|
|
if (filters.offset) {
|
|
query += ' OFFSET ?';
|
|
params.push(filters.offset);
|
|
}
|
|
|
|
const stmt = db.prepare(query);
|
|
const results = stmt.all(...params);
|
|
|
|
// Parse details JSON
|
|
return results.map(row => ({
|
|
...row,
|
|
details: row.details ? JSON.parse(row.details) : null
|
|
}));
|
|
} catch (e) {
|
|
logger.error(`[Audit Trail] Gagal mengambil audit: ${e.message}`);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Ambil statistik audit trail
|
|
*/
|
|
function getAuditStats() {
|
|
try {
|
|
const stats = {
|
|
total: 0,
|
|
by_action: {},
|
|
by_entity_type: {},
|
|
by_actor_type: {},
|
|
recent_24h: 0
|
|
};
|
|
|
|
// Total audit
|
|
const totalStmt = db.prepare('SELECT COUNT(*) as count FROM audit_trail');
|
|
stats.total = totalStmt.get().count;
|
|
|
|
// By action
|
|
const actionStmt = db.prepare(`
|
|
SELECT action, COUNT(*) as count
|
|
FROM audit_trail
|
|
GROUP BY action
|
|
ORDER BY count DESC
|
|
`);
|
|
stats.by_action = actionStmt.all();
|
|
|
|
// By entity type
|
|
const entityStmt = db.prepare(`
|
|
SELECT entity_type, COUNT(*) as count
|
|
FROM audit_trail
|
|
GROUP BY entity_type
|
|
ORDER BY count DESC
|
|
`);
|
|
stats.by_entity_type = entityStmt.all();
|
|
|
|
// By actor type
|
|
const actorStmt = db.prepare(`
|
|
SELECT actor_type, COUNT(*) as count
|
|
FROM audit_trail
|
|
GROUP BY actor_type
|
|
ORDER BY count DESC
|
|
`);
|
|
stats.by_actor_type = actorStmt.all();
|
|
|
|
// Recent 24h
|
|
const recentStmt = db.prepare(`
|
|
SELECT COUNT(*) as count
|
|
FROM audit_trail
|
|
WHERE created_at >= datetime(NOW_LOCAL(), '-24 hours')
|
|
`);
|
|
stats.recent_24h = recentStmt.get().count;
|
|
|
|
return stats;
|
|
} catch (e) {
|
|
logger.error(`[Audit Trail] Gagal mengambil statistik: ${e.message}`);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Hapus audit trail lama (retention policy)
|
|
* @param {number} days - Jumlah hari untuk dipertahankan (default 90 hari)
|
|
*/
|
|
function cleanupOldAuditTrail(days = 90) {
|
|
try {
|
|
const stmt = db.prepare(`
|
|
DELETE FROM audit_trail
|
|
WHERE created_at < datetime('now', '-${days} days')
|
|
`);
|
|
const result = stmt.run();
|
|
logger.info(`[Audit Trail] Cleaned up ${result.changes} old records (older than ${days} days)`);
|
|
return result.changes;
|
|
} catch (e) {
|
|
logger.error(`[Audit Trail] Gagal cleanup: ${e.message}`);
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
module.exports = {
|
|
logAuditTrail,
|
|
getAuditTrail,
|
|
getAuditStats,
|
|
cleanupOldAuditTrail
|
|
};
|