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

This commit is contained in:
2026-09-04 15:43:55 +01:00
parent 2c150b5b2e
commit 98f969ca0f
104 changed files with 3559 additions and 447 deletions
@@ -0,0 +1,15 @@
// Pending invitations page renderer.
const { renderView } = require('../../../view');
module.exports = function renderInvitationsPage(data, currentUser) {
return renderView('settings/invitations/list', {
title: 'Pending invitations',
active: 'users-invitations',
userManagementMenuOpen: true,
currentUser: currentUser || null,
message: data.message || '',
invitations: data.invitations || [],
pagination: data.pagination || null
});
};
@@ -98,6 +98,7 @@ function buildRbacAddViewModel(message, currentUser, formValues, permissionGroup
return {
title: 'Create role',
active: 'rbac',
userManagementMenuOpen: true,
isEdit: false,
usersPresent: true,
formId: 'role-create-form',
@@ -130,6 +131,7 @@ function buildRbacEditViewModel(role, message, currentUser, permissionGroups, us
return {
title: 'Edit role',
active: 'rbac',
userManagementMenuOpen: true,
isEdit: true,
usersPresent: true,
formId: 'role-edit-form',
+1
View File
@@ -6,6 +6,7 @@ module.exports = function renderRbacPage(data, message, currentUser) {
return renderView('settings/rbac/list', {
title: 'Roles and permissions',
active: 'rbac',
userManagementMenuOpen: true,
message: message,
currentUser: currentUser || null,
roles: data.roles || [],
+134 -9
View File
@@ -1,7 +1,7 @@
// 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 { AUDIT_CATEGORY_KEYS, AUDIT_CATEGORY_LABELS, SCREEN_CONTROL_COMMAND_KEYS, SCREEN_CONTROL_COMMAND_LABELS } = require('#src/data/audit-log');
const { fetchAppSettings, saveAppSettings } = require('#src/data/app-settings');
const { appVersion, detectSchemaVersion } = require('#src/db/migrations');
@@ -20,6 +20,7 @@ module.exports = function registerSettingsPageRoutes(app, deps) {
const pages = deps.pages;
const pool = deps.pool;
const recordRequestAuditEvent = deps.recordRequestAuditEvent;
const sendAccountEmail = deps.sendAccountEmail;
if (!pages || !pool || typeof deps.requirePermission !== 'function') {
throw new Error('registerSettingsPageRoutes requires pool, pages, and requirePermission.');
@@ -54,9 +55,12 @@ module.exports = function registerSettingsPageRoutes(app, deps) {
'security.password_require_number',
'security.password_require_symbol',
'security.require_password_change_for_new_users',
'security.require_password_change_after_admin_reset'
'security.require_password_change_after_admin_reset',
'security.allow_admin_email_verification_bypass'
],
audit: ['audit.enabled', 'audit.categories', 'audit.include_request_metadata', 'audit.retention_days'],
email: ['email.smtp_enabled', 'email.smtp_host', 'email.smtp_port', 'email.smtp_security', 'email.smtp_username', 'email.from_address', 'email.from_name'],
'email-templates': ['email.verification_subject', 'email.verification_body', 'email.verification_button_alignment', 'email.verification_button_text', 'email.reset_subject', 'email.reset_body', 'email.reset_button_alignment', 'email.reset_button_text', 'email.invitation_subject', 'email.invitation_body', 'email.invitation_button_alignment', 'email.invitation_button_text'],
audit: ['audit.enabled', 'audit.categories', 'audit.screen_control_commands', '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'],
weather: ['weather.open_meteo_api_key', 'weather.pirate_weather_api_key'],
icons: ['announcements.suggested_icons']
@@ -156,6 +160,27 @@ module.exports = function registerSettingsPageRoutes(app, deps) {
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']),
allowAdminEmailVerificationBypass: Boolean(settings['security.allow_admin_email_verification_bypass']),
smtpEnabled: Boolean(settings['email.smtp_enabled']),
smtpHost: String(settings['email.smtp_host'] || ''),
smtpPort: Number(settings['email.smtp_port']) || 587,
smtpSecurity: String(settings['email.smtp_security'] || 'starttls'),
smtpUsername: String(settings['email.smtp_username'] || ''),
smtpConfigured: Boolean(settings['email.smtp_password']),
emailFromAddress: String(settings['email.from_address'] || ''),
emailFromName: String(settings['email.from_name'] || ''),
verificationEmailSubject: String(settings['email.verification_subject'] || ''),
verificationEmailBody: String(settings['email.verification_body'] || ''),
verificationButtonAlignment: String(settings['email.verification_button_alignment'] || 'center'),
verificationButtonText: String(settings['email.verification_button_text'] || 'Verify email address'),
resetEmailSubject: String(settings['email.reset_subject'] || ''),
resetEmailBody: String(settings['email.reset_body'] || ''),
resetButtonAlignment: String(settings['email.reset_button_alignment'] || 'center'),
resetButtonText: String(settings['email.reset_button_text'] || 'Reset password'),
invitationEmailSubject: String(settings['email.invitation_subject'] || ''),
invitationEmailBody: String(settings['email.invitation_body'] || ''),
invitationButtonAlignment: String(settings['email.invitation_button_alignment'] || 'center'),
invitationButtonText: String(settings['email.invitation_button_text'] || 'Accept invitation'),
defaultSlideDurationSeconds: Number(settings['player.default_slide_duration_seconds']) || 10,
defaultFadeBetweenSlides: Boolean(settings['player.default_fade_between_slides']),
defaultSkipUnavailableRtmp: Boolean(settings['player.skip_unavailable_rtmp']),
@@ -176,7 +201,7 @@ module.exports = function registerSettingsPageRoutes(app, deps) {
,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) {
,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' && category !== 'weather' && category !== 'screen-controls'; }).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) {
@@ -185,9 +210,13 @@ module.exports = function registerSettingsPageRoutes(app, deps) {
,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) {
,auditDataSourceCategories: AUDIT_CATEGORY_KEYS.filter(function (category) { return category === 'api-sources' || category === 'rss-feeds' || category === 'timetables' || category === 'weather'; }).map(function (category) {
return { key: category, label: AUDIT_CATEGORY_LABELS[category] || category, isSelected: settings['audit.categories'].includes(category) };
})
,auditScreenControlCommands: SCREEN_CONTROL_COMMAND_KEYS.map(function (command) {
const selectedCommands = Array.isArray(settings['audit.screen_control_commands']) ? settings['audit.screen_control_commands'] : [];
return { key: command, label: SCREEN_CONTROL_COMMAND_LABELS[command] || command, isSelected: selectedCommands.includes(command) };
})
}, diagnostics));
}
@@ -244,7 +273,7 @@ module.exports = function registerSettingsPageRoutes(app, deps) {
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' && section !== 'weather') {
if (section !== 'icons' && section !== 'media' && section !== 'security' && section !== 'email' && section !== 'email-templates' && section !== 'defaults' && section !== 'audit' && section !== 'weather') {
const error = new Error('Unknown settings section.');
error.statusCode = 400;
error.expose = true;
@@ -252,6 +281,62 @@ module.exports = function registerSettingsPageRoutes(app, deps) {
}
const previousSettings = await fetchAppSettings(pool);
if (section === 'email' && String(req.body && req.body.settings_action || '').trim().toLowerCase() === 'test_email') {
const recipient = String(req.currentUser && req.currentUser.email || '').trim();
if (!previousSettings['email.smtp_enabled'] || typeof sendAccountEmail !== 'function') {
const error = new Error('Email delivery is not configured.');
error.statusCode = 400;
error.expose = true;
throw error;
}
if (!recipient) {
const error = new Error('Your account does not have an email address.');
error.statusCode = 400;
error.expose = true;
throw error;
}
await sendAccountEmail(previousSettings, {
to: recipient,
subject: 'Pulse Signage SMTP test email',
text: 'This is a test email from Pulse Signage. Your SMTP settings are working.'
});
return res.redirect('/settings/system?message=' + encodeURIComponent('Test email sent to ' + recipient + '.') + '#email-delivery');
}
if (section === 'email-templates') {
const verificationSubject = String(req.body && req.body.verification_subject || '').trim();
const verificationBody = String(req.body && req.body.verification_body || '').trim();
const resetSubject = String(req.body && req.body.reset_subject || '').trim();
const resetBody = String(req.body && req.body.reset_body || '').trim();
const verificationButtonText = String(req.body && req.body.verification_button_text || '').trim();
const resetButtonText = String(req.body && req.body.reset_button_text || '').trim();
const invitationSubject = String(req.body && req.body.invitation_subject || '').trim();
const invitationBody = String(req.body && req.body.invitation_body || '').trim();
const invitationButtonText = String(req.body && req.body.invitation_button_text || '').trim();
if (!verificationSubject || !verificationBody.includes('[[url]]') || !resetSubject || !resetBody.includes('[[url]]') || !invitationSubject || !invitationBody.includes('[[url]]') || !verificationButtonText || !resetButtonText || !invitationButtonText) {
const error = new Error('Email templates require a subject, message, and the correct link placeholder.');
error.statusCode = 400;
error.expose = true;
throw error;
}
const savedSettings = await saveAppSettings(pool, {
'email.verification_subject': verificationSubject,
'email.verification_body': verificationBody,
'email.verification_button_alignment': ['left', 'center', 'right'].includes(String(req.body && req.body.verification_button_alignment || '').trim()) ? String(req.body.verification_button_alignment).trim() : 'center',
'email.verification_button_text': verificationButtonText,
'email.reset_subject': resetSubject,
'email.reset_body': resetBody,
'email.reset_button_alignment': ['left', 'center', 'right'].includes(String(req.body && req.body.reset_button_alignment || '').trim()) ? String(req.body.reset_button_alignment).trim() : 'center',
'email.reset_button_text': resetButtonText
,'email.invitation_subject': invitationSubject
,'email.invitation_body': invitationBody
,'email.invitation_button_alignment': ['left', 'center', 'right'].includes(String(req.body && req.body.invitation_button_alignment || '').trim()) ? String(req.body.invitation_button_alignment).trim() : 'center'
,'email.invitation_button_text': invitationButtonText
}, req.currentUser && req.currentUser.id);
await recordSettingsAuditEvent(req, previousSettings, savedSettings, section);
return res.redirect('/settings/system?message=' + encodeURIComponent('Account email templates saved.') + '#email-templates');
}
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);
@@ -322,11 +407,41 @@ module.exports = function registerSettingsPageRoutes(app, deps) {
'security.password_require_symbol': passwordRequireSymbol,
'security.require_password_change_for_new_users': requirePasswordChangeForNewUsers,
'security.require_password_change_after_admin_reset': requirePasswordChangeAfterAdminReset
,'security.allow_admin_email_verification_bypass': req.body && (req.body.allow_admin_email_verification_bypass === '1' || req.body.allow_admin_email_verification_bypass === 'true')
}, 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 === 'email') {
const smtpEnabled = req.body && (req.body.smtp_enabled === '1' || req.body.smtp_enabled === 'true');
const smtpHost = String(req.body && req.body.smtp_host || '').trim();
const smtpPort = Number(req.body && req.body.smtp_port);
const smtpSecurity = String(req.body && req.body.smtp_security || 'starttls').trim().toLowerCase();
const smtpUsername = String(req.body && req.body.smtp_username || '').trim();
const smtpPassword = String(req.body && req.body.smtp_password || '');
const fromAddress = String(req.body && req.body.from_address || '').trim();
const fromName = String(req.body && req.body.from_name || '').trim();
if (smtpEnabled && (!smtpHost || !Number.isInteger(smtpPort) || smtpPort < 1 || smtpPort > 65535 || !fromAddress)) {
const error = new Error('Enabled email delivery requires an SMTP host, valid port, and from address.');
error.statusCode = 400;
error.expose = true;
throw error;
}
const savedSettings = await saveAppSettings(pool, {
'email.smtp_enabled': smtpEnabled,
'email.smtp_host': smtpHost,
'email.smtp_port': smtpPort || 587,
'email.smtp_security': smtpSecurity,
'email.smtp_username': smtpUsername,
'email.smtp_password': smtpPassword || previousSettings['email.smtp_password'],
'email.from_address': fromAddress,
'email.from_name': fromName
}, req.currentUser && req.currentUser.id);
await recordSettingsAuditEvent(req, previousSettings, savedSettings, section);
return res.redirect('/settings/system?message=' + encodeURIComponent('Email settings saved.') + '#email-delivery');
}
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');
@@ -334,8 +449,17 @@ module.exports = function registerSettingsPageRoutes(app, deps) {
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);
return AUDIT_CATEGORY_KEYS.includes(category) && category !== 'screen-controls';
})));
const selectedScreenControlCommands = req.body && (req.body['audit_screen_control_commands[]'] !== undefined ? req.body['audit_screen_control_commands[]'] : req.body.audit_screen_control_commands);
const screenControlCommands = Array.from(new Set((Array.isArray(selectedScreenControlCommands) ? selectedScreenControlCommands : selectedScreenControlCommands ? [selectedScreenControlCommands] : []).map(function (command) {
return String(command || '').trim().toLowerCase();
}).filter(function (command) {
return SCREEN_CONTROL_COMMAND_KEYS.includes(command);
})));
if (screenControlCommands.length) {
auditCategories.push('screen-controls');
}
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.');
@@ -346,6 +470,7 @@ module.exports = function registerSettingsPageRoutes(app, deps) {
const savedSettings = await saveAppSettings(pool, {
'audit.enabled': auditEnabled,
'audit.categories': auditCategories,
'audit.screen_control_commands': screenControlCommands,
'audit.include_request_metadata': includeRequestMetadata,
'audit.retention_days': retentionDays
}, req.currentUser && req.currentUser.id);
@@ -371,7 +496,7 @@ module.exports = function registerSettingsPageRoutes(app, deps) {
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', 'weather.open_meteo_api_key', 'weather.pirate_weather_api_key', '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']);
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', 'weather.open_meteo_api_key', 'weather.pirate_weather_api_key', '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', 'security.allow_admin_email_verification_bypass', 'email.smtp_enabled', 'email.smtp_host', 'email.smtp_port', 'email.smtp_security', 'email.smtp_username', 'email.smtp_password', 'email.from_address', 'email.from_name', 'email.reply_to', 'audit.enabled', 'audit.categories', 'audit.screen_control_commands', '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,
@@ -408,7 +533,7 @@ module.exports = function registerSettingsPageRoutes(app, deps) {
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', 'weather.open_meteo_api_key', 'weather.pirate_weather_api_key', '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']);
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', 'weather.open_meteo_api_key', 'weather.pirate_weather_api_key', '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.screen_control_commands', 'audit.include_request_metadata', 'audit.retention_days']);
const savedSettings = await saveAppSettings(pool, {
'announcements.suggested_icons': suggestedIcons
}, req.currentUser && req.currentUser.id);
@@ -22,6 +22,7 @@ function buildUsersAddViewModel(message, currentUser, roles, formValues, message
return {
title: 'Add user',
active: 'users',
userManagementMenuOpen: true,
isEdit: false,
formId: 'user-form',
formAction: '/settings/users',
@@ -44,6 +45,7 @@ function buildUsersEditViewModel(user, message, currentUser, roles, sessions) {
return {
title: 'Edit user',
active: 'users',
userManagementMenuOpen: true,
isEdit: true,
formId: 'user-profile-form',
formAction: '/settings/users/' + user.id + '/username',
+14
View File
@@ -0,0 +1,14 @@
const { renderView } = require('../../../view');
module.exports = function renderUsersInvitePage(message, currentUser, roles, formValues, messageVariant) {
return renderView('settings/users/invite', {
title: 'Invite user',
active: 'users',
userManagementMenuOpen: true,
currentUser: currentUser || null,
message: message || '',
messageVariant: messageVariant || 'success',
roles: Array.isArray(roles) ? roles : [],
formValues: formValues || {}
});
};
+1
View File
@@ -6,6 +6,7 @@ module.exports = function renderUsersPage(data, message, currentUser) {
return renderView('settings/users/list', {
title: 'Users',
active: 'users',
userManagementMenuOpen: true,
message: message,
currentUser: currentUser || null,
users: data.users || [],