release: v2.11.2
Publish Docker Image / build-and-push-existing-registry (./build/Dockerfile, web, pulse-signage-web) (push) Successful in 2m0s
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-05 18:14:38 +01:00
parent 7fec2154e0
commit c4ae69d25f
40 changed files with 314 additions and 352 deletions
+12 -4
View File
@@ -1,10 +1,17 @@
const DEFAULT_ACCOUNT_EMAIL_TEMPLATES = {
verificationSubject: 'Verify your Pulse Signage email address',
verificationBody: 'Hello [[display_name]]!\n\nConfirm this email address:\n\n[[action_button]]\n\nThis link expires in 30 minutes.',
verificationBody: 'Hello [b][[display_name]][/b]!\n[[action_button]]\nThis [u]link[/u] expires in [i][[expiry_time]][/i].\nConfirm this email address: [[url]]',
resetSubject: 'Reset your Pulse Signage password',
resetBody: 'Hello [[display_name]]!\n\nUse this link to choose a new password:\n\n[[action_button]]\n\nThis link expires in 30 minutes.'
resetBody: 'Hello [b][[display_name]][/b]!\n[[action_button]]\nThis [u]link[/u] expires in [i][[expiry_time]][/i].\nChoose a new password: [[url]]'
};
function formatAccountEmailExpiry(value, unit) {
const amount = Number(value);
const normalizedAmount = Number.isFinite(amount) && amount > 0 ? Math.round(amount) : 30;
const normalizedUnit = unit === 'hours' ? 'hour' : 'minute';
return normalizedAmount + ' ' + normalizedUnit + (normalizedAmount === 1 ? '' : 's');
}
function renderAccountEmailTemplate(subject, body, variables) {
const values = variables || {};
const replaceVariables = function (value) {
@@ -22,10 +29,11 @@ function renderAccountEmailTemplate(subject, body, variables) {
const actionButton = actionUrl ? '<a href="' + actionUrl + '" style="display:inline-block;background:#111827;color:#ffffff;padding:12px 22px;text-decoration:none;border-radius:4px;font-weight:600;">' + actionLabel + '</a>' : '';
const actionBlock = actionButton ? '<div style="margin:24px 0;text-align:' + actionAlignment + ';">' + actionButton + '</div>' : '';
const plainLink = actionUrl ? '<a href="' + actionUrl + '">' + actionUrl + '</a>' : '';
const escapedBodyUrl = values.url ? String(values.url).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;') : '';
const hasButtonPlaceholder = escapedBody.indexOf('[[action_button]]') !== -1;
const hasUrlPlaceholder = String(body || '').indexOf('[[url]]') !== -1;
const bodyWithAction = hasButtonPlaceholder
? escapedBody.replace(/\[\[action_button\]\]/g, actionBlock)
? escapedBody.split(escapedBodyUrl).join(plainLink).replace(/\[\[action_button\]\]/g, actionBlock).replace(/\[\[action_link\]\]/g, plainLink)
: hasUrlPlaceholder
? escapedBody.split(actionUrl).join(actionBlock + plainLink)
: escapedBody.replace(/\[\[action_link\]\]/g, plainLink).replace(actionUrl, plainLink);
@@ -43,4 +51,4 @@ function renderAccountEmailTemplate(subject, body, variables) {
return { subject: renderedSubject, text: plainText, html: html };
}
module.exports = { DEFAULT_ACCOUNT_EMAIL_TEMPLATES, renderAccountEmailTemplate };
module.exports = { DEFAULT_ACCOUNT_EMAIL_TEMPLATES, formatAccountEmailExpiry, renderAccountEmailTemplate };
+6 -3
View File
@@ -30,16 +30,19 @@ const SETTING_DEFINITIONS = [
{ key: 'email.smtp_password', type: 'string', defaultValue: '' },
{ key: 'email.from_address', type: 'string', defaultValue: '' },
{ key: 'email.from_name', type: 'string', defaultValue: 'Pulse Signage' },
{ key: 'email.verification_expiry_minutes', type: 'integer', min: 1, defaultValue: 30 },
{ key: 'email.reset_expiry_minutes', type: 'integer', min: 1, defaultValue: 30 },
{ key: 'email.invitation_expiry_hours', type: 'integer', min: 1, defaultValue: 24 },
{ key: 'email.verification_subject', type: 'string', defaultValue: 'Verify your Pulse Signage email address' },
{ key: 'email.verification_body', type: 'string', defaultValue: 'Hello [[display_name]]!\n\nConfirm this email address:\n\n[[action_button]]\n\nThis link expires in 30 minutes.' },
{ key: 'email.verification_body', type: 'string', defaultValue: 'Hello [b][[display_name]][/b]!\n[[action_button]]\nThis [u]link[/u] expires in [i][[expiry_time]][/i].\nConfirm this email address: [[url]]' },
{ key: 'email.reset_subject', type: 'string', defaultValue: 'Reset your Pulse Signage password' },
{ key: 'email.reset_body', type: 'string', defaultValue: 'Hello [[display_name]]!\n\nUse this link to choose a new password:\n\n[[action_button]]\n\nThis link expires in 30 minutes.' },
{ key: 'email.reset_body', type: 'string', defaultValue: 'Hello [b][[display_name]][/b]!\n[[action_button]]\nThis [u]link[/u] expires in [i][[expiry_time]][/i].\nChoose a new password: [[url]]' },
{ key: 'email.verification_button_alignment', type: 'enum', values: ['left', 'center', 'right'], defaultValue: 'center' },
{ key: 'email.verification_button_text', type: 'string', defaultValue: 'Verify email address' },
{ key: 'email.reset_button_alignment', type: 'enum', values: ['left', 'center', 'right'], defaultValue: 'center' },
{ key: 'email.reset_button_text', type: 'string', defaultValue: 'Reset password' },
{ key: 'email.invitation_subject', type: 'string', defaultValue: 'You have been invited to Pulse Signage' },
{ key: 'email.invitation_body', type: 'string', defaultValue: 'Hello [[display_name]]!\n\nYou have been invited to create a Pulse Signage account.\n\n[[action_button]]\n\nThis invitation expires in 24 hours.' },
{ key: 'email.invitation_body', type: 'string', defaultValue: 'Hello [b][[display_name]][/b]!\n[[action_button]]\nThis [u]invitation link[/u] expires in [i][[expiry_time]][/i].\nCreate your account: [[url]]' },
{ key: 'email.invitation_button_alignment', type: 'enum', values: ['left', 'center', 'right'], defaultValue: 'center' },
{ key: 'email.invitation_button_text', type: 'string', defaultValue: 'Accept invitation' },
{ key: 'audit.enabled', type: 'boolean', defaultValue: true },
+22
View File
@@ -50,6 +50,27 @@ const SCREEN_CONTROL_COMMAND_LABELS = Object.freeze({
});
const { fetchAppSettings } = require('./app-settings');
function formatUserAgentLabel(userAgent) {
const value = String(userAgent || '').trim();
if (!value) return '';
const browserMatch = value.match(/(?:Edg|OPR|Chrome|Firefox|Version|Electron)\/([\d.]+)/i);
let browser = '';
if (/Edg\//i.test(value)) browser = 'Edge';
else if (/OPR\//i.test(value)) browser = 'Opera';
else if (/Electron\//i.test(value)) browser = 'Electron';
else if (/Chrome\//i.test(value)) browser = 'Chrome';
else if (/Firefox\//i.test(value)) browser = 'Firefox';
else if (/Version\/.*Safari\//i.test(value)) browser = 'Safari';
const browserLabel = browserMatch && browser ? browser + ' ' + browserMatch[1] : browser;
let operatingSystem = '';
if (/Windows NT/i.test(value)) operatingSystem = 'Windows';
else if (/Macintosh|Mac OS X/i.test(value)) operatingSystem = 'macOS';
else if (/Android/i.test(value)) operatingSystem = 'Android';
else if (/iPhone|iPad|iPod/i.test(value)) operatingSystem = 'iOS';
else if (/Linux/i.test(value)) operatingSystem = 'Linux';
return [browserLabel, operatingSystem].filter(Boolean).join(' on ') || value;
}
function normalizeDetails(details) {
if (details === undefined || details === null) {
return null;
@@ -136,6 +157,7 @@ module.exports = {
AUDIT_CATEGORY_LABELS,
SCREEN_CONTROL_COMMAND_KEYS,
SCREEN_CONTROL_COMMAND_LABELS,
formatUserAgentLabel,
getRequestMetadata,
buildAuditChanges,
recordAuditEvent,