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( `UPDATE o_app_settings SET setting_value = ?, modified_by = ? WHERE setting_key = ?`, [JSON.stringify(normalizedSettings[definition.key]), modifiedBy || null, definition.key] ); await connection.query( `INSERT INTO o_app_settings (setting_key, setting_value, created_by, modified_by) SELECT ?, ?, ?, ? WHERE NOT EXISTS ( SELECT 1 FROM o_app_settings WHERE setting_key = ? )`, [definition.key, JSON.stringify(normalizedSettings[definition.key]), modifiedBy || null, modifiedBy || null, definition.key] ); } 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 };