Release 2.8.0
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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 };
|
||||
@@ -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));
|
||||
|
||||
|
||||
@@ -198,7 +198,6 @@ module.exports = {
|
||||
getAuditUserId: getAuditUserId,
|
||||
getCanvasSignature: getCanvasSignature,
|
||||
fetchPlaylistCanvasId: fetchPlaylistCanvasId,
|
||||
fetchPlaylistCanvasSignature: fetchPlaylistCanvasId,
|
||||
fetchScreensByPlaylistId: fetchScreensByPlaylistId,
|
||||
fetchScreensBySlideId: fetchScreensBySlideId,
|
||||
fetchScreensByTemplateId: fetchScreensByTemplateId,
|
||||
|
||||
Reference in New Issue
Block a user