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:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user