93 lines
3.5 KiB
JavaScript
93 lines
3.5 KiB
JavaScript
const { all, one } = require('../db');
|
|
|
|
// Read-only access to the system-wide audit trail captured by db.audit(). Powers the admin
|
|
// Activity & Audit Trail viewer: filtered + paginated queries, filter facets, and CSV export.
|
|
|
|
const MAX_PAGE_SIZE = 200;
|
|
|
|
// Build a parameterized WHERE clause from the supported filters.
|
|
function buildWhere(filters = {}) {
|
|
const clauses = [];
|
|
const params = [];
|
|
if (filters.actor) { clauses.push('al.actor_id = ?'); params.push(Number(filters.actor)); }
|
|
if (filters.action) { clauses.push('al.action = ?'); params.push(filters.action); }
|
|
if (filters.entity_type) { clauses.push('al.entity_type = ?'); params.push(filters.entity_type); }
|
|
if (filters.from) { clauses.push('al.created_at >= ?'); params.push(String(filters.from)); }
|
|
if (filters.to) { clauses.push('al.created_at <= ?'); params.push(`${String(filters.to)}`); }
|
|
if (filters.q) {
|
|
clauses.push('(al.entity_type LIKE ? OR al.entity_id LIKE ? OR al.action LIKE ? OR u.name LIKE ?)');
|
|
const like = `%${filters.q}%`;
|
|
params.push(like, like, like, like);
|
|
}
|
|
return { where: clauses.length ? `WHERE ${clauses.join(' AND ')}` : '', params };
|
|
}
|
|
|
|
const BASE = `
|
|
FROM audit_logs al
|
|
LEFT JOIN users u ON u.id = al.actor_id
|
|
`;
|
|
|
|
// Paginated query. Returns { rows, total, page, pageSize }.
|
|
function query(filters = {}, { page = 1, pageSize = 25 } = {}) {
|
|
const size = Math.min(MAX_PAGE_SIZE, Math.max(1, Number(pageSize) || 25));
|
|
const current = Math.max(1, Number(page) || 1);
|
|
const { where, params } = buildWhere(filters);
|
|
const total = one(`SELECT COUNT(*) AS c ${BASE} ${where}`, params).c;
|
|
const rows = all(
|
|
`SELECT al.id, al.actor_id, u.name AS actor_name, al.action, al.entity_type, al.entity_id,
|
|
al.before_json, al.after_json, al.created_at
|
|
${BASE} ${where}
|
|
ORDER BY al.id DESC
|
|
LIMIT ? OFFSET ?`,
|
|
[...params, size, (current - 1) * size]
|
|
);
|
|
return { rows, total, page: current, pageSize: size };
|
|
}
|
|
|
|
// All matching rows (no pagination) — used by CSV export. Capped to avoid unbounded memory.
|
|
function queryAll(filters = {}, limit = 50000) {
|
|
const { where, params } = buildWhere(filters);
|
|
return all(
|
|
`SELECT al.id, al.actor_id, u.name AS actor_name, al.action, al.entity_type, al.entity_id,
|
|
al.before_json, al.after_json, al.created_at
|
|
${BASE} ${where}
|
|
ORDER BY al.id DESC
|
|
LIMIT ?`,
|
|
[...params, limit]
|
|
);
|
|
}
|
|
|
|
// Distinct values for the filter dropdowns.
|
|
function facets() {
|
|
return {
|
|
actions: all('SELECT DISTINCT action FROM audit_logs ORDER BY action').map((r) => r.action),
|
|
entity_types: all('SELECT DISTINCT entity_type FROM audit_logs ORDER BY entity_type').map((r) => r.entity_type),
|
|
actors: all(
|
|
`SELECT DISTINCT al.actor_id AS id, u.name AS name
|
|
FROM audit_logs al LEFT JOIN users u ON u.id = al.actor_id
|
|
WHERE al.actor_id IS NOT NULL
|
|
ORDER BY u.name`
|
|
)
|
|
};
|
|
}
|
|
|
|
function csvCell(value) {
|
|
const s = value === null || value === undefined ? '' : String(value);
|
|
return /[",\n]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s;
|
|
}
|
|
|
|
// Serialize rows to CSV text.
|
|
function toCsv(rows) {
|
|
const header = ['id', 'timestamp', 'actor', 'action', 'entity_type', 'entity_id', 'before', 'after'];
|
|
const lines = [header.join(',')];
|
|
for (const r of rows) {
|
|
lines.push([
|
|
r.id, r.created_at, r.actor_name || `#${r.actor_id || ''}`, r.action,
|
|
r.entity_type, r.entity_id, r.before_json, r.after_json
|
|
].map(csvCell).join(','));
|
|
}
|
|
return lines.join('\n');
|
|
}
|
|
|
|
module.exports = { facets, query, queryAll, toCsv };
|