Password Policy/App rename/PM Scheduling Improvements

This commit is contained in:
2026-06-06 02:32:47 -05:00
parent dfd999b6a9
commit f024286b5e
59 changed files with 3814 additions and 299 deletions

View File

@@ -0,0 +1,92 @@
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 };