Release 2.8.0
This commit is contained in:
@@ -1,55 +1,69 @@
|
||||
const ANNOUNCEMENT_ICON_OPTIONS = [
|
||||
{ value: 'megaphone-fill', label: 'Megaphone' },
|
||||
{ value: 'megaphone', label: 'Megaphone outline' },
|
||||
{ value: 'bell-fill', label: 'Bell' },
|
||||
{ value: 'bell', label: 'Bell outline' },
|
||||
{ value: 'exclamation-triangle-fill', label: 'Warning' },
|
||||
{ value: 'exclamation-triangle', label: 'Warning outline' },
|
||||
{ value: 'info-circle-fill', label: 'Info' },
|
||||
{ value: 'info-circle', label: 'Info outline' },
|
||||
{ value: 'check-circle-fill', label: 'Success' },
|
||||
{ value: 'check-circle', label: 'Success outline' },
|
||||
{ value: 'lightbulb-fill', label: 'Idea' },
|
||||
{ value: 'lightbulb', label: 'Idea outline' },
|
||||
{ value: 'calendar-event-fill', label: 'Calendar' },
|
||||
{ value: 'calendar-event', label: 'Calendar outline' },
|
||||
{ value: 'clock-fill', label: 'Clock' },
|
||||
{ value: 'clock', label: 'Clock outline' },
|
||||
{ value: 'wifi-off', label: 'Wi-Fi Offline' },
|
||||
{ value: 'wifi', label: 'Wi-Fi' },
|
||||
{ value: 'hdd-network', label: 'Network' },
|
||||
{ value: 'hdd-network-fill', label: 'Network fill' },
|
||||
{ value: 'speaker-fill', label: 'Speaker' },
|
||||
{ value: 'speaker', label: 'Speaker outline' },
|
||||
{ value: 'shield-fill', label: 'Shield' },
|
||||
{ value: 'shield', label: 'Shield outline' },
|
||||
{ value: 'collection-play-fill', label: 'Playlist' },
|
||||
{ value: 'collection-play', label: 'Playlist outline' },
|
||||
{ value: 'broadcast', label: 'Broadcast' },
|
||||
{ value: 'broadcast-pin', label: 'Broadcast pin' },
|
||||
{ value: 'plug-fill', label: 'Plug' },
|
||||
{ value: 'plug', label: 'Plug outline' },
|
||||
{ value: 'lightning-charge-fill', label: 'Urgent' },
|
||||
{ value: 'lightning-charge', label: 'Urgent outline' },
|
||||
{ value: 'car-front-fill', label: 'Car Front' },
|
||||
{ value: 'car-front', label: 'Car Front outline' },
|
||||
{ value: 'lamp-fill', label: 'Lamp' },
|
||||
{ value: 'lamp', label: 'Lamp outline' },
|
||||
{ value: 'envelope-fill', label: 'Message' },
|
||||
{ value: 'envelope', label: 'Message outline' },
|
||||
{ value: 'people-fill', label: 'Audience' },
|
||||
{ value: 'people', label: 'Audience outline' },
|
||||
{ value: 'browser-chrome', label: 'Browser Chrome' },
|
||||
{ value: 'browser-edge', label: 'Browser Edge' },
|
||||
{ value: 'browser-firefox', label: 'Browser Firefox' },
|
||||
{ value: 'browser-safari', label: 'Browser Safari' },
|
||||
{ value: 'cone', label: 'Cone' },
|
||||
{ value: 'cone-striped', label: 'Cone striped' },
|
||||
{ value: 'cup-straw', label: 'Cup straw' },
|
||||
{ value: 'fire', label: 'Fire' }
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const BOOTSTRAP_ICON_CSS_PATH = path.join(__dirname, '..', 'web', 'public', 'adminlte', 'bootstrap-icons', 'css', 'bootstrap-icons.min.css');
|
||||
|
||||
const DEFAULT_ANNOUNCEMENT_ICON_KEYS = [
|
||||
'megaphone-fill', 'megaphone', 'bell-fill', 'bell',
|
||||
'exclamation-triangle-fill', 'exclamation-triangle', 'info-circle-fill', 'info-circle',
|
||||
'check-circle-fill', 'check-circle', 'lightbulb-fill', 'lightbulb',
|
||||
'calendar-event-fill', 'calendar-event', 'clock-fill', 'clock',
|
||||
'wifi-off', 'wifi', 'hdd-network', 'hdd-network-fill',
|
||||
'speaker-fill', 'speaker', 'shield-fill', 'shield',
|
||||
'collection-play-fill', 'collection-play', 'broadcast', 'broadcast-pin',
|
||||
'plug-fill', 'plug', 'lightning-charge-fill', 'lightning-charge',
|
||||
'car-front-fill', 'car-front', 'lamp-fill', 'lamp',
|
||||
'envelope-fill', 'envelope', 'people-fill', 'people',
|
||||
'browser-chrome', 'browser-edge', 'browser-firefox', 'browser-safari',
|
||||
'cone', 'cone-striped', 'cup-straw', 'fire'
|
||||
];
|
||||
|
||||
const ANNOUNCEMENT_ICON_KEYS = ANNOUNCEMENT_ICON_OPTIONS.map(function (option) {
|
||||
function humanizeBootstrapIconLabel(iconKey) {
|
||||
return String(iconKey || '')
|
||||
.trim()
|
||||
.replace(/[-_]+/g, ' ')
|
||||
.replace(/\b\w/g, function (character) {
|
||||
return character.toUpperCase();
|
||||
});
|
||||
}
|
||||
|
||||
function loadBootstrapIconCatalog() {
|
||||
try {
|
||||
const css = fs.readFileSync(BOOTSTRAP_ICON_CSS_PATH, 'utf8');
|
||||
const keys = Array.from(new Set((css.match(/\.bi-([a-z0-9-]+)::?before/g) || []).map(function (match) {
|
||||
return String(match || '')
|
||||
.replace(/^\.bi-/, '')
|
||||
.replace(/::?before$/, '')
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
}).filter(Boolean)));
|
||||
|
||||
return keys.map(function (value) {
|
||||
return {
|
||||
value: value,
|
||||
label: humanizeBootstrapIconLabel(value)
|
||||
};
|
||||
}).sort(function (left, right) {
|
||||
return left.value.localeCompare(right.value);
|
||||
});
|
||||
} catch (_error) {
|
||||
return DEFAULT_ANNOUNCEMENT_ICON_KEYS.map(function (value) {
|
||||
return { value: value, label: humanizeBootstrapIconLabel(value) };
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const ANNOUNCEMENT_ICON_CATALOG = loadBootstrapIconCatalog();
|
||||
const ANNOUNCEMENT_ICON_OPTIONS = DEFAULT_ANNOUNCEMENT_ICON_KEYS.map(function (value) {
|
||||
const catalogOption = ANNOUNCEMENT_ICON_CATALOG.find(function (option) {
|
||||
return option.value === value;
|
||||
});
|
||||
return catalogOption || { value: value, label: humanizeBootstrapIconLabel(value) };
|
||||
});
|
||||
|
||||
const ANNOUNCEMENT_ICON_KEYS = DEFAULT_ANNOUNCEMENT_ICON_KEYS.slice();
|
||||
|
||||
const ANNOUNCEMENT_ICON_CATALOG_KEYS = ANNOUNCEMENT_ICON_CATALOG.map(function (option) {
|
||||
return option.value;
|
||||
});
|
||||
|
||||
@@ -58,17 +72,27 @@ const ANNOUNCEMENT_ICON_LABELS = ANNOUNCEMENT_ICON_OPTIONS.reduce(function (labe
|
||||
return labels;
|
||||
}, Object.create(null));
|
||||
|
||||
ANNOUNCEMENT_ICON_CATALOG.forEach(function (option) {
|
||||
if (!Object.prototype.hasOwnProperty.call(ANNOUNCEMENT_ICON_LABELS, option.value)) {
|
||||
ANNOUNCEMENT_ICON_LABELS[option.value] = option.label;
|
||||
}
|
||||
});
|
||||
|
||||
const DEFAULT_ANNOUNCEMENT_ICON = 'megaphone-fill';
|
||||
|
||||
function normalizeAnnouncementIcon(value) {
|
||||
const normalized = String(value || '').trim().toLowerCase();
|
||||
return ANNOUNCEMENT_ICON_KEYS.includes(normalized) ? normalized : DEFAULT_ANNOUNCEMENT_ICON;
|
||||
return ANNOUNCEMENT_ICON_CATALOG_KEYS.includes(normalized) ? normalized : DEFAULT_ANNOUNCEMENT_ICON;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
DEFAULT_ANNOUNCEMENT_ICON_KEYS,
|
||||
ANNOUNCEMENT_ICON_OPTIONS,
|
||||
ANNOUNCEMENT_ICON_KEYS,
|
||||
ANNOUNCEMENT_ICON_CATALOG,
|
||||
ANNOUNCEMENT_ICON_CATALOG_KEYS,
|
||||
ANNOUNCEMENT_ICON_LABELS,
|
||||
DEFAULT_ANNOUNCEMENT_ICON,
|
||||
humanizeBootstrapIconLabel,
|
||||
normalizeAnnouncementIcon
|
||||
};
|
||||
@@ -22,10 +22,6 @@ function normalizeAuthMethod(value) {
|
||||
return ['basic', 'bearer', 'api_key_header'].includes(method) ? method : 'none';
|
||||
}
|
||||
|
||||
function getItemsPath(source) {
|
||||
return String(source && (source.items_path || source.itemsPath) || '').trim();
|
||||
}
|
||||
|
||||
function buildAuthHeaders(source) {
|
||||
const method = normalizeAuthMethod(source && (source.auth_method || source.authMethod));
|
||||
const headers = {};
|
||||
|
||||
@@ -0,0 +1,199 @@
|
||||
const { DEFAULT_ANNOUNCEMENT_ICON_KEYS } = require('./announcement-icons');
|
||||
|
||||
const SETTING_DEFINITIONS = [
|
||||
{ key: 'app.name', type: 'string', defaultValue: 'Pulse Signage' },
|
||||
{ key: 'locale.timezone', type: 'string', defaultValue: 'Europe/London' },
|
||||
{ key: 'locale.language', type: 'string', defaultValue: 'en' },
|
||||
{ key: 'ui.theme', type: 'enum', values: ['dark', 'light', 'auto'], defaultValue: 'dark' },
|
||||
{ key: 'security.session_lifetime_days', type: 'integer', min: 1, defaultValue: 14 },
|
||||
{ key: 'security.allow_user_session_revocation', type: 'boolean', defaultValue: true },
|
||||
{ key: 'security.max_active_sessions', type: 'integer', min: 0, defaultValue: 0 },
|
||||
{ key: 'security.password_min_length', type: 'integer', min: 8, defaultValue: 10 },
|
||||
{ key: 'security.password_min_categories', type: 'integer', min: 1, defaultValue: 3 },
|
||||
{ key: 'security.password_require_lowercase', type: 'boolean', defaultValue: false },
|
||||
{ key: 'security.password_require_uppercase', type: 'boolean', defaultValue: false },
|
||||
{ key: 'security.password_require_number', type: 'boolean', defaultValue: false },
|
||||
{ key: 'security.password_require_symbol', type: 'boolean', defaultValue: false },
|
||||
{ key: 'security.require_password_change_for_new_users', type: 'boolean', defaultValue: true },
|
||||
{ key: 'security.require_password_change_after_admin_reset', type: 'boolean', defaultValue: true },
|
||||
{ key: 'security.login_max_attempts', type: 'integer', min: 1, defaultValue: 5 },
|
||||
{ key: 'security.login_lockout_minutes', type: 'integer', min: 1, defaultValue: 15 },
|
||||
{ key: 'security.login_rate_limit_scope', type: 'enum', values: ['both', 'username', 'ip'], defaultValue: 'both' },
|
||||
{ key: 'audit.enabled', type: 'boolean', defaultValue: true },
|
||||
{ key: 'audit.categories', type: 'string_array', defaultValue: ['authentication', 'security', 'sessions', 'users', 'roles', 'system-settings'] },
|
||||
{ key: 'audit.include_request_metadata', type: 'boolean', defaultValue: true },
|
||||
{ key: 'audit.retention_days', type: 'integer', min: 0, defaultValue: 180 },
|
||||
{ key: 'uploads.image_max_bytes', type: 'integer', min: 1, defaultValue: 100 * 1024 * 1024 },
|
||||
{ key: 'uploads.video_max_bytes', type: 'integer', min: 1, defaultValue: 1024 * 1024 * 1024 },
|
||||
{ key: 'uploads.wysiwyg_image_max_bytes', type: 'integer', min: 1, defaultValue: 2 * 1024 * 1024 },
|
||||
{ key: 'uploads.allowed_mime_types', type: 'string_array', defaultValue: ['image/png', 'image/jpeg', 'image/gif', 'image/webp', 'image/svg+xml', 'video/mp4', 'video/webm', 'video/ogg'] },
|
||||
{ key: 'uploads.cleanup_days', type: 'integer', min: 0, defaultValue: 30 },
|
||||
{ key: 'uploads.optimize_images', type: 'boolean', defaultValue: true },
|
||||
{ key: 'announcements.default_icon', type: 'string', defaultValue: 'megaphone-fill' },
|
||||
{ key: 'announcements.default_duration_value', type: 'integer', min: 1, defaultValue: 10 },
|
||||
{ key: 'announcements.default_duration_unit', type: 'enum', values: ['seconds', 'minutes'], defaultValue: 'seconds' },
|
||||
{ key: 'announcements.suggested_icons', type: 'string_array', defaultValue: DEFAULT_ANNOUNCEMENT_ICON_KEYS.slice() },
|
||||
{ key: 'player.default_slide_duration_seconds', type: 'integer', min: 1, defaultValue: 10 },
|
||||
{ key: 'player.default_fade_between_slides', type: 'boolean', defaultValue: true },
|
||||
{ key: 'player.skip_unavailable_rtmp', type: 'boolean', defaultValue: true }
|
||||
,{ key: 'data-sources.rss_default_interval_value', type: 'integer', min: 1, defaultValue: 60 }
|
||||
,{ key: 'data-sources.rss_default_interval_unit', type: 'enum', values: ['seconds', 'minutes', 'hours'], defaultValue: 'minutes' }
|
||||
,{ key: 'data-sources.api_default_interval_value', type: 'integer', min: 1, defaultValue: 60 }
|
||||
,{ key: 'data-sources.api_default_interval_unit', type: 'enum', values: ['seconds', 'minutes', 'hours'], defaultValue: 'minutes' }
|
||||
];
|
||||
|
||||
const DEFINITIONS_BY_KEY = new Map(SETTING_DEFINITIONS.map(function (definition) {
|
||||
return [definition.key, definition];
|
||||
}));
|
||||
|
||||
function cloneValue(value) {
|
||||
if (Array.isArray(value)) {
|
||||
return value.slice();
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function getAppSettingDefinitions() {
|
||||
return SETTING_DEFINITIONS.map(function (definition) {
|
||||
return Object.assign({}, definition, {
|
||||
values: definition.values ? definition.values.slice() : undefined,
|
||||
defaultValue: cloneValue(definition.defaultValue)
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getDefaultAppSettings() {
|
||||
return SETTING_DEFINITIONS.reduce(function (settings, definition) {
|
||||
settings[definition.key] = cloneValue(definition.defaultValue);
|
||||
return settings;
|
||||
}, {});
|
||||
}
|
||||
|
||||
function normalizeSettingValue(key, value) {
|
||||
const definition = DEFINITIONS_BY_KEY.get(String(key || '').trim());
|
||||
if (!definition) {
|
||||
throw new Error('Unknown application setting: ' + key);
|
||||
}
|
||||
|
||||
if (definition.type === 'string') {
|
||||
return String(value == null ? '' : value).trim();
|
||||
}
|
||||
|
||||
if (definition.type === 'integer') {
|
||||
const normalized = Number(value);
|
||||
if (!Number.isInteger(normalized) || normalized < definition.min) {
|
||||
throw new Error('Invalid value for application setting: ' + definition.key);
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
if (definition.type === 'boolean') {
|
||||
if (value === true || value === 1 || value === '1' || value === 'true') {
|
||||
return true;
|
||||
}
|
||||
if (value === false || value === 0 || value === '0' || value === 'false') {
|
||||
return false;
|
||||
}
|
||||
throw new Error('Invalid value for application setting: ' + definition.key);
|
||||
}
|
||||
|
||||
if (definition.type === 'enum') {
|
||||
const normalized = String(value == null ? '' : value).trim();
|
||||
if (!definition.values.includes(normalized)) {
|
||||
throw new Error('Invalid value for application setting: ' + definition.key);
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
if (definition.type === 'string_array') {
|
||||
if (!Array.isArray(value)) {
|
||||
throw new Error('Invalid value for application setting: ' + definition.key);
|
||||
}
|
||||
return Array.from(new Set(value.map(function (item) {
|
||||
return String(item || '').trim();
|
||||
}).filter(Boolean)));
|
||||
}
|
||||
|
||||
throw new Error('Unsupported application setting type: ' + definition.type);
|
||||
}
|
||||
|
||||
function normalizeAppSettings(settings) {
|
||||
const input = settings && typeof settings === 'object' ? settings : {};
|
||||
return SETTING_DEFINITIONS.reduce(function (normalized, definition) {
|
||||
const value = Object.prototype.hasOwnProperty.call(input, definition.key)
|
||||
? input[definition.key]
|
||||
: definition.defaultValue;
|
||||
normalized[definition.key] = normalizeSettingValue(definition.key, value);
|
||||
return normalized;
|
||||
}, {});
|
||||
}
|
||||
|
||||
function parseStoredValue(value) {
|
||||
if (typeof value !== 'string') {
|
||||
return value;
|
||||
}
|
||||
try {
|
||||
return JSON.parse(value);
|
||||
} catch (_error) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchAppSettings(pool) {
|
||||
const [rows] = await pool.query('SELECT id, setting_key, setting_value FROM o_app_settings ORDER BY setting_key');
|
||||
const storedSettings = {};
|
||||
(rows || []).forEach(function (row) {
|
||||
const key = String(row && row.setting_key || '').trim();
|
||||
if (DEFINITIONS_BY_KEY.has(key)) {
|
||||
storedSettings[key] = parseStoredValue(row.setting_value);
|
||||
}
|
||||
});
|
||||
return normalizeAppSettings(Object.assign({}, getDefaultAppSettings(), storedSettings));
|
||||
}
|
||||
|
||||
async function saveAppSettings(pool, settings, modifiedBy) {
|
||||
const inputSettings = settings && typeof settings === 'object' ? settings : {};
|
||||
const normalizedSettings = normalizeAppSettings(inputSettings);
|
||||
const connection = typeof pool.getConnection === 'function' ? await pool.getConnection() : pool;
|
||||
const shouldRelease = connection !== pool;
|
||||
|
||||
try {
|
||||
if (typeof connection.beginTransaction === 'function') {
|
||||
await connection.beginTransaction();
|
||||
}
|
||||
for (const definition of SETTING_DEFINITIONS) {
|
||||
if (!Object.prototype.hasOwnProperty.call(inputSettings, definition.key)) {
|
||||
continue;
|
||||
}
|
||||
await connection.query(
|
||||
`INSERT INTO o_app_settings (setting_key, setting_value, created_by, modified_by)
|
||||
VALUES (?, ?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE setting_value = VALUES(setting_value), modified_by = VALUES(modified_by)`,
|
||||
[definition.key, JSON.stringify(normalizedSettings[definition.key]), modifiedBy || null, modifiedBy || null]
|
||||
);
|
||||
}
|
||||
if (typeof connection.commit === 'function') {
|
||||
await connection.commit();
|
||||
}
|
||||
} catch (error) {
|
||||
if (typeof connection.rollback === 'function') {
|
||||
await connection.rollback();
|
||||
}
|
||||
throw error;
|
||||
} finally {
|
||||
if (shouldRelease && typeof connection.release === 'function') {
|
||||
connection.release();
|
||||
}
|
||||
}
|
||||
|
||||
return normalizedSettings;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getAppSettingDefinitions,
|
||||
getDefaultAppSettings,
|
||||
normalizeSettingValue,
|
||||
normalizeAppSettings,
|
||||
fetchAppSettings,
|
||||
saveAppSettings
|
||||
};
|
||||
@@ -0,0 +1,102 @@
|
||||
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 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,
|
||||
recordAuditEvent,
|
||||
recordRequestAuditEvent
|
||||
};
|
||||
@@ -1,17 +1,6 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const QRCodeStyling = require(path.join(__dirname, '..', 'web', 'public', 'vendor', 'qr-code-styling', 'qr-code-styling.js'));
|
||||
const QR_PNG_WIDTH = 2048;
|
||||
const QR_STYLING_SCRIPT_PATH = path.join(__dirname, '..', 'web', 'public', 'vendor', 'qr-code-styling', 'qr-code-styling.js');
|
||||
const SYSTEM_CHROMIUM_PATHS = [
|
||||
process.env.PUPPETEER_EXECUTABLE_PATH,
|
||||
process.env.CHROMIUM_PATH,
|
||||
'/usr/bin/chromium',
|
||||
'/usr/bin/chromium-browser',
|
||||
'/usr/local/bin/chromium',
|
||||
'/snap/bin/chromium'
|
||||
].filter(Boolean);
|
||||
let qrBrowserPromise = null;
|
||||
|
||||
function escapeXml(value) {
|
||||
return String(value === undefined || value === null ? '' : value).replace(/[&<>"']/g, function (character) {
|
||||
@@ -340,81 +329,6 @@ function buildQrStylingOptions(source) {
|
||||
};
|
||||
}
|
||||
|
||||
function getQrBrowser() {
|
||||
if (qrBrowserPromise) {
|
||||
return qrBrowserPromise;
|
||||
}
|
||||
|
||||
qrBrowserPromise = (async function () {
|
||||
const puppeteer = require('puppeteer-core');
|
||||
const chromiumModule = require('@sparticuz/chromium');
|
||||
const chromium = chromiumModule && typeof chromiumModule.executablePath === 'function'
|
||||
? chromiumModule
|
||||
: chromiumModule && chromiumModule.default && typeof chromiumModule.default.executablePath === 'function'
|
||||
? chromiumModule.default
|
||||
: chromiumModule;
|
||||
let executablePath = SYSTEM_CHROMIUM_PATHS.find(function (candidate) {
|
||||
return fs.existsSync(candidate);
|
||||
}) || '';
|
||||
const usingSystemChromium = Boolean(executablePath);
|
||||
|
||||
if (!executablePath && chromium && typeof chromium.executablePath === 'function') {
|
||||
executablePath = await chromium.executablePath();
|
||||
}
|
||||
|
||||
if (!executablePath || !fs.existsSync(executablePath)) {
|
||||
throw new Error('Chromium executable was not found.');
|
||||
}
|
||||
|
||||
return puppeteer.launch({
|
||||
args: usingSystemChromium
|
||||
? [
|
||||
'--no-sandbox',
|
||||
'--disable-setuid-sandbox',
|
||||
'--disable-dev-shm-usage',
|
||||
'--disable-gpu'
|
||||
]
|
||||
: puppeteer.defaultArgs({
|
||||
args: chromium && chromium.args ? chromium.args : [],
|
||||
headless: 'shell'
|
||||
}),
|
||||
defaultViewport: usingSystemChromium
|
||||
? { width: QR_PNG_WIDTH, height: QR_PNG_WIDTH, deviceScaleFactor: 1 }
|
||||
: chromium && chromium.defaultViewport ? chromium.defaultViewport : null,
|
||||
executablePath: executablePath,
|
||||
headless: usingSystemChromium ? true : 'shell'
|
||||
});
|
||||
})();
|
||||
|
||||
return qrBrowserPromise;
|
||||
}
|
||||
|
||||
async function blobToDataUrl(blob) {
|
||||
if (!blob) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (typeof blob === 'string') {
|
||||
return blob;
|
||||
}
|
||||
|
||||
if (typeof blob.arrayBuffer === 'function') {
|
||||
const buffer = await blob.arrayBuffer();
|
||||
const bytes = new Uint8Array(buffer);
|
||||
let binary = '';
|
||||
for (let index = 0; index < bytes.length; index += 1) {
|
||||
binary += String.fromCharCode(bytes[index]);
|
||||
}
|
||||
return 'data:' + String(blob.type || 'image/png') + ';base64,' + Buffer.from(binary, 'binary').toString('base64');
|
||||
}
|
||||
|
||||
if (typeof blob.text === 'function') {
|
||||
return blob.text();
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
async function createStyledQrCodeDataUrl(value) {
|
||||
const source = value && typeof value === 'object' ? value : { value: value };
|
||||
const options = buildQrStylingOptions(source);
|
||||
@@ -435,10 +349,6 @@ async function createStyledQrCodeSvg(value) {
|
||||
return renderStyledQrRawData(options, 'svg');
|
||||
}
|
||||
|
||||
async function createQrCodeDataUrlPlain(value) {
|
||||
return createStyledQrCodeDataUrl(value);
|
||||
}
|
||||
|
||||
async function createQrCodeSvg(value) {
|
||||
return createStyledQrCodeSvg(value);
|
||||
}
|
||||
|
||||
@@ -93,14 +93,6 @@ function sanitizeRichText(html) {
|
||||
});
|
||||
}
|
||||
|
||||
function normalizePlainText(value) {
|
||||
return String(value === undefined || value === null ? '' : value)
|
||||
.replace(/<\s*br\s*\/?\s*>/gi, '\n')
|
||||
.replace(/<[^>]*>/g, '')
|
||||
.replace(/ /gi, ' ')
|
||||
.trim();
|
||||
}
|
||||
|
||||
function stripEditorOnlyMarkup(value) {
|
||||
return String(value || '')
|
||||
.replace(/<pre[^>]*class="[^"]*api-region-sample-preview[^"]*"[^>]*>[\s\S]*?<\/pre>/gi, '')
|
||||
|
||||
@@ -194,43 +194,6 @@ function extractTemplateRegions(body) {
|
||||
return regions;
|
||||
}
|
||||
|
||||
function extractGenericRegionContent(region, body, filesByField, existingContent) {
|
||||
const content = {};
|
||||
const current = existingContent && existingContent[region.region_key] && typeof existingContent[region.region_key] === 'object' ? existingContent[region.region_key] : {};
|
||||
const suffix = '_' + region.id;
|
||||
|
||||
Object.keys(body || {}).forEach((key) => {
|
||||
if (!key.startsWith('region_') || !key.endsWith(suffix)) {
|
||||
return;
|
||||
}
|
||||
const field = key.slice('region_'.length, -suffix.length);
|
||||
if (!field || field === 'type' || field === 'key' || field === 'name' || field === 'label') {
|
||||
return;
|
||||
}
|
||||
content[field] = body[key];
|
||||
});
|
||||
|
||||
Object.keys(filesByField || {}).forEach((fieldName) => {
|
||||
if (!fieldName.startsWith('region_') || !fieldName.endsWith(suffix)) {
|
||||
return;
|
||||
}
|
||||
const field = fieldName.slice('region_'.length, -suffix.length);
|
||||
if (!field) {
|
||||
return;
|
||||
}
|
||||
content[field] = `/media/uploads/${filesByField[fieldName].filename}`;
|
||||
});
|
||||
|
||||
Object.keys(current).forEach((key) => {
|
||||
if (content[key] === undefined) {
|
||||
content[key] = current[key];
|
||||
}
|
||||
});
|
||||
|
||||
content.type = region.region_type;
|
||||
return content;
|
||||
}
|
||||
|
||||
function getFilesByField(files) {
|
||||
const map = {};
|
||||
(files || []).forEach((file) => {
|
||||
|
||||
Reference in New Issue
Block a user