Release v2.2.0
This commit is contained in:
+81
-237
@@ -1,32 +1,29 @@
|
||||
// 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 path = require('path');
|
||||
const crypto = require('crypto');
|
||||
const common = require('./common');
|
||||
const { verifyPassword, createSessionToken, hashSessionToken, hashPassword } = require('./auth');
|
||||
const pages = require('./web/pages');
|
||||
const registerAuthRoutes = require('./web/routes/auth');
|
||||
const registerAdminPagesRoutes = require('./web/routes/admin/pages');
|
||||
const registerAdminAccountRoutes = require('./web/routes/admin/account');
|
||||
const registerAdminUsersRoutes = require('./web/routes/admin/users');
|
||||
const registerAdminManageRoutes = require('./web/routes/admin/manage');
|
||||
const registerAdminScreenCommandRoutes = require('./web/routes/admin/client-commands');
|
||||
const registerAdminContentRoutes = require('./web/routes/admin/content');
|
||||
const registerAdminDataSourceRoutes = require('./web/routes/data-sources');
|
||||
const registerAdminSettingsRoutes = require('./web/routes/settings/background-tasks');
|
||||
const { createBackgroundTaskQueue } = require('./web/lib/background-task-queue');
|
||||
const { createBackgroundTaskSetup } = require('./web/lib/background-task-setup');
|
||||
const { captureSlideThumbnail } = require('./web/lib/slide-thumbnails');
|
||||
const { refreshApiSource, refreshRssFeed } = require('./web/lib/data-source-refresh');
|
||||
const { createWebBootstrap } = require('./web/bootstrap');
|
||||
const { requirePermission } = require('./rbac');
|
||||
const rbacData = require('./web/lib/rbac-data');
|
||||
const { createPlayerActionService } = require('./web/lib/player-actions');
|
||||
const { isClientNameAvailable, withClientNameReservation } = require('./data/client-name-check');
|
||||
const { createSessionService } = require('./web/lib/session');
|
||||
const { hasAnyPermission } = require('./rbac');
|
||||
const { verifyPassword, createSessionToken, hashSessionToken, hashPassword } = 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 { 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 } = 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,
|
||||
@@ -41,57 +38,34 @@ const {
|
||||
fetchScreensByTemplateId,
|
||||
fetchOrderedPlaylistSlides,
|
||||
redirectAfterSave
|
||||
} = require('./web/lib/helpers');
|
||||
const PLAYER_INTERNAL_BASE_URL = (process.env.PLAYER_INTERNAL_BASE_URL || process.env.PLAYER_BASE_URL || 'http://player:8081').replace(/\/$/, '');
|
||||
const PLAYER_PUBLIC_BASE_URL = (process.env.PLAYER_PUBLIC_BASE_URL || process.env.PLAYER_BASE_URL || 'http://localhost:8081').replace(/\/$/, '');
|
||||
const PLAYER_WS_BASE_URL = PLAYER_INTERNAL_BASE_URL.replace(/^http/, 'ws');
|
||||
const SESSION_COOKIE_NAME = 'digital_signage_session';
|
||||
const SESSION_MAX_AGE_DAYS = Number(process.env.SESSION_MAX_AGE_DAYS || 14);
|
||||
const SESSION_MAX_AGE_MS = (Number.isFinite(SESSION_MAX_AGE_DAYS) && SESSION_MAX_AGE_DAYS > 0 ? SESSION_MAX_AGE_DAYS : 14) * 24 * 60 * 60 * 1000;
|
||||
} = 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 PORT = Number(process.env.WEB_PORT || 8080);
|
||||
const MEDIA_DIR = path.join(__dirname, '..', 'media');
|
||||
const UPLOADS_DIR = path.join(MEDIA_DIR, 'uploads');
|
||||
const THUMBNAILS_DIR = path.join(MEDIA_DIR, 'thumbnails');
|
||||
const ASSET_DIR = path.join(__dirname, 'web', 'public');
|
||||
const WEB_BASE_URL = (process.env.WEB_PUBLIC_BASE_URL || process.env.WEB_BASE_URL || ('http://127.0.0.1:' + PORT)).replace(/\/$/, '');
|
||||
const DATA_SOURCE_STARTUP_REFRESH_STAGGER_MS = Math.max(100, Number(process.env.DATA_SOURCE_STARTUP_REFRESH_STAGGER_MS || 250));
|
||||
const webConfig = createWebConfig();
|
||||
|
||||
// Background services and request forwarding helpers.
|
||||
const backgroundTaskQueue = createBackgroundTaskQueue({
|
||||
pool: pool,
|
||||
maxConcurrent: 1
|
||||
});
|
||||
const playerActionService = createPlayerActionService({
|
||||
pool: pool,
|
||||
common: common,
|
||||
playerInternalBaseUrl: PLAYER_INTERNAL_BASE_URL
|
||||
playerInternalBaseUrl: webConfig.playerInternalBaseUrl
|
||||
});
|
||||
const notifyPlayerScreens = function (slugs, commandOrPayload) {
|
||||
const uniqueSlugs = Array.from(new Set((slugs || []).map(function (slug) {
|
||||
return String(slug || '').trim();
|
||||
}).filter(Boolean)));
|
||||
const notifyPlayerScreens = createNotifyPlayerScreens(playerActionService.forwardPlayerCommand);
|
||||
|
||||
if (!uniqueSlugs.length) {
|
||||
return Promise.resolve(0);
|
||||
}
|
||||
|
||||
return Promise.allSettled(uniqueSlugs.map(function (slug) {
|
||||
return playerActionService.forwardPlayerCommand(slug, commandOrPayload || 'refresh');
|
||||
})).then(function (results) {
|
||||
return results.filter(function (result) {
|
||||
return result.status === 'fulfilled';
|
||||
}).length;
|
||||
});
|
||||
};
|
||||
// Centralized dashboard/player bootstrap.
|
||||
const webBootstrap = createWebBootstrap({
|
||||
pool: pool,
|
||||
common: common,
|
||||
playerInternalBaseUrl: PLAYER_INTERNAL_BASE_URL,
|
||||
playerPublicBaseUrl: PLAYER_PUBLIC_BASE_URL,
|
||||
uploadDir: UPLOADS_DIR,
|
||||
dashboardRefreshIntervalMs: Number(process.env.DASHBOARD_REFRESH_INTERVAL_MS || 2000),
|
||||
playerInternalBaseUrl: webConfig.playerInternalBaseUrl,
|
||||
playerPublicBaseUrl: webConfig.playerPublicBaseUrl,
|
||||
uploadDir: webConfig.uploadsDir,
|
||||
formatDashboardDate: formatDashboardDate,
|
||||
notifyPlayerScreens: notifyPlayerScreens,
|
||||
backgroundTaskQueue: backgroundTaskQueue,
|
||||
@@ -102,214 +76,80 @@ async function start() {
|
||||
const collectUploadReferencesFromTemplate = webBootstrap.collectUploadReferencesFromTemplate;
|
||||
const collectUploadReferencesFromPayload = webBootstrap.collectUploadReferencesFromPayload;
|
||||
const removeUnusedUploadFiles = webBootstrap.removeUnusedUploadFiles;
|
||||
const collectUploadPathsFromDirectory = webBootstrap.collectUploadPathsFromDirectory;
|
||||
const syncPlaylistUploadsOnChange = webBootstrap.syncPlaylistUploadsOnChange;
|
||||
const syncExistingUploadsToPlayer = webBootstrap.syncExistingUploadsToPlayer;
|
||||
const runMediaSyncTask = webBootstrap.runMediaSyncTask;
|
||||
const backgroundTaskSetup = createBackgroundTaskSetup({
|
||||
pool: pool,
|
||||
common: common,
|
||||
backgroundTaskQueue: backgroundTaskQueue,
|
||||
collectUploadPathsFromDirectory: collectUploadPathsFromDirectory,
|
||||
removeUnusedUploadFiles: removeUnusedUploadFiles,
|
||||
captureSlideThumbnail: captureSlideThumbnail,
|
||||
refreshApiSource: refreshApiSource,
|
||||
refreshRssFeed: refreshRssFeed,
|
||||
runMediaSyncTask: runMediaSyncTask,
|
||||
mediaDir: MEDIA_DIR,
|
||||
uploadsDir: UPLOADS_DIR,
|
||||
playerInternalBaseUrl: PLAYER_INTERNAL_BASE_URL,
|
||||
dataSourceStartupRefreshStaggerMs: DATA_SOURCE_STARTUP_REFRESH_STAGGER_MS
|
||||
});
|
||||
|
||||
// Background task initialization.
|
||||
// Session and auth helpers for route registration.
|
||||
const broadcastDashboardState = webBootstrap.broadcastDashboardState;
|
||||
const sessionService = createSessionService({
|
||||
sessionCookieName: SESSION_COOKIE_NAME,
|
||||
sessionMaxAgeMs: SESSION_MAX_AGE_MS,
|
||||
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 createUserSession = sessionService.createUserSession;
|
||||
const requireAuth = sessionService.requireAuth;
|
||||
|
||||
app.use(express.urlencoded({ extended: true }));
|
||||
app.use(express.json());
|
||||
app.use('/assets', express.static(ASSET_DIR));
|
||||
app.use('/assets/vendor/cropperjs', express.static(path.join(__dirname, '..', 'node_modules', 'cropperjs', 'dist')));
|
||||
app.use('/media', express.static(MEDIA_DIR));
|
||||
fs.mkdirSync(UPLOADS_DIR, { recursive: true });
|
||||
fs.mkdirSync(THUMBNAILS_DIR, { recursive: true });
|
||||
|
||||
app.use(async function (req, _res, next) {
|
||||
try {
|
||||
req.currentUser = await loadCurrentUser(pool, req);
|
||||
next();
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
app.use(function (req, res, next) {
|
||||
if (req.path === '/' || req.path === '/login' || req.path === '/logout' || req.path.indexOf('/api/internal/slide-thumbnails/') === 0) {
|
||||
return next();
|
||||
}
|
||||
|
||||
return requireAuth(req, res, next);
|
||||
});
|
||||
|
||||
registerAuthRoutes(app, {
|
||||
// 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: SESSION_COOKIE_NAME
|
||||
});
|
||||
|
||||
registerAdminPagesRoutes(app, {
|
||||
pool: pool,
|
||||
common: common,
|
||||
pages: pages,
|
||||
requirePermission: requirePermission,
|
||||
buildDashboardState: webBootstrap.buildDashboardState
|
||||
});
|
||||
|
||||
registerAdminAccountRoutes(app, {
|
||||
pool: pool,
|
||||
common: common,
|
||||
pages: pages,
|
||||
formatDashboardDate: formatDashboardDate,
|
||||
getAuditUserId: getAuditUserId,
|
||||
verifyPassword: verifyPassword,
|
||||
hashPassword: hashPassword,
|
||||
createUserSession: createUserSession,
|
||||
setSessionCookie: setSessionCookie
|
||||
});
|
||||
|
||||
registerAdminUsersRoutes(app, {
|
||||
pool: pool,
|
||||
common: common,
|
||||
pages: pages,
|
||||
sessionCookieName: webConfig.sessionCookieName,
|
||||
formatDashboardDate: formatDashboardDate,
|
||||
getAuditUserId: getAuditUserId,
|
||||
hashPassword: hashPassword,
|
||||
readArrayField: readArrayField,
|
||||
rbacData: rbacData,
|
||||
requirePermission: requirePermission
|
||||
});
|
||||
|
||||
registerAdminManageRoutes(app, {
|
||||
pool: pool,
|
||||
common: common,
|
||||
pages: pages,
|
||||
fetchOrderedPlaylistSlides: fetchOrderedPlaylistSlides,
|
||||
fetchScreensByPlaylistId: fetchScreensByPlaylistId,
|
||||
fetchScreensBySlideId: fetchScreensBySlideId,
|
||||
fetchScreensByTemplateId: fetchScreensByTemplateId,
|
||||
fetchPlaylistCanvasSignature: fetchPlaylistCanvasSignature,
|
||||
getCanvasSignature: getCanvasSignature,
|
||||
normalizeScheduleMode: normalizeScheduleMode,
|
||||
parseDateTimeLocal: parseDateTimeLocal,
|
||||
parseTimeLocal: parseTimeLocal,
|
||||
readArrayField: readArrayField,
|
||||
getAuditUserId: getAuditUserId,
|
||||
redirectAfterSave: redirectAfterSave,
|
||||
notifyPlayerScreens: notifyPlayerScreens,
|
||||
broadcastDashboardState: broadcastDashboardState,
|
||||
getScreenDeleteBlockMessage: playerActionService.getScreenDeleteBlockMessage,
|
||||
getScreenConnections: playerActionService.getScreenConnections,
|
||||
getPlaylistDeleteBlockMessage: playerActionService.getPlaylistDeleteBlockMessage,
|
||||
forwardPlayerCommand: playerActionService.forwardPlayerCommand,
|
||||
playerPublicBaseUrl: PLAYER_PUBLIC_BASE_URL,
|
||||
requirePermission: requirePermission
|
||||
});
|
||||
|
||||
registerAdminScreenCommandRoutes(app, {
|
||||
pool: pool,
|
||||
forwardPlayerCommand: playerActionService.forwardPlayerCommand,
|
||||
getScreenConnections: playerActionService.getScreenConnections,
|
||||
playerActionService: playerActionService,
|
||||
isClientNameAvailable: isClientNameAvailable,
|
||||
withClientNameReservation: withClientNameReservation,
|
||||
broadcastDashboardState: broadcastDashboardState,
|
||||
requirePermission: requirePermission
|
||||
});
|
||||
|
||||
const registerAdminRbacRoutes = require('./web/routes/admin/rbac');
|
||||
registerAdminRbacRoutes(app, {
|
||||
pool: pool,
|
||||
common: common,
|
||||
pages: pages,
|
||||
getAuditUserId: getAuditUserId,
|
||||
rbacData: rbacData,
|
||||
readArrayField: readArrayField,
|
||||
permissions: require('./rbac').PERMISSIONS,
|
||||
normalizePermissionKeys: require('./rbac').normalizePermissionKeys,
|
||||
requirePermission: requirePermission
|
||||
});
|
||||
|
||||
registerAdminContentRoutes(app, {
|
||||
pool: pool,
|
||||
common: common,
|
||||
pages: pages,
|
||||
upload: upload,
|
||||
uploadDir: UPLOADS_DIR,
|
||||
fetchScreensBySlideId: fetchScreensBySlideId,
|
||||
fetchScreensByTemplateId: fetchScreensByTemplateId,
|
||||
requirePermission: createRequirePermission,
|
||||
hasAnyPermission: hasAnyPermission,
|
||||
backgroundTaskQueue: backgroundTaskQueue,
|
||||
collectUploadReferencesFromSlide: collectUploadReferencesFromSlide,
|
||||
collectUploadReferencesFromTemplate: collectUploadReferencesFromTemplate,
|
||||
collectUploadReferencesFromPayload: collectUploadReferencesFromPayload,
|
||||
removeUnusedUploadFiles: removeUnusedUploadFiles,
|
||||
syncPlaylistUploadsOnChange: syncPlaylistUploadsOnChange,
|
||||
getAuditUserId: getAuditUserId,
|
||||
redirectAfterSave: redirectAfterSave,
|
||||
notifyPlayerScreens: notifyPlayerScreens,
|
||||
broadcastDashboardState: broadcastDashboardState,
|
||||
backgroundTaskQueue: backgroundTaskQueue,
|
||||
hasAnyPermission: hasAnyPermission,
|
||||
getSlideDeleteBlockMessage: playerActionService.getSlideDeleteBlockMessage,
|
||||
getTemplateDeleteBlockMessage: playerActionService.getTemplateDeleteBlockMessage,
|
||||
getCanvasSizeDeleteBlockMessage: playerActionService.getCanvasSizeDeleteBlockMessage,
|
||||
requirePermission: requirePermission
|
||||
});
|
||||
|
||||
registerAdminDataSourceRoutes(app, {
|
||||
pool: pool,
|
||||
common: common,
|
||||
pages: pages,
|
||||
formatDashboardDate: formatDashboardDate,
|
||||
getAuditUserId: getAuditUserId,
|
||||
redirectAfterSave: redirectAfterSave,
|
||||
fetchRssFeedItems: common.fetchRssFeedItems,
|
||||
backgroundTaskQueue: backgroundTaskQueue,
|
||||
requirePermission: requirePermission
|
||||
});
|
||||
|
||||
registerAdminSettingsRoutes(app, {
|
||||
pages: pages,
|
||||
backgroundTaskQueue: backgroundTaskQueue
|
||||
});
|
||||
|
||||
|
||||
app.use(function (req, res, next) {
|
||||
const pathName = String(req.originalUrl || '');
|
||||
const wantsHtml = !pathName.startsWith('/api/') && (!req.accepts || req.accepts('html'));
|
||||
|
||||
if (!wantsHtml) {
|
||||
return next();
|
||||
}
|
||||
|
||||
return res.status(404).send(pages.renderErrorPage({
|
||||
statusCode: 404,
|
||||
title: 'Not found',
|
||||
errorTitle: 'Oops! Page not found.',
|
||||
message: 'We could not find the page you were looking for.',
|
||||
backUrl: req.currentUser ? '/dashboard' : '/login',
|
||||
backLabel: req.currentUser ? 'Back to dashboard' : 'Sign in'
|
||||
}, req.currentUser));
|
||||
buildDashboardState: webBootstrap.buildDashboardState
|
||||
});
|
||||
|
||||
app.use(function (error, req, res, _next) {
|
||||
@@ -343,20 +183,24 @@ async function start() {
|
||||
res.status(statusCode).send(statusCode >= 500 ? 'Internal server error' : String(error && error.message ? error.message : 'Error'));
|
||||
});
|
||||
|
||||
// Ensure schema and mirror media before the web service starts handling traffic.
|
||||
await common.ensureSchema(pool, { mediaDir: MEDIA_DIR });
|
||||
await common.bootstrapDatabase(pool);
|
||||
await backgroundTaskQueue.initialize();
|
||||
await backgroundTaskSetup.initialize();
|
||||
|
||||
fs.mkdirSync(MEDIA_DIR, { recursive: true });
|
||||
await syncExistingUploadsToPlayer(pool, MEDIA_DIR).catch(function (error) {
|
||||
console.warn('Unable to sync existing media to player:', 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,
|
||||
dataSourceStartupRefreshStaggerMs: webConfig.dataSourceStartupRefreshStaggerMs
|
||||
});
|
||||
webBootstrap.installDashboardWebsocket(server, loadCurrentUser);
|
||||
|
||||
server.listen(PORT, function () {
|
||||
console.log(`Pulse Signage app listening on port ${PORT}`);
|
||||
server.listen(webConfig.port, function () {
|
||||
console.log(`Pulse Signage app listening on port ${webConfig.port}`);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user