Release 2.8.0
Publish Docker Image / build-and-push-existing-registry (./build/Dockerfile, web, pulse-signage-web) (push) Successful in 1m49s
Publish Docker Image / build-and-push-existing-registry (./build/Dockerfile.player, player, pulse-signage-player) (push) Successful in 32s

This commit is contained in:
2026-08-16 17:15:31 +01:00
parent 1bdc122995
commit 203d0bfc01
105 changed files with 4185 additions and 677 deletions
+15 -3
View File
@@ -113,7 +113,7 @@ async function fetchRolesForUser(pool, userId) {
async function fetchUsersWithRoles(pool) {
const [rows] = await pool.query(
`SELECT u.id, u.name, u.username, u.created_at, u.modified_at,
`SELECT u.id, u.name, u.username, u.account_locked, u.created_at, u.modified_at,
COALESCE(role_data.role_names, '') AS role_names,
COALESCE(role_data.role_ids_csv, '') AS role_ids_csv
FROM a_users u
@@ -142,7 +142,7 @@ async function fetchUsersWithRolesPage(pool, page, pageSize, searchTerm, sortKey
const whereSql = hasExcludedUserId ? 'WHERE u.id <> ?' : '';
const queryArgs = hasExcludedUserId ? [excludedUserId] : [];
const paged = await fetchPagedRows(pool, {
selectSql: `SELECT u.id, u.name, u.username, u.created_at, u.modified_at,
selectSql: `SELECT u.id, u.name, u.username, u.account_locked, u.created_at, u.modified_at,
COALESCE(role_data.role_names, '') AS role_names,
COALESCE(role_data.role_ids_csv, '') AS role_ids_csv
FROM a_users u
@@ -189,7 +189,7 @@ async function fetchUsersWithRolesPage(pool, page, pageSize, searchTerm, sortKey
async function fetchUserWithRoles(pool, userId) {
const [rows] = await pool.query(
`SELECT u.id, u.name, u.username, u.created_at, u.modified_at,
`SELECT u.id, u.name, u.username, u.account_locked, u.created_at, u.modified_at,
COALESCE(role_data.role_names, '') AS role_names,
COALESCE(role_data.role_ids_csv, '') AS role_ids_csv
FROM a_users u
@@ -216,6 +216,17 @@ async function fetchUserWithRoles(pool, userId) {
});
}
async function fetchActiveUserSessions(pool, userId) {
const [rows] = await pool.query(
`SELECT id, ip_address, user_agent, created_at, last_used_at, expires_at
FROM a_sessions
WHERE user_id = ? AND expires_at > NOW()
ORDER BY last_used_at DESC`,
[userId]
);
return rows || [];
}
async function syncUserRoles(pool, userId, roleIds) {
const uniqueRoleIds = Array.from(new Set((Array.isArray(roleIds) ? roleIds : []).map(function (roleId) {
return Number(roleId);
@@ -273,6 +284,7 @@ module.exports = {
fetchUsersWithRoles,
fetchUsersWithRolesPage,
fetchUserWithRoles,
fetchActiveUserSessions,
syncUserRoles,
syncRoleUsers,
syncRolePermissions
+43 -6
View File
@@ -34,6 +34,8 @@ function normalizeReturnToPath(value, baseUrl) {
function createSessionService(options) {
const sessionCookieName = String(options && options.sessionCookieName || '').trim();
const sessionMaxAgeMs = Number(options && options.sessionMaxAgeMs);
const getConfiguredSessionMaxAgeMs = options && options.getConfiguredSessionMaxAgeMs;
const getConfiguredMaxActiveSessions = options && options.getConfiguredMaxActiveSessions;
const hashSessionToken = options && options.hashSessionToken;
const createSessionToken = options && options.createSessionToken;
const authMessageCookieName = 'pulse_auth_message';
@@ -88,7 +90,20 @@ function createSessionService(options) {
}
function setSessionCookie(res, token) {
appendCookieHeader(res, serializeCookie(sessionCookieName, token, { maxAge: sessionMaxAgeMs }));
const hasRequestedMaxAge = arguments.length > 2;
const requestedMaxAgeMs = hasRequestedMaxAge ? Number(arguments[2]) : sessionMaxAgeMs;
const cookieOptions = hasRequestedMaxAge && arguments[2] === null
? {}
: { maxAge: Number.isFinite(requestedMaxAgeMs) && requestedMaxAgeMs > 0 ? requestedMaxAgeMs : sessionMaxAgeMs };
appendCookieHeader(res, serializeCookie(sessionCookieName, token, cookieOptions));
}
async function getSessionMaxAgeMs() {
const configuredValue = typeof getConfiguredSessionMaxAgeMs === 'function'
? await getConfiguredSessionMaxAgeMs()
: sessionMaxAgeMs;
const normalizedValue = Number(configuredValue);
return Number.isFinite(normalizedValue) && normalizedValue > 0 ? normalizedValue : sessionMaxAgeMs;
}
function setAuthMessageCookie(res, message) {
@@ -113,7 +128,7 @@ function createSessionService(options) {
const tokenHash = hashSessionToken(token);
const [rows] = await pool.query(
`SELECT s.user_id, u.id, u.name, u.username
`SELECT s.user_id, u.id, u.name, u.username, u.must_change_password
FROM a_sessions s
JOIN a_users u ON u.id = s.user_id
WHERE s.session_hash = ?
@@ -146,6 +161,7 @@ function createSessionService(options) {
await pool.query('UPDATE a_sessions SET last_used_at = CURRENT_TIMESTAMP, modified_by = ? WHERE session_hash = ?', [rows[0].user_id, tokenHash]);
return Object.assign({}, rows[0], {
mustChangePassword: Boolean(rows[0].must_change_password),
roleKeys: roleRows.map(function (row) {
return String(row.role_key || '').trim();
}).filter(Boolean),
@@ -155,14 +171,34 @@ function createSessionService(options) {
});
}
async function createUserSession(pool, userId) {
async function createUserSession(pool, userId, configuredMaxAgeMs, metadata) {
const token = createSessionToken();
const tokenHash = hashSessionToken(token);
const expiresAt = new Date(Date.now() + sessionMaxAgeMs);
const maxAgeMs = Number.isFinite(Number(configuredMaxAgeMs)) && Number(configuredMaxAgeMs) > 0
? Number(configuredMaxAgeMs)
: await getSessionMaxAgeMs();
const expiresAt = new Date(Date.now() + maxAgeMs);
const sessionMetadata = metadata && typeof metadata === 'object' ? metadata : {};
await pool.query(
'INSERT INTO a_sessions (session_hash, user_id, expires_at, created_by, modified_by) VALUES (?, ?, ?, ?, ?)',
[tokenHash, userId, expiresAt, userId, userId]
'INSERT INTO a_sessions (session_hash, user_id, ip_address, user_agent, expires_at, created_by, modified_by) VALUES (?, ?, ?, ?, ?, ?, ?)',
[tokenHash, userId, String(sessionMetadata.ipAddress || '').slice(0, 255) || null, String(sessionMetadata.userAgent || '').slice(0, 512) || null, expiresAt, userId, userId]
);
if (typeof getConfiguredMaxActiveSessions === 'function') {
const maxActiveSessions = Number(await getConfiguredMaxActiveSessions());
if (Number.isInteger(maxActiveSessions) && maxActiveSessions > 0) {
const [sessionRows] = await pool.query(
'SELECT id, session_hash FROM a_sessions WHERE user_id = ? AND expires_at > NOW() ORDER BY last_used_at ASC, created_at ASC, id ASC',
[userId]
);
const sessionsToRemove = (sessionRows || []).filter(function (session) {
return session.session_hash !== tokenHash;
}).slice(0, Math.max(0, sessionRows.length - maxActiveSessions));
for (const session of sessionsToRemove) {
await pool.query('DELETE FROM a_sessions WHERE id = ? AND user_id = ?', [session.id, userId]);
}
}
}
return token;
}
@@ -184,6 +220,7 @@ function createSessionService(options) {
consumeAuthMessageCookie: consumeAuthMessageCookie,
loadCurrentUser: loadCurrentUser,
createUserSession: createUserSession,
getSessionMaxAgeMs: getSessionMaxAgeMs,
requireAuth: requireAuth,
getRequestOrigin: getRequestOrigin
};
@@ -0,0 +1,34 @@
const { fetchAppSettings } = require('#src/data/app-settings');
const TASK = {
key: 'audit-log-sweep',
title: 'Audit log cleanup',
category: 'cleanup',
intervalMs: 24 * 60 * 60 * 1000
};
function registerAuditLogSweepTask(options) {
const backgroundTaskQueue = options && options.backgroundTaskQueue;
const pool = options && options.pool;
if (!backgroundTaskQueue || !pool) {
throw new Error('registerAuditLogSweepTask requires audit cleanup dependencies.');
}
backgroundTaskQueue.registerRecurringTask({
key: TASK.key,
title: TASK.title,
category: TASK.category,
intervalMs: TASK.intervalMs,
metadata: {},
run: async function () {
const settings = await fetchAppSettings(pool);
const retentionDays = Number(settings['audit.retention_days']);
if (!Number.isInteger(retentionDays) || retentionDays <= 0) {
return;
}
const cutoff = new Date(Date.now() - retentionDays * 24 * 60 * 60 * 1000);
await pool.query('DELETE FROM o_audit_events WHERE occurred_at < ?', [cutoff]);
}
});
}
module.exports = { registerAuditLogSweepTask };
@@ -0,0 +1,31 @@
const TASK = {
key: 'expired-session-sweep',
title: 'Expired session cleanup',
category: 'cleanup',
trigger: 'scheduled recurring task, hourly',
purpose: 'remove expired authentication sessions and their metadata.',
taskType: 'recurring-run',
intervalMs: 60 * 60 * 1000
};
function registerExpiredSessionSweepTask(options) {
const backgroundTaskQueue = options && options.backgroundTaskQueue;
const pool = options && options.pool;
if (!backgroundTaskQueue || !pool) {
throw new Error('registerExpiredSessionSweepTask requires the session cleanup dependencies.');
}
backgroundTaskQueue.registerRecurringTask({
key: TASK.key,
title: TASK.title,
category: TASK.category,
intervalMs: TASK.intervalMs,
metadata: {},
run: async function () {
await pool.query('DELETE FROM a_sessions WHERE expires_at <= NOW()');
}
});
}
module.exports = { registerExpiredSessionSweepTask };
+1 -2
View File
@@ -9,8 +9,7 @@ function createWebConfig() {
const bridgeInternalUrl = (process.env.BRIDGE_INTERNAL_URL || 'http://player-bridge:8090').replace(/\/$/, '');
const webInternalUrl = (process.env.WEB_INTERNAL_URL || `http://127.0.0.1:${Number(process.env.WEB_PORT || 8080)}`).replace(/\/$/, '');
const sessionCookieName = 'digital_signage_session';
const sessionMaxAgeDays = Number(process.env.SESSION_MAX_AGE_DAYS || 14);
const sessionMaxAgeMs = (Number.isFinite(sessionMaxAgeDays) && sessionMaxAgeDays > 0 ? sessionMaxAgeDays : 14) * 24 * 60 * 60 * 1000;
const sessionMaxAgeMs = 14 * 24 * 60 * 60 * 1000;
const port = Number(process.env.WEB_PORT || 8080);
const dataSourceStartupRefreshStaggerMs = Math.max(100, Number(process.env.DATA_SOURCE_STARTUP_REFRESH_STAGGER_MS || 250));
-1
View File
@@ -198,7 +198,6 @@ module.exports = {
getAuditUserId: getAuditUserId,
getCanvasSignature: getCanvasSignature,
fetchPlaylistCanvasId: fetchPlaylistCanvasId,
fetchPlaylistCanvasSignature: fetchPlaylistCanvasId,
fetchScreensByPlaylistId: fetchScreensByPlaylistId,
fetchScreensBySlideId: fetchScreensBySlideId,
fetchScreensByTemplateId: fetchScreensByTemplateId,