Release v2.2.0

This commit is contained in:
2026-08-01 21:41:44 +01:00
parent c643d2fb07
commit d6417b667c
673 changed files with 146752 additions and 7389 deletions
+83 -7
View File
@@ -1,5 +1,7 @@
const { isClientNameAvailable, withClientNameReservation } = require('../../data/client-name-check');
const { getSharedSecret, verifyPageAuthToken } = require('../../request-auth');
// Player onboarding routes and signup flow helpers.
const { isClientNameAvailable, withClientNameReservation } = require('#src/data/client-name-check');
const { getSharedSecret, verifyPageAuthToken } = require('#src/request-auth');
const { isTransientDbError } = require('./store');
const ONBOARDING_SIGNUP_LIMIT_WINDOW_MS = 5 * 60 * 1000;
@@ -10,8 +12,8 @@ function normalizeDeviceId(value) {
return String(value || '').trim().replace(/[^a-zA-Z0-9_-]/g, '').slice(0, 128);
}
function getPublicBaseUrl(req) {
const configured = String(process.env.PLAYER_PUBLIC_BASE_URL || process.env.PLAYER_BASE_URL || '').trim().replace(/\/$/, '');
function getPublicBaseUrl(req, configuredUrl) {
const configured = String(configuredUrl || process.env.PLAYER_PUBLIC_BASE_URL || process.env.PLAYER_BASE_URL || '').trim().replace(/\/$/, '');
if (configured) {
return configured;
}
@@ -40,6 +42,15 @@ function getOnboardingLimitKey(req, deviceId) {
return [getRequestIp(req), normalizeDeviceId(deviceId) || 'anonymous'].join('|');
}
function getPlayerPublicBaseUrl(req, configuredUrl) {
return getPublicBaseUrl(req, configuredUrl);
}
function getPlayerInternalBaseUrl(configuredUrl) {
const configured = String(configuredUrl || process.env.PLAYER_INTERNAL_BASE_URL || process.env.PLAYER_BASE_URL || '').trim().replace(/\/$/, '');
return configured || null;
}
function clearOnboardingSignupAttempts() {
if (onboardingSignupAttempts.size > 1000) {
onboardingSignupAttempts.clear();
@@ -121,6 +132,62 @@ async function commitDeviceBinding(pool, deviceId, clientName, screenSlug, isNam
});
}
async function upsertPlayerRegistration(pool, deviceId, publicBaseUrl, internalBaseUrl, screenSlug) {
const normalizedDeviceId = normalizeDeviceId(deviceId);
const normalizedPublicBaseUrl = String(publicBaseUrl || '').trim().replace(/\/$/, '');
const normalizedInternalBaseUrl = String(internalBaseUrl || '').trim().replace(/\/$/, '');
if (!normalizedDeviceId) {
return null;
}
await pool.query(
`INSERT INTO d_players (device_id, public_base_url, internal_base_url, last_seen_at)
VALUES (?, ?, ?, CURRENT_TIMESTAMP)
ON DUPLICATE KEY UPDATE public_base_url = VALUES(public_base_url), internal_base_url = VALUES(internal_base_url), last_seen_at = CURRENT_TIMESTAMP, modified_at = CURRENT_TIMESTAMP`,
[normalizedDeviceId, normalizedPublicBaseUrl || null, normalizedInternalBaseUrl || null]
);
return {
device_id: normalizedDeviceId,
public_base_url: normalizedPublicBaseUrl || null,
internal_base_url: normalizedInternalBaseUrl || null
};
}
async function bindPlayerToScreen(pool, deviceId, screenSlug) {
const normalizedDeviceId = normalizeDeviceId(deviceId);
const normalizedScreenSlug = String(screenSlug || '').trim();
if (!normalizedDeviceId || !normalizedScreenSlug) {
return null;
}
const [screenRows] = await pool.query('SELECT id FROM d_screens WHERE slug = ? LIMIT 1', [normalizedScreenSlug]);
if (!screenRows.length) {
return null;
}
const screenId = Number(screenRows[0].id);
const connection = await pool.getConnection();
try {
await connection.beginTransaction();
// Current model: every screen binds to the shared player row '1'.
// If we introduce multiple players, resolve the correct player row here instead of hardcoding it.
await connection.query("UPDATE d_screens SET player_id = '1', modified_at = CURRENT_TIMESTAMP WHERE id = ?", [screenId]);
await connection.commit();
} catch (error) {
await connection.rollback();
throw error;
} finally {
connection.release();
}
return {
screen_id: screenId,
screen_slug: normalizedScreenSlug,
player_id: '1'
};
}
async function bindDeviceToScreen(pool, deviceId, clientName, screenSlug, isNameAvailableOnScreen, playerRuntime, onboardingStore) {
const liveConnections = playerRuntime && typeof playerRuntime.snapshotAllConnections === 'function'
? playerRuntime.snapshotAllConnections()
@@ -184,9 +251,13 @@ function registerPlayerOnboardingRoutes(app, options) {
res.send(common.renderPlayerOnboardingLandingPage());
});
app.get('/onboard', function (req, res) {
app.get('/onboard', async function (req, res, next) {
res.set('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate');
res.send(common.renderPlayerOnboardingFormPage(String(req.query.deviceId || '').trim()));
try {
res.send(common.renderPlayerOnboardingFormPage(String(req.query.deviceId || '').trim()));
} catch (error) {
next(error);
}
});
app.get('/api/onboarding/status', function (req, res, next) {
@@ -231,7 +302,7 @@ function registerPlayerOnboardingRoutes(app, options) {
if (!deviceId) {
return res.status(400).json({ error: 'Device ID is required' });
}
const onboardingUrl = `${getPublicBaseUrl(req)}/onboard?deviceId=${encodeURIComponent(deviceId)}`;
const onboardingUrl = `${getPublicBaseUrl(req, options && options.playerPublicBaseUrl)}/onboard?deviceId=${encodeURIComponent(deviceId)}`;
const svg = await QRCode.toString(onboardingUrl, { type: 'svg', margin: 1, errorCorrectionLevel: 'M' });
res.set('Content-Type', 'image/svg+xml; charset=utf-8');
res.set('Cache-Control', 'no-store');
@@ -262,6 +333,7 @@ function registerPlayerOnboardingRoutes(app, options) {
}
const status = await bindDeviceToScreen(pool, deviceId, clientName, screenSlug, playerRuntime.isClientNameAvailableOnScreen, playerRuntime, onboardingStore);
await bindPlayerToScreen(pool, deviceId, screenSlug);
res.json({
deviceId: deviceId,
clientName: status ? status.client_name : clientName,
@@ -280,8 +352,12 @@ function registerPlayerOnboardingRoutes(app, options) {
module.exports = {
normalizeDeviceId: normalizeDeviceId,
getPublicBaseUrl: getPublicBaseUrl,
getPlayerPublicBaseUrl: getPlayerPublicBaseUrl,
getPlayerInternalBaseUrl: getPlayerInternalBaseUrl,
getOnboardingStatus: getOnboardingStatus,
commitDeviceBinding: commitDeviceBinding,
upsertPlayerRegistration: upsertPlayerRegistration,
bindPlayerToScreen: bindPlayerToScreen,
bindDeviceToScreen: bindDeviceToScreen,
registerPlayerOnboardingRoutes: registerPlayerOnboardingRoutes
};
+2
View File
@@ -1,3 +1,5 @@
// File-backed storage for onboarding device mappings.
const fs = require('fs');
const path = require('path');