Files
pulse-signage/src/data/audit-log.js
T
lzstealth 3f4f57020a
Publish Docker Image / build-and-push-existing-registry (./build/Dockerfile, web, pulse-signage-web) (push) Successful in 1m13s
Publish Docker Image / build-and-push-existing-registry (./build/Dockerfile.player, player, pulse-signage-player) (push) Successful in 31s
Release v2.10.3
2026-08-29 01:56:45 +01:00

120 lines
4.1 KiB
JavaScript

// Audit event definitions and data access helpers for administrative activity.
const AUDIT_EVENT_CATEGORIES = Object.freeze({
AUTHENTICATION: 'authentication',
SECURITY: 'security',
SESSIONS: 'sessions',
USERS: 'users',
ROLES: 'roles',
SETTINGS: 'system-settings',
SLIDES: 'slides',
TEMPLATES: 'templates',
PLAYLISTS: 'playlists',
SCREENS: 'screens',
ANNOUNCEMENTS: 'announcements',
CANVAS_SIZES: 'canvas-sizes',
API_SOURCES: 'api-sources',
RSS_FEEDS: 'rss-feeds',
TIMETABLES: 'timetables'
});
const AUDIT_CATEGORY_KEYS = Object.freeze(Object.values(AUDIT_EVENT_CATEGORIES));
const AUDIT_CATEGORY_LABELS = Object.freeze({
authentication: 'Authentication',
security: 'Security',
sessions: 'Sessions',
users: 'Users',
roles: 'Roles',
'system-settings': 'System Settings',
slides: 'Slides',
templates: 'Templates',
playlists: 'Playlists',
screens: 'Screens',
announcements: 'Announcements',
'canvas-sizes': 'Canvas Sizes',
'api-sources': 'API Sources',
'rss-feeds': 'RSS Feeds',
timetables: 'Timetables'
});
const { fetchAppSettings } = require('./app-settings');
function normalizeDetails(details) {
if (details === undefined || details === null) {
return null;
}
return JSON.stringify(details);
}
function buildAuditChanges(previousValues, nextValues) {
const previous = previousValues && typeof previousValues === 'object' ? previousValues : {};
const next = nextValues && typeof nextValues === 'object' ? nextValues : {};
const changes = {};
const keys = new Set(Object.keys(previous).concat(Object.keys(next)));
keys.forEach(function (key) {
if (JSON.stringify(previous[key]) !== JSON.stringify(next[key])) {
changes[key] = { from: previous[key], to: next[key] };
}
});
return changes;
}
function getRequestMetadata(req) {
const forwardedAddress = String(req && req.headers && req.headers['x-forwarded-for'] || '').split(',')[0].trim();
return {
ipAddress: forwardedAddress || String(req && req.ip || req && req.socket && req.socket.remoteAddress || '').trim() || null,
userAgent: String(req && req.headers && req.headers['user-agent'] || '').trim() || null
};
}
async function recordAuditEvent(pool, event) {
const input = event && typeof event === 'object' ? event : {};
const category = String(input.category || '').trim().toLowerCase();
const eventType = String(input.eventType || '').trim().toLowerCase();
if (!category || !eventType) {
throw new Error('Audit events require a category and event type.');
}
await pool.query(
`INSERT INTO o_audit_events
(category, event_type, actor_user_id, target_type, target_id, target_label, ip_address, user_agent, details_json)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[
category,
eventType,
Number.isInteger(Number(input.actorUserId)) && Number(input.actorUserId) > 0 ? Number(input.actorUserId) : null,
String(input.targetType || '').trim() || null,
String(input.targetId || '').trim() || null,
String(input.targetLabel || '').trim() || null,
String(input.ipAddress || '').trim() || null,
String(input.userAgent || '').trim() || null,
normalizeDetails(input.details)
]
);
}
async function recordRequestAuditEvent(pool, req, event) {
try {
const settings = await fetchAppSettings(pool);
const category = String(event && event.category || '').trim().toLowerCase();
const enabledCategories = Array.isArray(settings['audit.categories']) ? settings['audit.categories'] : AUDIT_CATEGORY_KEYS;
if (!settings['audit.enabled'] || !enabledCategories.includes(category)) {
return;
}
const metadata = settings['audit.include_request_metadata'] ? getRequestMetadata(req) : {};
await recordAuditEvent(pool, Object.assign({}, event, metadata));
} catch (error) {
// Auditing must not turn a successful login or administration action into a failed request.
console.error('Unable to record audit event:', error.message);
}
}
module.exports = {
AUDIT_EVENT_CATEGORIES,
AUDIT_CATEGORY_KEYS,
AUDIT_CATEGORY_LABELS,
getRequestMetadata,
buildAuditChanges,
recordAuditEvent,
recordRequestAuditEvent
};