Save worktree changes

This commit is contained in:
2026-07-25 02:29:19 +01:00
parent 8d3b7d557b
commit db9d718cd8
170 changed files with 11719 additions and 3414 deletions
+132 -26
View File
@@ -6,20 +6,24 @@ 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 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 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, normalizeIntervalMs } = require('./web/lib/background-task-queue');
const { refreshApiSource, refreshRssFeed } = require('./web/lib/data-source-refresh');
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 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 {
formatDashboardDate,
readArrayField,
@@ -34,7 +38,7 @@ const {
fetchScreensByTemplateId,
fetchOrderedPlaylistSlides,
redirectAfterSave
} = require('./web/helpers');
} = 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');
@@ -47,8 +51,12 @@ async function start() {
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 MEDIA_DIR = path.join(__dirname, '..', 'media');
const ASSET_DIR = path.join(__dirname, 'web', 'public');
const backgroundTaskQueue = createBackgroundTaskQueue({
pool: pool,
maxConcurrent: 1
});
const playerActionService = createPlayerActionService({
common: common,
playerInternalBaseUrl: PLAYER_INTERNAL_BASE_URL
@@ -75,10 +83,11 @@ async function start() {
common: common,
playerInternalBaseUrl: PLAYER_INTERNAL_BASE_URL,
playerPublicBaseUrl: PLAYER_PUBLIC_BASE_URL,
uploadDir: UPLOAD_DIR,
uploadDir: MEDIA_DIR,
dashboardRefreshIntervalMs: Number(process.env.DASHBOARD_REFRESH_INTERVAL_MS || 2000),
formatDashboardDate: formatDashboardDate,
notifyPlayerScreens: notifyPlayerScreens
notifyPlayerScreens: notifyPlayerScreens,
backgroundTaskQueue: backgroundTaskQueue
});
const upload = webBootstrap.upload;
const collectUploadReferencesFromSlide = webBootstrap.collectUploadReferencesFromSlide;
@@ -86,6 +95,7 @@ async function start() {
const collectUploadReferencesFromPayload = webBootstrap.collectUploadReferencesFromPayload;
const syncPlaylistUploadsOnChange = webBootstrap.syncPlaylistUploadsOnChange;
const syncExistingUploadsToPlayer = webBootstrap.syncExistingUploadsToPlayer;
const runMediaSyncTask = webBootstrap.runMediaSyncTask;
const broadcastDashboardState = webBootstrap.broadcastDashboardState;
const sessionService = createSessionService({
sessionCookieName: SESSION_COOKIE_NAME,
@@ -100,10 +110,38 @@ async function start() {
const createUserSession = sessionService.createUserSession;
const requireAuth = sessionService.requireAuth;
backgroundTaskQueue.setTaskHandler('media-sync', function (task) {
return runMediaSyncTask(task && task.payload ? task.payload : task);
});
backgroundTaskQueue.setTaskHandler('data-source-refresh', async function (task) {
const payload = task && task.payload ? task.payload : {};
const sourceType = String(payload.sourceType || '').trim();
const sourceId = Number(payload.sourceId || 0);
if (sourceType === 'api-source') {
const apiSource = await common.fetchApiSourceById(pool, sourceId);
if (!apiSource) {
throw new Error('API source not found.');
}
return refreshApiSource(pool, common, apiSource.id, apiSource.api_url, Number(payload.actorId) || null);
}
if (sourceType === 'rss-feed') {
const rssFeed = await common.fetchRssFeedById(pool, sourceId);
if (!rssFeed) {
throw new Error('RSS feed not found.');
}
return refreshRssFeed(pool, common, rssFeed.id, rssFeed.feed_url, rssFeed.item_limit, Number(payload.actorId) || null);
}
throw new Error('Unsupported data source refresh task.');
});
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('/media', express.static(MEDIA_DIR));
app.use(async function (req, _res, next) {
try {
@@ -114,7 +152,13 @@ async function start() {
}
});
app.use('/admin', requireAuth);
app.use(function (req, res, next) {
if (req.path === '/' || req.path === '/login' || req.path === '/logout') {
return next();
}
return requireAuth(req, res, next);
});
registerAuthRoutes(app, {
pool: pool,
@@ -149,6 +193,7 @@ async function start() {
registerAdminUsersRoutes(app, {
pool: pool,
common: common,
pages: pages,
formatDashboardDate: formatDashboardDate,
getAuditUserId: getAuditUserId,
@@ -191,9 +236,10 @@ async function start() {
requirePermission: requirePermission
});
const registerAdminRbacRoutes = require('./web/routes/admin-rbac');
const registerAdminRbacRoutes = require('./web/routes/admin/rbac');
registerAdminRbacRoutes(app, {
pool: pool,
common: common,
pages: pages,
getAuditUserId: getAuditUserId,
rbacData: rbacData,
@@ -208,7 +254,7 @@ async function start() {
common: common,
pages: pages,
upload: upload,
uploadDir: UPLOAD_DIR,
uploadDir: MEDIA_DIR,
fetchScreensBySlideId: fetchScreensBySlideId,
fetchScreensByTemplateId: fetchScreensByTemplateId,
collectUploadReferencesFromSlide: collectUploadReferencesFromSlide,
@@ -225,6 +271,23 @@ async function start() {
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 || '');
@@ -239,7 +302,7 @@ async function start() {
title: 'Not found',
errorTitle: 'Oops! Page not found.',
message: 'We could not find the page you were looking for.',
backUrl: req.currentUser ? '/admin' : '/login',
backUrl: req.currentUser ? '/dashboard' : '/login',
backLabel: req.currentUser ? 'Back to dashboard' : 'Sign in'
}, req.currentUser));
});
@@ -247,7 +310,8 @@ async function start() {
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'));
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;
@@ -266,7 +330,7 @@ async function start() {
errorTitle: title,
message: message,
detail: statusCode >= 500 ? 'The server could not complete the request.' : '',
backUrl: req.currentUser ? '/admin' : '/login',
backUrl: req.currentUser ? '/dashboard' : '/login',
backLabel: req.currentUser ? 'Back to dashboard' : 'Sign in'
}, req.currentUser));
}
@@ -274,11 +338,53 @@ async function start() {
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.
// Ensure schema and mirror media 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);
await backgroundTaskQueue.initialize();
async function syncRecurringRefreshes() {
const apiSourcesData = await common.fetchApiSourcesData(pool);
(apiSourcesData.apiSources || []).forEach(function (apiSource) {
backgroundTaskQueue.registerRecurringTask({
key: 'api-source-refresh:' + Number(apiSource.id),
title: 'API source refresh',
category: 'data-source',
intervalMs: normalizeIntervalMs(apiSource.update_interval_value, apiSource.update_interval_unit),
metadata: {
sourceType: 'api-source',
sourceId: Number(apiSource.id),
sourceName: apiSource.name
},
run: function () {
return refreshApiSource(pool, common, apiSource.id, apiSource.api_url, null);
}
});
});
const rssFeedsData = await common.fetchRssFeedsData(pool);
(rssFeedsData.rssFeeds || []).forEach(function (rssFeed) {
backgroundTaskQueue.registerRecurringTask({
key: 'rss-feed-refresh:' + Number(rssFeed.id),
title: 'RSS feed refresh',
category: 'data-source',
intervalMs: normalizeIntervalMs(rssFeed.update_interval_value, rssFeed.update_interval_unit),
metadata: {
sourceType: 'rss-feed',
sourceId: Number(rssFeed.id),
sourceName: rssFeed.name
},
run: function () {
return refreshRssFeed(pool, common, rssFeed.id, rssFeed.feed_url, rssFeed.item_limit, null);
}
});
});
}
await syncRecurringRefreshes();
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);