370 lines
15 KiB
JavaScript
370 lines
15 KiB
JavaScript
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 {
|
|
formatDashboardDate,
|
|
readArrayField,
|
|
parseDateTimeLocal,
|
|
parseTimeLocal,
|
|
normalizeScheduleMode,
|
|
getAuditUserId,
|
|
getCanvasSignature,
|
|
fetchPlaylistCanvasSignature,
|
|
fetchScreensByPlaylistId,
|
|
fetchScreensBySlideId,
|
|
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:3001').replace(/\/$/, '');
|
|
const PLAYER_PUBLIC_BASE_URL = (process.env.PLAYER_PUBLIC_BASE_URL || process.env.PLAYER_BASE_URL || 'http://localhost:3001').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;
|
|
|
|
async function start() {
|
|
const app = express();
|
|
const server = http.createServer(app);
|
|
const pool = common.createPool();
|
|
const PORT = Number(process.env.WEB_PORT || 3000);
|
|
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 backgroundTaskQueue = createBackgroundTaskQueue({
|
|
pool: pool,
|
|
maxConcurrent: 1
|
|
});
|
|
const playerActionService = createPlayerActionService({
|
|
common: common,
|
|
playerInternalBaseUrl: PLAYER_INTERNAL_BASE_URL
|
|
});
|
|
const notifyPlayerScreens = function (slugs, commandOrPayload) {
|
|
const uniqueSlugs = Array.from(new Set((slugs || []).map(function (slug) {
|
|
return String(slug || '').trim();
|
|
}).filter(Boolean)));
|
|
|
|
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;
|
|
});
|
|
};
|
|
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),
|
|
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 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
|
|
});
|
|
const broadcastDashboardState = webBootstrap.broadcastDashboardState;
|
|
const sessionService = createSessionService({
|
|
sessionCookieName: SESSION_COOKIE_NAME,
|
|
sessionMaxAgeMs: SESSION_MAX_AGE_MS,
|
|
hashSessionToken: hashSessionToken,
|
|
createSessionToken: createSessionToken
|
|
});
|
|
const parseCookies = sessionService.parseCookies;
|
|
const clearSessionCookie = sessionService.clearSessionCookie;
|
|
const setSessionCookie = sessionService.setSessionCookie;
|
|
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, {
|
|
pool: pool,
|
|
pages: pages,
|
|
createUserSession: createUserSession,
|
|
setSessionCookie: setSessionCookie,
|
|
clearSessionCookie: clearSessionCookie,
|
|
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,
|
|
formatDashboardDate: formatDashboardDate,
|
|
getAuditUserId: getAuditUserId,
|
|
hashPassword: hashPassword,
|
|
readArrayField: readArrayField,
|
|
rbacData: rbacData,
|
|
requirePermission: requirePermission
|
|
});
|
|
|
|
registerAdminManageRoutes(app, {
|
|
pool: pool,
|
|
common: common,
|
|
pages: pages,
|
|
fetchOrderedPlaylistSlides: fetchOrderedPlaylistSlides,
|
|
fetchScreensByPlaylistId: fetchScreensByPlaylistId,
|
|
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,
|
|
isClientNameAvailable: isClientNameAvailable,
|
|
withClientNameReservation: withClientNameReservation,
|
|
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,
|
|
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));
|
|
});
|
|
|
|
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 : 'You do not have permission to access this area.')
|
|
: 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'));
|
|
});
|
|
|
|
// Ensure schema and mirror media before the web service starts handling traffic.
|
|
await common.ensureSchema(pool, { mediaDir: MEDIA_DIR });
|
|
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);
|
|
});
|
|
webBootstrap.installDashboardWebsocket(server, loadCurrentUser);
|
|
|
|
server.listen(PORT, function () {
|
|
console.log(`Pulse Signage app listening on port ${PORT}`);
|
|
});
|
|
|
|
}
|
|
|
|
module.exports = { start };
|
|
|
|
if (require.main === module) {
|
|
start().catch(function (error) {
|
|
console.error(error);
|
|
process.exit(1);
|
|
});
|
|
}
|