Files
pulse-signage/src/web.js
T
lzstealth 0a03cdd0b7
Publish Docker Image / build-and-push-existing-registry (./build/Dockerfile, web, pulse-signage-web) (push) Successful in 2m6s
Publish Docker Image / build-and-push-existing-registry (./build/Dockerfile.player, player, pulse-signage-player) (push) Successful in 33s
Release v2.12.0
2026-09-11 17:22:31 +01:00

265 lines
11 KiB
JavaScript

// Main web application bootstrap, route wiring, and lifecycle setup.
const express = require('express');
const http = require('http');
const multer = require('multer');
const fs = require('fs');
const crypto = require('crypto');
const common = require('./common');
const { verifyPassword, createSessionToken, hashSessionToken, hashPassword, validatePasswordStrength, createOneTimeToken } = require('#src/auth');
const pages = require('#src/web/pages');
const registerMiddleware = require('#src/web/middleware');
const registerNotFoundHandler = require('#src/web/middleware/not-found');
const { createBackgroundTaskQueue } = require('#src/web/lib/background-tasks/queue');
const { initializeBackgroundTasks } = require('#src/web/lib/background-tasks');
const { createDataSourceTaskService } = require('#src/web/lib/background-tasks/tasks-scheduled/data-source-refresh');
const { captureSlideThumbnail } = require('#src/web/lib/media');
const { createWebBootstrap } = require('#src/web/bootstrap');
const { registerRoutes } = require('#src/web/routes/register');
const { createNotifyPlayerScreens } = require('#src/web/lib/notify-player-screens');
const { initializeWebServer } = require('#src/web/lib/web-startup');
const { createWebConfig } = require('#src/web/lib/config');
const { requirePermission: createRequirePermission, PERMISSION_DENIED_MESSAGE } = require('#src/rbac');
const { rbacData } = require('#src/web/lib/auth');
const { createPlayerActionService } = require('#src/web/lib/player-actions');
const { isClientNameAvailable, withClientNameReservation } = require('#src/data/client-name-check');
const { createSessionService, getRequestOrigin } = require('#src/web/lib/auth');
const { fetchAppSettings } = require('#src/data/app-settings');
const { sendAccountEmail } = require('#src/data/mailer');
const { recordRequestAuditEvent } = require('#src/data/audit-log');
const { hasAnyPermission } = require('#src/rbac');
const { ensureFontLibrary } = require('#src/web/lib/media/font-library');
const {
formatDashboardDate,
readArrayField,
parseDateTimeLocal,
parseTimeLocal,
normalizeScheduleMode,
getAuditUserId,
getCanvasSignature,
fetchPlaylistCanvasId,
fetchScreensByPlaylistId,
fetchScreensBySlideId,
fetchScreensByTemplateId,
fetchOrderedPlaylistSlides,
redirectAfterSave
} = require('#src/web/lib/helpers');
async function start() {
// Core app and filesystem setup.
const app = express();
const server = http.createServer(app);
const pool = common.createPool();
const webConfig = createWebConfig();
// Background services and request forwarding helpers.
const backgroundTaskQueue = createBackgroundTaskQueue({
pool: pool,
maxConcurrent: 1
});
const dataSourceTasks = createDataSourceTaskService({
pool: pool,
common: common,
backgroundTaskQueue: backgroundTaskQueue
});
const playerActionService = createPlayerActionService({
pool: pool,
common: common,
playerInternalBaseUrl: webConfig.playerInternalUrl,
bridgeInternalBaseUrl: webConfig.bridgeInternalUrl
});
const notifyPlayerScreens = createNotifyPlayerScreens(
playerActionService.forwardPlayerCommand,
playerActionService.getScreenConnections,
playerActionService.forwardPlayerCommandToBaseUrl,
playerActionService.resolvePlayerBaseUrlForPublicUrl,
playerActionService.forwardPlayerCommandToDevice
);
// Centralized dashboard/player bootstrap.
const webBootstrap = createWebBootstrap({
pool: pool,
common: common,
playerInternalBaseUrl: webConfig.playerInternalUrl,
bridgeInternalBaseUrl: webConfig.bridgeInternalUrl,
uploadDir: webConfig.uploadsDir,
formatDashboardDate: formatDashboardDate,
notifyPlayerScreens: notifyPlayerScreens,
backgroundTaskQueue: backgroundTaskQueue,
hasAnyPermission: hasAnyPermission,
});
const upload = webBootstrap.upload;
const collectUploadReferencesFromSlide = webBootstrap.collectUploadReferencesFromSlide;
const collectUploadReferencesFromTemplate = webBootstrap.collectUploadReferencesFromTemplate;
const collectUploadReferencesFromPayload = webBootstrap.collectUploadReferencesFromPayload;
const removeUnusedUploadFiles = webBootstrap.removeUnusedUploadFiles;
const syncPlaylistUploadsOnChange = webBootstrap.syncPlaylistUploadsOnChange;
// Background task initialization.
// Session and auth helpers for route registration.
const broadcastDashboardState = webBootstrap.broadcastDashboardState;
const sessionService = createSessionService({
sessionCookieName: webConfig.sessionCookieName,
sessionMaxAgeMs: webConfig.sessionMaxAgeMs,
getConfiguredSessionMaxAgeMs: async function () {
const settings = await fetchAppSettings(pool);
return Number(settings['security.session_lifetime_days']) * 24 * 60 * 60 * 1000;
},
getConfiguredMaxActiveSessions: async function () {
const settings = await fetchAppSettings(pool);
return Number(settings['security.max_active_sessions']);
},
hashSessionToken: hashSessionToken,
createSessionToken: createSessionToken
});
const parseCookies = sessionService.parseCookies;
const createUserSession = sessionService.createUserSession;
const clearSessionCookie = sessionService.clearSessionCookie;
const setSessionCookie = sessionService.setSessionCookie;
const getSessionMaxAgeMs = sessionService.getSessionMaxAgeMs;
const setAuthMessageCookie = sessionService.setAuthMessageCookie;
const consumeAuthMessageCookie = sessionService.consumeAuthMessageCookie;
const loadCurrentUser = sessionService.loadCurrentUser;
const requireAuth = sessionService.requireAuth;
// Middleware and route registration.
registerMiddleware(app, {
pool: pool,
loadCurrentUser: loadCurrentUser,
requireAuth: requireAuth,
MEDIA_DIR: webConfig.mediaDir,
UPLOADS_DIR: webConfig.uploadsDir,
THUMBNAILS_DIR: webConfig.thumbnailsDir,
ASSET_DIR: webConfig.assetDir
});
registerRoutes(app, {
pool: pool,
common: common,
pages: pages,
upload: upload,
mediaDir: webConfig.mediaDir,
uploadDir: webConfig.uploadsDir,
createUserSession: createUserSession,
setSessionCookie: setSessionCookie,
getSessionMaxAgeMs: getSessionMaxAgeMs,
clearSessionCookie: clearSessionCookie,
setAuthMessageCookie: setAuthMessageCookie,
consumeAuthMessageCookie: consumeAuthMessageCookie,
parseCookies: parseCookies,
hashSessionToken: hashSessionToken,
verifyPassword: verifyPassword,
sessionCookieName: webConfig.sessionCookieName,
formatDashboardDate: formatDashboardDate,
getAuditUserId: getAuditUserId,
getRequestOrigin: getRequestOrigin,
recordRequestAuditEvent: recordRequestAuditEvent,
sendAccountEmail: function (settings, message) { return sendAccountEmail(settings, message); },
createOneTimeToken: createOneTimeToken,
hashPassword: hashPassword,
validatePasswordStrength: validatePasswordStrength,
readArrayField: readArrayField,
rbacData: rbacData,
fetchOrderedPlaylistSlides: fetchOrderedPlaylistSlides,
fetchScreensByPlaylistId: fetchScreensByPlaylistId,
fetchScreensBySlideId: fetchScreensBySlideId,
fetchScreensByTemplateId: fetchScreensByTemplateId,
fetchPlaylistCanvasId: fetchPlaylistCanvasId,
getCanvasSignature: getCanvasSignature,
normalizeScheduleMode: normalizeScheduleMode,
parseDateTimeLocal: parseDateTimeLocal,
parseTimeLocal: parseTimeLocal,
redirectAfterSave: redirectAfterSave,
notifyPlayerScreens: notifyPlayerScreens,
broadcastDashboardState: broadcastDashboardState,
dataSourceTasks: dataSourceTasks,
playerActionService: playerActionService,
playerInternalBaseUrl: webConfig.playerInternalUrl,
isClientNameAvailable: isClientNameAvailable,
findAvailableClientName: common.findAvailableClientName,
withClientNameReservation: withClientNameReservation,
requirePermission: function (permissionKey, options) {
return createRequirePermission(permissionKey, Object.assign({ setAuthMessageCookie: setAuthMessageCookie }, options));
},
hasAnyPermission: hasAnyPermission,
backgroundTaskQueue: backgroundTaskQueue,
uploadSyncService: webBootstrap.uploadSyncService,
collectUploadReferencesFromSlide: collectUploadReferencesFromSlide,
collectUploadReferencesFromTemplate: collectUploadReferencesFromTemplate,
collectUploadReferencesFromPayload: collectUploadReferencesFromPayload,
removeUnusedUploadFiles: removeUnusedUploadFiles,
syncPlaylistUploadsOnChange: syncPlaylistUploadsOnChange,
buildDashboardState: webBootstrap.buildDashboardState
});
registerNotFoundHandler(app);
app.use(function (error, req, res, _next) {
const statusCode = Number(error && (error.statusCode || error.status)) || 500;
if (statusCode >= 500) {
console.error(error);
} else if (statusCode >= 400 && statusCode !== 404) {
console.warn(error && error.message ? error.message : 'Request failed.', { statusCode: statusCode });
}
const isXhr = String(req.get && req.get('X-Requested-With') || '').toLowerCase() === 'xmlhttprequest';
const wantsHtml = !isXhr && !String(req.originalUrl || '').startsWith('/api/') && (!req.accepts || req.accepts('html'));
if (wantsHtml && pages.renderErrorPage) {
const isPermissionError = statusCode === 403;
const message = isPermissionError
? String(error && error.message ? error.message : PERMISSION_DENIED_MESSAGE)
: String(error && error.message ? error.message : 'An unexpected error occurred.');
const title = isPermissionError
? 'Access denied'
: statusCode === 404
? 'Not found'
: 'Something went wrong';
return res.status(statusCode).send(pages.renderErrorPage({
statusCode: statusCode,
title: title,
errorTitle: title,
message: message,
detail: statusCode >= 500 ? 'The server could not complete the request.' : '',
backUrl: req.currentUser ? '/dashboard' : '/login',
backLabel: req.currentUser ? 'Back to dashboard' : 'Sign in'
}, req.currentUser));
}
res.status(statusCode).send(statusCode >= 500 ? 'Internal server error' : String(error && error.message ? error.message : 'Error'));
});
// Bootstrap and startup tasks.
fs.mkdirSync(webConfig.mediaDir, { recursive: true });
await ensureFontLibrary(webConfig.mediaDir);
await initializeWebServer({
common: common,
pool: pool,
mediaDir: webConfig.mediaDir,
backgroundTaskQueue: backgroundTaskQueue,
webBootstrap: webBootstrap,
notifyPlayerScreens: notifyPlayerScreens,
forwardPlayerCommandToDevice: playerActionService.forwardPlayerCommandToDevice,
loadCurrentUser: loadCurrentUser,
initializeBackgroundTasks: initializeBackgroundTasks,
captureSlideThumbnail: captureSlideThumbnail,
server: server,
webBaseUrl: webConfig.webInternalUrl,
localPlayerInternalUrl: webConfig.playerInternalUrl,
dataSourceStartupRefreshStaggerMs: webConfig.dataSourceStartupRefreshStaggerMs
});
server.listen(webConfig.port, function () {
console.log(`Pulse Signage app listening on port ${webConfig.port}`);
});
}
module.exports = { start };
if (require.main === module) {
start().catch(function (error) {
console.error(error);
process.exit(1);
});
}