299 lines
11 KiB
JavaScript
299 lines
11 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/routes');
|
|
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 { createWebBootstrap } = require('./web/bootstrap');
|
|
const { requirePermission } = require('./rbac');
|
|
const rbacData = require('./web/rbac-data');
|
|
const { createPlayerActionService } = require('./web/player-actions');
|
|
const { isClientNameAvailable, withClientNameReservation } = require('./client-name-check');
|
|
const { createSessionService } = require('./web/session');
|
|
const {
|
|
formatDashboardDate,
|
|
readArrayField,
|
|
parseDateTimeLocal,
|
|
parseTimeLocal,
|
|
normalizeScheduleMode,
|
|
getAuditUserId,
|
|
getCanvasSignature,
|
|
fetchPlaylistCanvasSignature,
|
|
fetchScreensByPlaylistId,
|
|
fetchScreensBySlideId,
|
|
fetchScreensByTemplateId,
|
|
fetchOrderedPlaylistSlides,
|
|
redirectAfterSave
|
|
} = require('./web/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 UPLOAD_DIR = path.join(__dirname, '..', 'uploads');
|
|
const ASSET_DIR = path.join(__dirname, 'web', 'public');
|
|
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: UPLOAD_DIR,
|
|
dashboardRefreshIntervalMs: Number(process.env.DASHBOARD_REFRESH_INTERVAL_MS || 2000),
|
|
formatDashboardDate: formatDashboardDate,
|
|
notifyPlayerScreens: notifyPlayerScreens
|
|
});
|
|
const upload = webBootstrap.upload;
|
|
const collectUploadReferencesFromSlide = webBootstrap.collectUploadReferencesFromSlide;
|
|
const collectUploadReferencesFromTemplate = webBootstrap.collectUploadReferencesFromTemplate;
|
|
const collectUploadReferencesFromPayload = webBootstrap.collectUploadReferencesFromPayload;
|
|
const syncPlaylistUploadsOnChange = webBootstrap.syncPlaylistUploadsOnChange;
|
|
const syncExistingUploadsToPlayer = webBootstrap.syncExistingUploadsToPlayer;
|
|
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('/uploads', express.static(UPLOAD_DIR));
|
|
|
|
app.use(async function (req, _res, next) {
|
|
try {
|
|
req.currentUser = await loadCurrentUser(pool, req);
|
|
next();
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
});
|
|
|
|
app.use('/admin', requireAuth);
|
|
|
|
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,
|
|
pages: pages,
|
|
formatDashboardDate: formatDashboardDate,
|
|
getAuditUserId: getAuditUserId,
|
|
verifyPassword: verifyPassword,
|
|
hashPassword: hashPassword,
|
|
createUserSession: createUserSession,
|
|
setSessionCookie: setSessionCookie
|
|
});
|
|
|
|
registerAdminUsersRoutes(app, {
|
|
pool: pool,
|
|
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,
|
|
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: UPLOAD_DIR,
|
|
fetchScreensBySlideId: fetchScreensBySlideId,
|
|
fetchScreensByTemplateId: fetchScreensByTemplateId,
|
|
collectUploadReferencesFromSlide: collectUploadReferencesFromSlide,
|
|
collectUploadReferencesFromTemplate: collectUploadReferencesFromTemplate,
|
|
collectUploadReferencesFromPayload: collectUploadReferencesFromPayload,
|
|
syncPlaylistUploadsOnChange: syncPlaylistUploadsOnChange,
|
|
getAuditUserId: getAuditUserId,
|
|
redirectAfterSave: redirectAfterSave,
|
|
notifyPlayerScreens: notifyPlayerScreens,
|
|
broadcastDashboardState: broadcastDashboardState,
|
|
getSlideDeleteBlockMessage: playerActionService.getSlideDeleteBlockMessage,
|
|
getTemplateDeleteBlockMessage: playerActionService.getTemplateDeleteBlockMessage,
|
|
getCanvasSizeDeleteBlockMessage: playerActionService.getCanvasSizeDeleteBlockMessage,
|
|
requirePermission: requirePermission
|
|
});
|
|
|
|
|
|
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 ? '/admin' : '/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 wantsHtml = !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 ? '/admin' : '/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 uploads before the web service starts handling traffic.
|
|
await common.ensureSchema(pool);
|
|
fs.mkdirSync(UPLOAD_DIR, { recursive: true });
|
|
syncExistingUploadsToPlayer(pool, UPLOAD_DIR).catch(function (error) {
|
|
console.warn('Unable to sync existing uploads 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);
|
|
});
|
|
}
|