// 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 } = require('#src/auth'); const pages = require('#src/web/pages'); const registerMiddleware = require('#src/web/middleware'); 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 } = require('#src/web/lib/auth'); const { hasAnyPermission } = require('#src/rbac'); const { ensureFontLibrary } = require('#src/web/lib/media/font-library'); const { formatDashboardDate, readArrayField, parseDateTimeLocal, parseTimeLocal, normalizeScheduleMode, getAuditUserId, getCanvasSignature, fetchPlaylistCanvasId, fetchPlaylistCanvasSignature, 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.thinClientBaseUrl }); const notifyPlayerScreens = createNotifyPlayerScreens(playerActionService.forwardPlayerCommand); // Centralized dashboard/player bootstrap. const webBootstrap = createWebBootstrap({ pool: pool, common: common, playerInternalBaseUrl: webConfig.playerInternalBaseUrl, thinClientBaseUrl: webConfig.thinClientBaseUrl, 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, hashSessionToken: hashSessionToken, createSessionToken: createSessionToken }); const parseCookies = sessionService.parseCookies; const createUserSession = sessionService.createUserSession; const clearSessionCookie = sessionService.clearSessionCookie; const setSessionCookie = sessionService.setSessionCookie; 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, uploadDir: webConfig.uploadsDir, createUserSession: createUserSession, setSessionCookie: setSessionCookie, clearSessionCookie: clearSessionCookie, setAuthMessageCookie: setAuthMessageCookie, consumeAuthMessageCookie: consumeAuthMessageCookie, parseCookies: parseCookies, hashSessionToken: hashSessionToken, verifyPassword: verifyPassword, sessionCookieName: webConfig.sessionCookieName, formatDashboardDate: formatDashboardDate, getAuditUserId: getAuditUserId, hashPassword: hashPassword, validatePasswordStrength: validatePasswordStrength, readArrayField: readArrayField, rbacData: rbacData, fetchOrderedPlaylistSlides: fetchOrderedPlaylistSlides, fetchScreensByPlaylistId: fetchScreensByPlaylistId, fetchScreensBySlideId: fetchScreensBySlideId, fetchScreensByTemplateId: fetchScreensByTemplateId, fetchPlaylistCanvasId: fetchPlaylistCanvasId, fetchPlaylistCanvasSignature: fetchPlaylistCanvasSignature, getCanvasSignature: getCanvasSignature, normalizeScheduleMode: normalizeScheduleMode, parseDateTimeLocal: parseDateTimeLocal, parseTimeLocal: parseTimeLocal, redirectAfterSave: redirectAfterSave, notifyPlayerScreens: notifyPlayerScreens, broadcastDashboardState: broadcastDashboardState, dataSourceTasks: dataSourceTasks, playerActionService: playerActionService, isClientNameAvailable: isClientNameAvailable, withClientNameReservation: withClientNameReservation, requirePermission: function (permissionKey, options) { return createRequirePermission(permissionKey, Object.assign({ setAuthMessageCookie: setAuthMessageCookie }, options)); }, hasAnyPermission: hasAnyPermission, backgroundTaskQueue: backgroundTaskQueue, mediaDir: webConfig.mediaDir, uploadSyncService: webBootstrap.uploadSyncService, collectUploadReferencesFromSlide: collectUploadReferencesFromSlide, collectUploadReferencesFromTemplate: collectUploadReferencesFromTemplate, collectUploadReferencesFromPayload: collectUploadReferencesFromPayload, removeUnusedUploadFiles: removeUnusedUploadFiles, syncPlaylistUploadsOnChange: syncPlaylistUploadsOnChange, buildDashboardState: webBootstrap.buildDashboardState }); app.use(function (error, req, res, _next) { console.error(error); const statusCode = Number(error && (error.statusCode || error.status)) || 500; 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, loadCurrentUser: loadCurrentUser, initializeBackgroundTasks: initializeBackgroundTasks, captureSlideThumbnail: captureSlideThumbnail, server: server, webBaseUrl: webConfig.webBaseUrl, 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); }); }