Release v2.17.5
Publish Docker Image / build-and-push-existing-registry (./build/Dockerfile, web, pulse-signage-web) (push) Successful in 1m11s
Publish Docker Image / build-and-push-existing-registry (./build/Dockerfile.player, player, pulse-signage-player) (push) Successful in 33s

This commit is contained in:
lzstealth committed 2026-10-23 21:23:43 +01:00
1 parent 46636939de
commit 86b9609ea7
11 files changed
+285 -56

No files matched your search

+102 -37
View File
@@ -1,10 +1,29 @@
const { renderView } = require('#src/web/view');
const { hasPermission, PERMISSION_DENIED_MESSAGE } = require('#src/rbac');
const { AUDIT_CATEGORY_KEYS, formatUserAgentLabel } = require('#src/data/audit-log');
const { buildSearchFilter } = require('#src/data/utils');
const { buildPagination } = require('#src/web/lib/pagination');
const { createSearchMatcher, getSearchQuery, getSortDirectionQuery, getSortQuery, parsePageNumber, sortRows } = require('#src/web/lib/list-query');
const { getSearchQuery, getSortDirectionQuery, getSortQuery, parsePageNumber } = require('#src/web/lib/list-query');
const PAGE_SIZE = 25;
const AUDIT_FROM_SQL = `FROM o_audit_events e
LEFT JOIN a_users u ON u.id = e.actor_user_id`;
const AUDIT_TARGET_LABEL_SQL = "CASE WHEN COALESCE(e.target_label, '') <> '' THEN e.target_label WHEN e.target_type IS NOT NULL AND e.target_id IS NOT NULL THEN CONCAT(e.target_type, ' #', e.target_id) ELSE '' END";
const AUDIT_SEARCH_COLUMNS = [
'e.category',
'e.event_type',
"COALESCE(NULLIF(u.name, ''), u.username, 'System')",
AUDIT_TARGET_LABEL_SQL,
'e.ip_address',
'e.user_agent'
];
const AUDIT_SORT_COLUMNS = {
time: 'e.occurred_at',
event: "REPLACE(REPLACE(REPLACE(e.event_type, '.', ' '), '_', ' '), '-', ' ')",
actor: "COALESCE(NULLIF(u.name, ''), u.username, 'System')",
target: AUDIT_TARGET_LABEL_SQL,
category: 'e.category'
};
const SORTED_AUDIT_CATEGORY_KEYS = AUDIT_CATEGORY_KEYS.slice().sort(function (left, right) {
return String(left).localeCompare(String(right));
@@ -168,35 +187,78 @@ function mapAuditRow(row, formatDashboardDate) {
});
}
function filterAuditRows(rows, req, formatDashboardDate) {
function buildAuditFilters(req) {
const category = String(req.query.category || '').trim().toLowerCase();
const eventType = String(req.query.event_type || '').trim().toLowerCase();
const search = getSearchQuery(req);
const requestedSort = getSortQuery(req);
const sort = requestedSort || 'time';
const sort = Object.prototype.hasOwnProperty.call(AUDIT_SORT_COLUMNS, requestedSort) ? requestedSort : 'time';
const direction = requestedSort ? getSortDirectionQuery(req) : 'desc';
const mappedRows = (rows || []).map(function (row) { return mapAuditRow(row, formatDashboardDate); });
const filteredRows = sortRows(mappedRows.filter(function (event) {
return (!category || event.category === category) && (!eventType || event.event_type === eventType);
}).filter(createSearchMatcher(search, ['category', 'event_type', 'actorLabel', 'targetLabelDisplay', 'ip_address', 'user_agent'])), function (event) {
if (sort === 'event') return event.eventLabel;
if (sort === 'actor') return event.actorLabel;
if (sort === 'target') return event.targetLabelDisplay;
if (sort === 'category') return event.category;
return event.occurred_at;
}, direction);
return { rows: filteredRows, category: category, eventType: eventType, search: search, sort: sort, direction: direction };
const whereClauses = [];
const params = [];
if (category) {
whereClauses.push('e.category = ?');
params.push(category);
}
if (eventType) {
whereClauses.push('e.event_type = ?');
params.push(eventType);
}
const searchFilter = buildSearchFilter(AUDIT_SEARCH_COLUMNS, search);
if (searchFilter.clause) {
whereClauses.push(searchFilter.clause.replace(/^\s*WHERE\s+/i, ''));
params.push(...searchFilter.params);
}
return {
category: category,
eventType: eventType,
search: search,
sort: sort,
direction: direction,
whereSql: whereClauses.length ? ' WHERE ' + whereClauses.join(' AND ') : '',
params: params
};
}
async function fetchAuditRows(pool) {
function buildAuditOrderBy(filters) {
const sortExpression = AUDIT_SORT_COLUMNS[filters.sort] || AUDIT_SORT_COLUMNS.time;
const tieBreakers = filters.sort === 'time' ? 'e.id DESC' : 'e.occurred_at DESC, e.id DESC';
return ' ORDER BY ' + sortExpression + ' ' + filters.direction.toUpperCase() + ', ' + tieBreakers;
}
async function fetchAuditCount(pool, filters) {
const [rows] = await pool.query(
`SELECT e.id, e.occurred_at, e.category, e.event_type, e.target_type, e.target_id,
'SELECT COUNT(*) AS count ' + AUDIT_FROM_SQL + filters.whereSql,
filters.params
);
return Number(rows && rows[0] && rows[0].count) || 0;
}
async function fetchAuditEventTypes(pool, filters) {
const [rows] = await pool.query(
'SELECT DISTINCT e.event_type AS event_type ' + AUDIT_FROM_SQL + filters.whereSql + (filters.whereSql ? ' AND ' : ' WHERE ') + "e.event_type IS NOT NULL AND e.event_type <> '' ORDER BY e.event_type ASC",
filters.params
);
return (rows || []).map(function (row) {
return String(row.event_type || '').trim();
}).filter(Boolean);
}
async function fetchAuditRows(pool, filters, page) {
let sql = `SELECT e.id, e.occurred_at, e.category, e.event_type, e.target_type, e.target_id,
e.target_label, e.ip_address, e.user_agent, e.details_json,
u.name AS actor_name, u.username AS actor_username
FROM o_audit_events e
LEFT JOIN a_users u ON u.id = e.actor_user_id
ORDER BY e.occurred_at DESC, e.id DESC`, []
);
${AUDIT_FROM_SQL}${filters.whereSql}${buildAuditOrderBy(filters)}`;
const params = filters.params.slice();
if (page) {
sql += ' LIMIT ? OFFSET ?';
params.push(page.limit, page.offset);
}
const [rows] = await pool.query(sql, params);
return rows || [];
}
@@ -214,27 +276,28 @@ module.exports = function registerAuditLogRoutes(app, deps) {
const search = getSearchQuery(req);
const sort = getSortQuery(req);
const direction = getSortDirectionQuery(req);
const filtered = filterAuditRows(await fetchAuditRows(pool), req, formatDashboardDate);
const filteredEvents = filtered.rows;
const eventTypes = Array.from(new Set(filtered.rows.map(function (event) {
return String(event.event_type || '').trim();
}).filter(Boolean))).sort();
const exportQuery = new URLSearchParams({
category: filtered.category,
event_type: filtered.eventType,
search: filtered.search,
sort: filtered.sort,
direction: filtered.direction
}).toString();
const pagination = buildPagination(filteredEvents.length, parsePageNumber(req.query.page), 'page', {
const filters = buildAuditFilters(req);
const totalEvents = await fetchAuditCount(pool, filters);
const pagination = buildPagination(totalEvents, parsePageNumber(req.query.page), 'page', {
search: search,
sort: sort,
direction: direction,
category: category,
event_type: eventType
}, PAGE_SIZE, 'events', 'Audit event pages');
const startIndex = (pagination.currentPage - 1) * PAGE_SIZE;
const events = filteredEvents.slice(startIndex, startIndex + PAGE_SIZE);
const rawEvents = await fetchAuditRows(pool, filters, {
limit: PAGE_SIZE,
offset: (pagination.currentPage - 1) * PAGE_SIZE
});
const events = rawEvents.map(function (row) { return mapAuditRow(row, formatDashboardDate); });
const eventTypes = await fetchAuditEventTypes(pool, filters);
const exportQuery = new URLSearchParams({
category: filters.category,
event_type: filters.eventType,
search: filters.search,
sort: filters.sort,
direction: filters.direction
}).toString();
res.send(renderView('settings/audit-log/index', {
title: 'Audit log',
active: 'audit-log',
@@ -257,11 +320,13 @@ module.exports = function registerAuditLogRoutes(app, deps) {
app.get('/settings/audit-log/export', requireAuditExportAccess(deps.setAuthMessageCookie), async function (req, res, next) {
try {
const filtered = filterAuditRows(await fetchAuditRows(pool), req, formatDashboardDate);
const filters = buildAuditFilters(req);
const rawEvents = await fetchAuditRows(pool, filters);
const events = rawEvents.map(function (row) { return mapAuditRow(row, formatDashboardDate); });
const lines = [
['Occurred At (UTC)', 'Category', 'Event Type', 'Actor', 'Target Type', 'Target ID', 'Target', 'IP Address', 'User Agent', 'Details'].map(csvCell).join(',')
];
filtered.rows.forEach(function (event) {
events.forEach(function (event) {
lines.push([
event.occurredAtValue,
event.category,