feat: add multi-user support for favorites management and room clock synchronization

- Implemented a new API endpoint for retrieving and managing user favorites in /api/users.
- Added functionality for admins to edit the shared "main" user's favorites.
- Created a one-shot DB smoke test script for verifying multi-user kiosk migrations.
- Introduced a RoomClock class for synchronizing server time across clients using WebSocket.
This commit is contained in:
Marco Mooren
2026-05-13 13:53:12 +02:00
parent f6cdfd975c
commit 29423288ca
41 changed files with 4229 additions and 275 deletions

View File

@@ -3,7 +3,7 @@
// source; controllers/panels on other devices keep working over the LAN
// because the server still binds 0.0.0.0.
import { app, BrowserWindow, Menu, shell } from 'electron';
import { app, BrowserWindow, Menu, session, shell } from 'electron';
import { fileURLToPath } from 'node:url';
import { dirname, resolve } from 'node:path';
import { mkdirSync } from 'node:fs';
@@ -43,6 +43,43 @@ async function bootServer() {
return serverHandle;
}
// Inject permissive CORS headers on cross-origin MEDIA responses so the
// renderer's AnalyserNode can read PCM from Icecast/SHOUTcast streams that
// otherwise omit Access-Control-Allow-Origin. Without this, the master's
// spectrum visualiser sees only zeros even though the audio plays fine.
// Scoped to media responses (audio/* content-type or resourceType 'media')
// so we don't accidentally weaken non-audio security guarantees.
function installMediaCorsRewrite(sess) {
sess.webRequest.onHeadersReceived({ urls: ['*://*/*'] }, (details, callback) => {
const headers = details.responseHeaders || {};
const ct = (headers['content-type'] || headers['Content-Type'] || [''])[0] || '';
const isMedia = details.resourceType === 'media'
|| /^audio\/|^application\/(ogg|x-mpegurl|vnd\.apple\.mpegurl)|^video\/mp2t/i.test(ct);
if (!isMedia) {
callback({ responseHeaders: headers });
return;
}
// Strip any existing variants so case differences don't leave two copies.
for (const k of Object.keys(headers)) {
if (/^access-control-allow-origin$/i.test(k)) delete headers[k];
if (/^access-control-allow-headers$/i.test(k)) delete headers[k];
if (/^timing-allow-origin$/i.test(k)) delete headers[k];
}
headers['Access-Control-Allow-Origin'] = ['*'];
headers['Access-Control-Allow-Headers'] = ['*'];
headers['Timing-Allow-Origin'] = ['*'];
callback({ responseHeaders: headers });
});
// Auto-grant the `media` permission for our own origin so the preload's
// one-shot getUserMedia (used to unlock audio-output device labels) does
// not prompt the user.
sess.setPermissionRequestHandler((webContents, permission, callback) => {
if (permission === 'media') return callback(true);
return callback(false);
});
}
async function createMainWindow(port) {
mainWindow = new BrowserWindow({
width: 1280,
@@ -52,11 +89,14 @@ async function createMainWindow(port) {
webPreferences: {
contextIsolation: true,
nodeIntegration: false,
sandbox: true
sandbox: true,
preload: resolve(__dirname, 'preload.cjs')
}
});
Menu.setApplicationMenu(null);
installMediaCorsRewrite(mainWindow.webContents.session);
// Open target/external links in the OS browser instead of inside Electron.
mainWindow.webContents.setWindowOpenHandler(({ url }) => {
shell.openExternal(url);