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
This commit is contained in:
commit
eaf05f2811
280 files changed
+123975
No files matched your search
@@ -0,0 +1,223 @@
|
||||
/**
|
||||
* 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
|
||||
};
|
||||
Reference in new issue
Block a user