397 lines
28 KiB
JavaScript
397 lines
28 KiB
JavaScript
// Settings page route registration.
|
|
|
|
const { ANNOUNCEMENT_ICON_CATALOG } = require('#src/data/announcement-icons');
|
|
const { AUDIT_CATEGORY_KEYS, AUDIT_CATEGORY_LABELS } = require('#src/data/audit-log');
|
|
const { fetchAppSettings, saveAppSettings } = require('#src/data/app-settings');
|
|
const { appVersion, detectSchemaVersion } = require('#src/db/migrations');
|
|
|
|
const UPLOAD_MIME_TYPE_OPTIONS = [
|
|
{ value: 'image/png', label: 'PNG', category: 'Images' },
|
|
{ value: 'image/jpeg', label: 'JPEG', category: 'Images' },
|
|
{ value: 'image/gif', label: 'GIF', category: 'Images' },
|
|
{ value: 'image/webp', label: 'WebP', category: 'Images' },
|
|
{ value: 'image/svg+xml', label: 'SVG', category: 'Images' },
|
|
{ value: 'video/mp4', label: 'MP4', category: 'Video' },
|
|
{ value: 'video/webm', label: 'WebM', category: 'Video' },
|
|
{ value: 'video/ogg', label: 'Ogg', category: 'Video' }
|
|
];
|
|
|
|
module.exports = function registerSettingsPageRoutes(app, deps) {
|
|
const pages = deps.pages;
|
|
const pool = deps.pool;
|
|
const recordRequestAuditEvent = deps.recordRequestAuditEvent;
|
|
|
|
if (!pages || !pool || typeof deps.requirePermission !== 'function') {
|
|
throw new Error('registerSettingsPageRoutes requires pool, pages, and requirePermission.');
|
|
}
|
|
|
|
const requireSettingsRead = deps.requirePermission('system-settings.read');
|
|
const MAX_SUGGESTED_ICONS = 48;
|
|
const SECTION_SETTING_KEYS = {
|
|
defaults: [
|
|
'player.default_slide_duration_seconds',
|
|
'player.default_fade_between_slides',
|
|
'player.skip_unavailable_rtmp',
|
|
'announcements.default_icon',
|
|
'announcements.default_duration_value',
|
|
'announcements.default_duration_unit',
|
|
'data-sources.rss_default_interval_value',
|
|
'data-sources.rss_default_interval_unit',
|
|
'data-sources.api_default_interval_value',
|
|
'data-sources.api_default_interval_unit'
|
|
],
|
|
security: [
|
|
'security.session_lifetime_days',
|
|
'security.allow_user_session_revocation',
|
|
'security.max_active_sessions',
|
|
'security.login_max_attempts',
|
|
'security.login_lockout_minutes',
|
|
'security.login_rate_limit_scope',
|
|
'security.password_min_length',
|
|
'security.password_min_categories',
|
|
'security.password_require_lowercase',
|
|
'security.password_require_uppercase',
|
|
'security.password_require_number',
|
|
'security.password_require_symbol',
|
|
'security.require_password_change_for_new_users',
|
|
'security.require_password_change_after_admin_reset'
|
|
],
|
|
audit: ['audit.enabled', 'audit.categories', 'audit.include_request_metadata', 'audit.retention_days'],
|
|
media: ['uploads.image_max_bytes', 'uploads.video_max_bytes', 'uploads.wysiwyg_image_max_bytes', 'uploads.allowed_mime_types'],
|
|
icons: ['announcements.suggested_icons']
|
|
};
|
|
|
|
function buildSettingChanges(previousSettings, nextSettings, keys) {
|
|
const changes = {};
|
|
(Array.isArray(keys) ? keys : []).forEach(function (key) {
|
|
const previousValue = previousSettings && previousSettings[key];
|
|
const nextValue = nextSettings[key];
|
|
if (JSON.stringify(previousValue) !== JSON.stringify(nextValue)) {
|
|
changes[key] = { from: previousValue, to: nextValue };
|
|
}
|
|
});
|
|
return changes;
|
|
}
|
|
|
|
async function recordSettingsAuditEvent(req, previousSettings, nextSettings, section) {
|
|
if (typeof recordRequestAuditEvent !== 'function') {
|
|
return;
|
|
}
|
|
const changes = buildSettingChanges(previousSettings, nextSettings, SECTION_SETTING_KEYS[section]);
|
|
const changedKeys = Object.keys(changes);
|
|
if (!changedKeys.length) {
|
|
return;
|
|
}
|
|
await recordRequestAuditEvent(pool, req, {
|
|
category: 'system-settings',
|
|
eventType: 'system-settings.updated',
|
|
actorUserId: req.currentUser && req.currentUser.id,
|
|
targetType: 'settings',
|
|
targetId: section,
|
|
targetLabel: section,
|
|
details: { section: section, changes: changes }
|
|
});
|
|
}
|
|
|
|
function buildSuggestedAnnouncementIcons(settings) {
|
|
const configuredKeys = settings && Array.isArray(settings['announcements.suggested_icons'])
|
|
? settings['announcements.suggested_icons']
|
|
: [];
|
|
const selectedKeys = new Set(configuredKeys.map(function (key) {
|
|
return String(key || '').trim().toLowerCase();
|
|
}));
|
|
const catalogByKey = new Map(ANNOUNCEMENT_ICON_CATALOG.map(function (option) {
|
|
return [option.value, option];
|
|
}));
|
|
const selectedOptions = Array.from(new Set(configuredKeys.map(function (key) {
|
|
return catalogByKey.get(String(key || '').trim().toLowerCase()) || null;
|
|
}).filter(Boolean)));
|
|
const remainingOptions = ANNOUNCEMENT_ICON_CATALOG.filter(function (option) {
|
|
return !selectedKeys.has(option.value);
|
|
});
|
|
return selectedOptions.concat(remainingOptions).map(function (option) {
|
|
return Object.assign({}, option, {
|
|
isSelected: selectedKeys.has(option.value),
|
|
isDefault: selectedOptions.length > 0 && selectedOptions[0].value === option.value
|
|
});
|
|
});
|
|
}
|
|
|
|
async function renderSettings(req, res, message) {
|
|
const settings = await fetchAppSettings(pool);
|
|
const diagnostics = await buildDiagnostics();
|
|
res.send(pages.renderSettingsPage(settings, buildSuggestedAnnouncementIcons(settings), req.currentUser, message, {
|
|
imageMaxMb: Math.round(Number(settings['uploads.image_max_bytes']) / 1024 / 1024),
|
|
videoMaxMb: Math.round(Number(settings['uploads.video_max_bytes']) / 1024 / 1024),
|
|
wysiwygImageMaxMb: (Number(settings['uploads.wysiwyg_image_max_bytes']) / 1024 / 1024).toFixed(2).replace(/0+$/, '').replace(/\.$/, '')
|
|
,allowedMimeTypes: UPLOAD_MIME_TYPE_OPTIONS.map(function (option) {
|
|
return Object.assign({}, option, { isSelected: settings['uploads.allowed_mime_types'].indexOf(option.value) !== -1 });
|
|
}),
|
|
imageMimeTypes: UPLOAD_MIME_TYPE_OPTIONS.filter(function (option) { return option.category === 'Images'; }).map(function (option) {
|
|
return Object.assign({}, option, { isSelected: settings['uploads.allowed_mime_types'].indexOf(option.value) !== -1 });
|
|
}),
|
|
videoMimeTypes: UPLOAD_MIME_TYPE_OPTIONS.filter(function (option) { return option.category === 'Video'; }).map(function (option) {
|
|
return Object.assign({}, option, { isSelected: settings['uploads.allowed_mime_types'].indexOf(option.value) !== -1 });
|
|
}),
|
|
sessionLifetimeDays: Number(settings['security.session_lifetime_days']) || 14,
|
|
allowUserSessionRevocation: Boolean(settings['security.allow_user_session_revocation']),
|
|
maxActiveSessions: Number(settings['security.max_active_sessions']) || 0,
|
|
loginMaxAttempts: Number(settings['security.login_max_attempts']) || 5,
|
|
loginLockoutMinutes: Number(settings['security.login_lockout_minutes']) || 15,
|
|
loginRateLimitScope: String(settings['security.login_rate_limit_scope'] || 'both'),
|
|
passwordMinLength: Number(settings['security.password_min_length']) || 10,
|
|
passwordMinCategories: Number(settings['security.password_min_categories']) || 3,
|
|
passwordRequireLowercase: Boolean(settings['security.password_require_lowercase']),
|
|
passwordRequireUppercase: Boolean(settings['security.password_require_uppercase']),
|
|
passwordRequireNumber: Boolean(settings['security.password_require_number']),
|
|
passwordRequireSymbol: Boolean(settings['security.password_require_symbol']),
|
|
requirePasswordChangeForNewUsers: Boolean(settings['security.require_password_change_for_new_users']),
|
|
requirePasswordChangeAfterAdminReset: Boolean(settings['security.require_password_change_after_admin_reset']),
|
|
defaultSlideDurationSeconds: Number(settings['player.default_slide_duration_seconds']) || 10,
|
|
defaultFadeBetweenSlides: Boolean(settings['player.default_fade_between_slides']),
|
|
defaultSkipUnavailableRtmp: Boolean(settings['player.skip_unavailable_rtmp']),
|
|
defaultAnnouncementIcon: String(settings['announcements.default_icon'] || 'megaphone-fill'),
|
|
defaultAnnouncementDurationValue: Number(settings['announcements.default_duration_value']) || 10,
|
|
defaultAnnouncementDurationUnit: String(settings['announcements.default_duration_unit'] || 'seconds')
|
|
,rssDefaultIntervalValue: Number(settings['data-sources.rss_default_interval_value']) || 60
|
|
,rssDefaultIntervalUnit: String(settings['data-sources.rss_default_interval_unit'] || 'minutes')
|
|
,apiDefaultIntervalValue: Number(settings['data-sources.api_default_interval_value']) || 60
|
|
,apiDefaultIntervalUnit: String(settings['data-sources.api_default_interval_unit'] || 'minutes')
|
|
,auditRetentionDays: Number(settings['audit.retention_days']) || 0
|
|
,auditEnabled: Boolean(settings['audit.enabled'])
|
|
,auditIncludeRequestMetadata: Boolean(settings['audit.include_request_metadata'])
|
|
,auditCategories: AUDIT_CATEGORY_KEYS.map(function (category) {
|
|
return { key: category, label: AUDIT_CATEGORY_LABELS[category] || category, isSelected: settings['audit.categories'].includes(category) };
|
|
})
|
|
,auditEventCategories: AUDIT_CATEGORY_KEYS.filter(function (category) { return category !== 'users' && category !== 'roles' && category !== 'system-settings' && category !== 'slides' && category !== 'templates' && category !== 'playlists' && category !== 'screens' && category !== 'announcements' && category !== 'canvas-sizes' && category !== 'api-sources' && category !== 'rss-feeds' && category !== 'timetables'; }).map(function (category) {
|
|
return { key: category, label: AUDIT_CATEGORY_LABELS[category] || category, isSelected: settings['audit.categories'].includes(category) };
|
|
})
|
|
,auditAdministrationCategories: AUDIT_CATEGORY_KEYS.filter(function (category) { return category === 'users' || category === 'roles' || category === 'system-settings'; }).map(function (category) {
|
|
return { key: category, label: AUDIT_CATEGORY_LABELS[category] || category, isSelected: settings['audit.categories'].includes(category) };
|
|
})
|
|
,auditContentCategories: AUDIT_CATEGORY_KEYS.filter(function (category) { return category === 'slides' || category === 'templates' || category === 'playlists' || category === 'screens' || category === 'announcements' || category === 'canvas-sizes'; }).map(function (category) {
|
|
return { key: category, label: AUDIT_CATEGORY_LABELS[category] || category, isSelected: settings['audit.categories'].includes(category) };
|
|
})
|
|
,auditDataSourceCategories: AUDIT_CATEGORY_KEYS.filter(function (category) { return category === 'api-sources' || category === 'rss-feeds' || category === 'timetables'; }).map(function (category) {
|
|
return { key: category, label: AUDIT_CATEGORY_LABELS[category] || category, isSelected: settings['audit.categories'].includes(category) };
|
|
})
|
|
}, diagnostics));
|
|
}
|
|
|
|
async function buildDiagnostics() {
|
|
const diagnostics = {
|
|
applicationVersion: appVersion,
|
|
schemaVersion: 'Unknown',
|
|
databaseStatus: 'Unavailable',
|
|
activeSessionCount: 0,
|
|
backgroundTaskCount: 0,
|
|
criticalTableCounts: []
|
|
};
|
|
try {
|
|
await pool.query('SELECT 1 AS health');
|
|
diagnostics.databaseStatus = 'Connected';
|
|
diagnostics.schemaVersion = await detectSchemaVersion(pool);
|
|
const [sessionRows] = await pool.query('SELECT COUNT(*) AS count FROM a_sessions WHERE expires_at > NOW()');
|
|
diagnostics.activeSessionCount = Number(sessionRows && sessionRows[0] && sessionRows[0].count) || 0;
|
|
const [taskRows] = await pool.query('SELECT COUNT(*) AS count FROM o_background_tasks');
|
|
diagnostics.backgroundTaskCount = Number(taskRows && taskRows[0] && taskRows[0].count) || 0;
|
|
const criticalTables = [
|
|
['Users', 'a_users'],
|
|
['Roles', 'a_roles'],
|
|
['Screens', 'd_screens'],
|
|
['Playlists', 'c_playlists'],
|
|
['Slides', 'c_slides'],
|
|
['Templates', 'c_templates'],
|
|
['Announcements', 'd_announcements'],
|
|
['RSS feeds', 'i_rss_feeds'],
|
|
['API sources', 'i_api_sources']
|
|
];
|
|
for (const [label, tableName] of criticalTables) {
|
|
try {
|
|
const [rows] = await pool.query('SELECT COUNT(*) AS count FROM ' + tableName);
|
|
diagnostics.criticalTableCounts.push({ label: label, count: Number(rows && rows[0] && rows[0].count) || 0 });
|
|
} catch (_error) {
|
|
diagnostics.criticalTableCounts.push({ label: label, count: 'Unavailable' });
|
|
}
|
|
}
|
|
} catch (_error) {
|
|
// Keep diagnostics page renderable while the database is unavailable.
|
|
}
|
|
return diagnostics;
|
|
}
|
|
|
|
app.get('/settings/system', requireSettingsRead, async function (req, res, next) {
|
|
try {
|
|
await renderSettings(req, res, req.query.message ? String(req.query.message) : '');
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
});
|
|
|
|
app.post('/settings/system', deps.requirePermission('system-settings.update'), async function (req, res, next) {
|
|
try {
|
|
const section = String(req.body && req.body.settings_section || '').trim().toLowerCase();
|
|
if (section !== 'icons' && section !== 'media' && section !== 'security' && section !== 'defaults' && section !== 'audit') {
|
|
const error = new Error('Unknown settings section.');
|
|
error.statusCode = 400;
|
|
error.expose = true;
|
|
throw error;
|
|
}
|
|
const previousSettings = await fetchAppSettings(pool);
|
|
|
|
if (section === 'defaults') {
|
|
const defaultSlideDurationSeconds = Number(req.body && req.body.default_slide_duration_seconds);
|
|
const defaultAnnouncementDurationValue = Number(req.body && req.body.default_announcement_duration_value);
|
|
const defaultAnnouncementDurationUnit = String(req.body && req.body.default_announcement_duration_unit || 'seconds').trim().toLowerCase();
|
|
const rssDefaultIntervalValue = Number(req.body && req.body.rss_default_interval_value);
|
|
const rssDefaultIntervalUnit = String(req.body && req.body.rss_default_interval_unit || 'minutes').trim().toLowerCase();
|
|
const apiDefaultIntervalValue = Number(req.body && req.body.api_default_interval_value);
|
|
const apiDefaultIntervalUnit = String(req.body && req.body.api_default_interval_unit || 'minutes').trim().toLowerCase();
|
|
const defaultFadeBetweenSlides = req.body && (req.body.default_fade_between_slides === '1' || req.body.default_fade_between_slides === 'true');
|
|
const defaultSkipUnavailableRtmp = req.body && (req.body.default_skip_unavailable_rtmp === '1' || req.body.default_skip_unavailable_rtmp === 'true');
|
|
const defaultAnnouncementIcon = String(req.body && req.body.default_announcement_icon || '').trim();
|
|
if (!Number.isInteger(defaultSlideDurationSeconds) || defaultSlideDurationSeconds < 1 || defaultSlideDurationSeconds > 3600 || !Number.isInteger(defaultAnnouncementDurationValue) || defaultAnnouncementDurationValue < 1 || defaultAnnouncementDurationValue > 3600 || !['seconds', 'minutes'].includes(defaultAnnouncementDurationUnit) || !Number.isInteger(rssDefaultIntervalValue) || rssDefaultIntervalValue < 1 || rssDefaultIntervalValue > 86400 || !['seconds', 'minutes', 'hours'].includes(rssDefaultIntervalUnit) || !Number.isInteger(apiDefaultIntervalValue) || apiDefaultIntervalValue < 1 || apiDefaultIntervalValue > 86400 || !['seconds', 'minutes', 'hours'].includes(apiDefaultIntervalUnit) || !ANNOUNCEMENT_ICON_CATALOG.some(function (option) { return option.value === defaultAnnouncementIcon; })) {
|
|
const error = new Error('Default values must be valid.');
|
|
error.statusCode = 400;
|
|
error.expose = true;
|
|
throw error;
|
|
}
|
|
const savedSettings = await saveAppSettings(pool, {
|
|
'player.default_slide_duration_seconds': defaultSlideDurationSeconds,
|
|
'player.default_fade_between_slides': defaultFadeBetweenSlides,
|
|
'player.skip_unavailable_rtmp': defaultSkipUnavailableRtmp,
|
|
'announcements.default_icon': defaultAnnouncementIcon,
|
|
'announcements.default_duration_value': defaultAnnouncementDurationValue,
|
|
'announcements.default_duration_unit': defaultAnnouncementDurationUnit,
|
|
'data-sources.rss_default_interval_value': rssDefaultIntervalValue,
|
|
'data-sources.rss_default_interval_unit': rssDefaultIntervalUnit,
|
|
'data-sources.api_default_interval_value': apiDefaultIntervalValue,
|
|
'data-sources.api_default_interval_unit': apiDefaultIntervalUnit
|
|
}, req.currentUser && req.currentUser.id);
|
|
await recordSettingsAuditEvent(req, previousSettings, savedSettings, section);
|
|
return res.redirect('/settings/system?message=' + encodeURIComponent('Defaults saved.') + '#application-defaults');
|
|
}
|
|
|
|
if (section === 'security') {
|
|
const sessionLifetimeDays = Number(req.body && req.body.session_lifetime_days);
|
|
const allowUserSessionRevocation = req.body && (req.body.allow_user_session_revocation === '1' || req.body.allow_user_session_revocation === 'true');
|
|
const maxActiveSessions = Number(req.body && req.body.max_active_sessions);
|
|
const loginMaxAttempts = Number(req.body && req.body.login_max_attempts);
|
|
const loginLockoutMinutes = Number(req.body && req.body.login_lockout_minutes);
|
|
const loginRateLimitScope = String(req.body && req.body.login_rate_limit_scope || 'both').trim().toLowerCase();
|
|
const passwordMinLength = Number(req.body && req.body.password_min_length);
|
|
const passwordMinCategories = Number(req.body && req.body.password_min_categories);
|
|
const passwordRequireLowercase = req.body && (req.body.password_require_lowercase === '1' || req.body.password_require_lowercase === 'true');
|
|
const passwordRequireUppercase = req.body && (req.body.password_require_uppercase === '1' || req.body.password_require_uppercase === 'true');
|
|
const passwordRequireNumber = req.body && (req.body.password_require_number === '1' || req.body.password_require_number === 'true');
|
|
const passwordRequireSymbol = req.body && (req.body.password_require_symbol === '1' || req.body.password_require_symbol === 'true');
|
|
const requirePasswordChangeForNewUsers = req.body && (req.body.require_password_change_for_new_users === '1' || req.body.require_password_change_for_new_users === 'true');
|
|
const requirePasswordChangeAfterAdminReset = req.body && (req.body.require_password_change_after_admin_reset === '1' || req.body.require_password_change_after_admin_reset === 'true');
|
|
const requiredCategoryCount = [passwordRequireLowercase, passwordRequireUppercase, passwordRequireNumber, passwordRequireSymbol].filter(Boolean).length;
|
|
if (!Number.isInteger(sessionLifetimeDays) || sessionLifetimeDays < 1 || sessionLifetimeDays > 365 || !Number.isInteger(maxActiveSessions) || maxActiveSessions < 0 || maxActiveSessions > 100 || !Number.isInteger(loginMaxAttempts) || loginMaxAttempts < 1 || loginMaxAttempts > 100 || !Number.isInteger(loginLockoutMinutes) || loginLockoutMinutes < 1 || loginLockoutMinutes > 1440 || !['both', 'username', 'ip'].includes(loginRateLimitScope) || !Number.isInteger(passwordMinLength) || passwordMinLength < 8 || passwordMinLength > 128 || !Number.isInteger(passwordMinCategories) || passwordMinCategories < 1 || passwordMinCategories > 4 || requiredCategoryCount > passwordMinCategories) {
|
|
const error = new Error('Session and password settings must use valid values; required password categories cannot exceed the minimum category count.');
|
|
error.statusCode = 400;
|
|
error.expose = true;
|
|
throw error;
|
|
}
|
|
const savedSettings = await saveAppSettings(pool, {
|
|
'security.session_lifetime_days': sessionLifetimeDays,
|
|
'security.allow_user_session_revocation': allowUserSessionRevocation,
|
|
'security.max_active_sessions': maxActiveSessions,
|
|
'security.login_max_attempts': loginMaxAttempts,
|
|
'security.login_lockout_minutes': loginLockoutMinutes,
|
|
'security.login_rate_limit_scope': loginRateLimitScope,
|
|
'security.password_min_length': passwordMinLength,
|
|
'security.password_min_categories': passwordMinCategories,
|
|
'security.password_require_lowercase': passwordRequireLowercase,
|
|
'security.password_require_uppercase': passwordRequireUppercase,
|
|
'security.password_require_number': passwordRequireNumber,
|
|
'security.password_require_symbol': passwordRequireSymbol,
|
|
'security.require_password_change_for_new_users': requirePasswordChangeForNewUsers,
|
|
'security.require_password_change_after_admin_reset': requirePasswordChangeAfterAdminReset
|
|
}, req.currentUser && req.currentUser.id);
|
|
await recordSettingsAuditEvent(req, previousSettings, savedSettings, section);
|
|
return res.redirect('/settings/system?message=' + encodeURIComponent('Security settings saved.') + '#security-sessions');
|
|
}
|
|
|
|
if (section === 'audit') {
|
|
const auditEnabled = req.body && (req.body.audit_enabled === '1' || req.body.audit_enabled === 'true');
|
|
const includeRequestMetadata = req.body && (req.body.include_request_metadata === '1' || req.body.include_request_metadata === 'true');
|
|
const selectedCategories = req.body && (req.body['audit_categories[]'] !== undefined ? req.body['audit_categories[]'] : req.body.audit_categories);
|
|
const auditCategories = Array.from(new Set((Array.isArray(selectedCategories) ? selectedCategories : selectedCategories ? [selectedCategories] : []).map(function (category) {
|
|
return String(category || '').trim().toLowerCase();
|
|
}).filter(function (category) {
|
|
return AUDIT_CATEGORY_KEYS.includes(category);
|
|
})));
|
|
const retentionDays = Number(req.body && req.body.audit_retention_days);
|
|
if (!auditCategories.length || !Number.isInteger(retentionDays) || retentionDays < 0 || retentionDays > 3650) {
|
|
const error = new Error('Select at least one audit category and use a retention period between 0 and 3650 days.');
|
|
error.statusCode = 400;
|
|
error.expose = true;
|
|
throw error;
|
|
}
|
|
const savedSettings = await saveAppSettings(pool, {
|
|
'audit.enabled': auditEnabled,
|
|
'audit.categories': auditCategories,
|
|
'audit.include_request_metadata': includeRequestMetadata,
|
|
'audit.retention_days': retentionDays
|
|
}, req.currentUser && req.currentUser.id);
|
|
await recordSettingsAuditEvent(req, previousSettings, savedSettings, section);
|
|
return res.redirect('/settings/system?message=' + encodeURIComponent('Audit logging settings saved.') + '#audit-logging');
|
|
}
|
|
|
|
if (section === 'media') {
|
|
const imageMaxMb = Number(req.body && req.body.image_max_mb);
|
|
const videoMaxMb = Number(req.body && req.body.video_max_mb);
|
|
const wysiwygImageMaxMb = Number(req.body && req.body.wysiwyg_image_max_mb);
|
|
const selectedMimeTypes = req.body && (req.body['allowed_mime_types[]'] !== undefined ? req.body['allowed_mime_types[]'] : req.body.allowed_mime_types);
|
|
const allowedMimeTypes = Array.from(new Set((Array.isArray(selectedMimeTypes) ? selectedMimeTypes : selectedMimeTypes ? [selectedMimeTypes] : []).map(function (mimeType) {
|
|
return String(mimeType || '').trim().toLowerCase();
|
|
}).filter(function (mimeType) {
|
|
return UPLOAD_MIME_TYPE_OPTIONS.some(function (option) { return option.value === mimeType; });
|
|
})));
|
|
const hasImageMimeType = allowedMimeTypes.some(function (mimeType) { return mimeType.indexOf('image/') === 0; });
|
|
const hasVideoMimeType = allowedMimeTypes.some(function (mimeType) { return mimeType.indexOf('video/') === 0; });
|
|
if (!Number.isInteger(imageMaxMb) || imageMaxMb < 1 || imageMaxMb > 1024 || !Number.isInteger(videoMaxMb) || videoMaxMb < 1 || videoMaxMb > 4096 || !Number.isFinite(wysiwygImageMaxMb) || wysiwygImageMaxMb < 0.1 || wysiwygImageMaxMb > imageMaxMb || !hasImageMimeType || !hasVideoMimeType) {
|
|
const error = new Error('Media upload limits must be valid and at least one image and video type must remain enabled.');
|
|
error.statusCode = 400;
|
|
error.expose = true;
|
|
throw error;
|
|
}
|
|
await pool.query('DELETE FROM o_app_settings WHERE setting_key NOT IN (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', ['announcements.suggested_icons', 'announcements.default_icon', 'announcements.default_duration_value', 'announcements.default_duration_unit', 'player.default_slide_duration_seconds', 'player.default_fade_between_slides', 'player.skip_unavailable_rtmp', 'data-sources.rss_default_interval_value', 'data-sources.rss_default_interval_unit', 'data-sources.api_default_interval_value', 'data-sources.api_default_interval_unit', 'uploads.image_max_bytes', 'uploads.video_max_bytes', 'uploads.wysiwyg_image_max_bytes', 'uploads.allowed_mime_types', 'security.session_lifetime_days', 'security.allow_user_session_revocation', 'security.max_active_sessions', 'security.login_max_attempts', 'security.login_lockout_minutes', 'security.login_rate_limit_scope', 'security.password_min_length', 'security.password_min_categories', 'security.password_require_lowercase', 'security.password_require_uppercase', 'security.password_require_number', 'security.password_require_symbol', 'security.require_password_change_for_new_users', 'security.require_password_change_after_admin_reset', 'audit.enabled', 'audit.categories', 'audit.include_request_metadata', 'audit.retention_days']);
|
|
const savedSettings = await saveAppSettings(pool, {
|
|
'uploads.image_max_bytes': imageMaxMb * 1024 * 1024,
|
|
'uploads.video_max_bytes': videoMaxMb * 1024 * 1024,
|
|
'uploads.wysiwyg_image_max_bytes': Math.round(wysiwygImageMaxMb * 1024 * 1024),
|
|
'uploads.allowed_mime_types': allowedMimeTypes
|
|
}, req.currentUser && req.currentUser.id);
|
|
await recordSettingsAuditEvent(req, previousSettings, savedSettings, section);
|
|
return res.redirect('/settings/system?message=' + encodeURIComponent('Media settings saved.') + '#media-uploads');
|
|
}
|
|
|
|
const selectedIcons = req.body && (req.body['suggested_icons[]'] !== undefined ? req.body['suggested_icons[]'] : req.body.suggested_icons);
|
|
const catalogKeys = new Set(ANNOUNCEMENT_ICON_CATALOG.map(function (option) {
|
|
return option.value;
|
|
}));
|
|
const suggestedIcons = Array.from(new Set((Array.isArray(selectedIcons) ? selectedIcons : selectedIcons ? [selectedIcons] : []).map(function (iconKey) {
|
|
return String(iconKey || '').trim().toLowerCase();
|
|
}).filter(function (iconKey) {
|
|
return catalogKeys.has(iconKey);
|
|
})));
|
|
if (suggestedIcons.length > MAX_SUGGESTED_ICONS) {
|
|
const error = new Error('Choose no more than ' + MAX_SUGGESTED_ICONS + ' suggested icons.');
|
|
error.statusCode = 400;
|
|
error.expose = true;
|
|
throw error;
|
|
}
|
|
await pool.query('DELETE FROM o_app_settings WHERE setting_key NOT IN (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', ['announcements.suggested_icons', 'announcements.default_icon', 'announcements.default_duration_value', 'announcements.default_duration_unit', 'player.default_slide_duration_seconds', 'player.default_fade_between_slides', 'player.skip_unavailable_rtmp', 'data-sources.rss_default_interval_value', 'data-sources.rss_default_interval_unit', 'data-sources.api_default_interval_value', 'data-sources.api_default_interval_unit', 'uploads.image_max_bytes', 'uploads.video_max_bytes', 'uploads.wysiwyg_image_max_bytes', 'uploads.allowed_mime_types', 'security.session_lifetime_days', 'security.allow_user_session_revocation', 'security.max_active_sessions', 'security.login_max_attempts', 'security.login_lockout_minutes', 'security.login_rate_limit_scope', 'security.password_min_length', 'security.password_min_categories', 'security.password_require_lowercase', 'security.password_require_uppercase', 'security.password_require_number', 'security.password_require_symbol', 'security.require_password_change_for_new_users', 'security.require_password_change_after_admin_reset', 'audit.enabled', 'audit.categories', 'audit.include_request_metadata', 'audit.retention_days']);
|
|
const savedSettings = await saveAppSettings(pool, {
|
|
'announcements.suggested_icons': suggestedIcons
|
|
}, req.currentUser && req.currentUser.id);
|
|
await recordSettingsAuditEvent(req, previousSettings, savedSettings, section);
|
|
res.redirect('/settings/system?message=' + encodeURIComponent('Settings saved.'));
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
});
|
|
}; |