Files
open-isp/services/auditTrailService.js
oxpoe eaf05f2811 OPEN-ISP v0.5 (pickle-lime) — rilis open-source perdana
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
2026-10-09 05:51:21 +07:00

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
};