Save worktree changes
This commit is contained in:
@@ -0,0 +1,287 @@
|
||||
const { isClientNameAvailable, withClientNameReservation } = require('../../data/client-name-check');
|
||||
const { getSharedSecret, verifyPageAuthToken } = require('../../request-auth');
|
||||
const { isTransientDbError } = require('./store');
|
||||
|
||||
const ONBOARDING_SIGNUP_LIMIT_WINDOW_MS = 5 * 60 * 1000;
|
||||
const ONBOARDING_SIGNUP_LIMIT_MAX_ATTEMPTS = 8;
|
||||
const onboardingSignupAttempts = new Map();
|
||||
|
||||
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(/\/$/, '');
|
||||
if (configured) {
|
||||
return configured;
|
||||
}
|
||||
const forwardedProto = String(req.headers['x-forwarded-proto'] || '').trim().split(',')[0];
|
||||
const protocol = forwardedProto || (req.socket && req.socket.encrypted ? 'https' : 'http');
|
||||
const forwardedHost = String(req.headers['x-forwarded-host'] || '').trim().split(',')[0];
|
||||
const host = forwardedHost || String(req.headers.host || '').trim();
|
||||
return `${protocol}://${host}`.replace(/\/$/, '');
|
||||
}
|
||||
|
||||
function getRequestIp(req) {
|
||||
const forwardedFor = String(req && req.headers && req.headers['x-forwarded-for'] || '').trim().split(',')[0];
|
||||
if (forwardedFor) {
|
||||
return forwardedFor;
|
||||
}
|
||||
|
||||
const remoteAddress = req && req.socket && req.socket.remoteAddress ? String(req.socket.remoteAddress).trim() : '';
|
||||
if (!remoteAddress) {
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
return remoteAddress.toLowerCase().startsWith('::ffff:') ? remoteAddress.slice(7) : remoteAddress;
|
||||
}
|
||||
|
||||
function getOnboardingLimitKey(req, deviceId) {
|
||||
return [getRequestIp(req), normalizeDeviceId(deviceId) || 'anonymous'].join('|');
|
||||
}
|
||||
|
||||
function clearOnboardingSignupAttempts() {
|
||||
if (onboardingSignupAttempts.size > 1000) {
|
||||
onboardingSignupAttempts.clear();
|
||||
}
|
||||
}
|
||||
|
||||
function isOnboardingSignupRateLimited(req, deviceId) {
|
||||
const now = Date.now();
|
||||
const key = getOnboardingLimitKey(req, deviceId);
|
||||
const attempts = onboardingSignupAttempts.get(key) || [];
|
||||
const windowStart = now - ONBOARDING_SIGNUP_LIMIT_WINDOW_MS;
|
||||
const recentAttempts = attempts.filter(function (timestamp) {
|
||||
return timestamp >= windowStart;
|
||||
});
|
||||
|
||||
if (recentAttempts.length >= ONBOARDING_SIGNUP_LIMIT_MAX_ATTEMPTS) {
|
||||
onboardingSignupAttempts.set(key, recentAttempts);
|
||||
return Math.max(1, Math.ceil((recentAttempts[0] + ONBOARDING_SIGNUP_LIMIT_WINDOW_MS - now) / 1000));
|
||||
}
|
||||
|
||||
recentAttempts.push(now);
|
||||
onboardingSignupAttempts.set(key, recentAttempts);
|
||||
clearOnboardingSignupAttempts();
|
||||
return 0;
|
||||
}
|
||||
|
||||
async function getOnboardingStatus(pool, deviceId) {
|
||||
const normalizedDeviceId = normalizeDeviceId(deviceId);
|
||||
if (!normalizedDeviceId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const [rows] = await pool.query(
|
||||
`SELECT d.device_id, d.client_name, d.screen_id, s.name AS screen_name, s.slug AS screen_slug, s.playlist_id
|
||||
FROM player_onboarding_devices d
|
||||
LEFT JOIN screens s ON s.id = d.screen_id
|
||||
WHERE d.device_id = ?`,
|
||||
[normalizedDeviceId]
|
||||
);
|
||||
|
||||
return rows[0] || null;
|
||||
}
|
||||
|
||||
async function commitDeviceBinding(pool, deviceId, clientName, screenSlug, isNameAvailableOnScreen, liveConnections) {
|
||||
const normalizedDeviceId = normalizeDeviceId(deviceId);
|
||||
const normalizedClientName = String(clientName || '').trim();
|
||||
const normalizedScreenSlug = String(screenSlug || '').trim();
|
||||
if (!normalizedDeviceId) {
|
||||
throw new Error('Device ID is required.');
|
||||
}
|
||||
if (!normalizedClientName) {
|
||||
throw new Error('Client name is required.');
|
||||
}
|
||||
|
||||
if (!normalizedScreenSlug) {
|
||||
throw new Error('Screen is required.');
|
||||
}
|
||||
|
||||
return withClientNameReservation(pool, normalizedClientName, async function () {
|
||||
const [screenRows] = await pool.query('SELECT id, name, slug FROM screens WHERE slug = ?', [normalizedScreenSlug]);
|
||||
if (!screenRows.length) {
|
||||
throw new Error('Screen not found.');
|
||||
}
|
||||
const screen = screenRows[0];
|
||||
|
||||
const available = await isClientNameAvailable(pool, normalizedClientName, normalizedDeviceId, liveConnections);
|
||||
if (!available) {
|
||||
const error = new Error('Client name already exists.');
|
||||
error.statusCode = 400;
|
||||
throw error;
|
||||
}
|
||||
|
||||
await pool.query(
|
||||
'INSERT INTO player_onboarding_devices (device_id, client_name, screen_id) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE client_name = VALUES(client_name), screen_id = VALUES(screen_id), modified_at = CURRENT_TIMESTAMP',
|
||||
[normalizedDeviceId, normalizedClientName, screen.id]
|
||||
);
|
||||
|
||||
return getOnboardingStatus(pool, normalizedDeviceId);
|
||||
});
|
||||
}
|
||||
|
||||
async function bindDeviceToScreen(pool, deviceId, clientName, screenSlug, isNameAvailableOnScreen, playerRuntime, onboardingStore) {
|
||||
const liveConnections = playerRuntime && typeof playerRuntime.snapshotAllConnections === 'function'
|
||||
? playerRuntime.snapshotAllConnections()
|
||||
: [];
|
||||
|
||||
try {
|
||||
return await commitDeviceBinding(pool, deviceId, clientName, screenSlug, isNameAvailableOnScreen, liveConnections);
|
||||
} catch (error) {
|
||||
if (!isTransientDbError(error)) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (onboardingStore && typeof onboardingStore.enqueueBinding === 'function') {
|
||||
await onboardingStore.enqueueBinding({
|
||||
deviceId: deviceId,
|
||||
clientName: clientName,
|
||||
screenSlug: screenSlug,
|
||||
queuedAt: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
device_id: normalizeDeviceId(deviceId),
|
||||
client_name: String(clientName || '').trim(),
|
||||
screen_slug: String(screenSlug || '').trim(),
|
||||
queued: true
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function registerPlayerOnboardingRoutes(app, options) {
|
||||
const pool = options && options.pool ? options.pool : null;
|
||||
const common = options && options.common ? options.common : null;
|
||||
const playerRuntime = options && options.playerRuntime ? options.playerRuntime : null;
|
||||
const QRCode = options && options.QRCode ? options.QRCode : null;
|
||||
const onboardingStore = options && options.onboardingStore ? options.onboardingStore : null;
|
||||
|
||||
if (!app || !pool || !common || !playerRuntime || !QRCode) {
|
||||
throw new Error('registerPlayerOnboardingRoutes requires app, pool, common, playerRuntime, and QRCode.');
|
||||
}
|
||||
|
||||
const sharedSecret = getSharedSecret();
|
||||
|
||||
function requireOnboardingPageAuth(req, res, next) {
|
||||
if (!sharedSecret) {
|
||||
return next();
|
||||
}
|
||||
|
||||
const token = String(req.headers['x-pulse-page-auth'] || '').trim();
|
||||
const payload = verifyPageAuthToken(token);
|
||||
if (!payload || String(payload.scope || '').trim() !== 'onboarding') {
|
||||
return res.status(401).json({ error: 'Onboarding page authentication required.' });
|
||||
}
|
||||
|
||||
req.playerPageAuth = payload;
|
||||
next();
|
||||
}
|
||||
|
||||
app.get('/', function (_req, res) {
|
||||
res.set('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate');
|
||||
res.send(common.renderPlayerOnboardingLandingPage());
|
||||
});
|
||||
|
||||
app.get('/onboard', function (req, res) {
|
||||
res.set('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate');
|
||||
res.send(common.renderPlayerOnboardingFormPage(String(req.query.deviceId || '').trim()));
|
||||
});
|
||||
|
||||
app.get('/api/onboarding/status', function (req, res, next) {
|
||||
if (sharedSecret) {
|
||||
const token = String(req.headers['x-pulse-page-auth'] || '').trim();
|
||||
const payload = verifyPageAuthToken(token);
|
||||
if (!payload || ['onboarding', 'player'].indexOf(String(payload.scope || '').trim()) === -1) {
|
||||
return res.status(401).json({ error: 'Onboarding page authentication required.' });
|
||||
}
|
||||
req.playerPageAuth = payload;
|
||||
}
|
||||
next();
|
||||
}, async function (req, res, next) {
|
||||
try {
|
||||
const status = await getOnboardingStatus(pool, req.query.deviceId);
|
||||
res.json({
|
||||
deviceId: normalizeDeviceId(req.query.deviceId),
|
||||
onboarded: Boolean(status && status.screen_id),
|
||||
clientName: status ? status.client_name : null,
|
||||
screenId: status ? status.screen_id : null,
|
||||
screenSlug: status ? status.screen_slug : null,
|
||||
screenName: status ? status.screen_name : null,
|
||||
playerUrl: status && status.screen_slug ? `${getPublicBaseUrl(req)}/screen/${encodeURIComponent(status.screen_slug)}` : null
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/onboarding/screens', requireOnboardingPageAuth, async function (_req, res, next) {
|
||||
try {
|
||||
const [rows] = await pool.query('SELECT id, name, slug FROM screens ORDER BY name ASC, id ASC');
|
||||
res.json({ screens: rows });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/onboarding/qr', async function (req, res, next) {
|
||||
try {
|
||||
const deviceId = normalizeDeviceId(req.query.deviceId);
|
||||
if (!deviceId) {
|
||||
return res.status(400).json({ error: 'Device ID is required' });
|
||||
}
|
||||
const onboardingUrl = `${getPublicBaseUrl(req)}/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');
|
||||
res.send(svg);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/onboarding', requireOnboardingPageAuth, async function (req, res, next) {
|
||||
try {
|
||||
const deviceId = normalizeDeviceId(req.body && req.body.deviceId);
|
||||
const clientName = String((req.body && req.body.clientName) || '').trim();
|
||||
const screenSlug = String((req.body && req.body.screenSlug) || '').trim();
|
||||
const retryAfterSeconds = isOnboardingSignupRateLimited(req, deviceId);
|
||||
if (retryAfterSeconds) {
|
||||
res.set('Retry-After', String(retryAfterSeconds));
|
||||
return res.status(429).json({ error: 'Too many onboarding attempts. Please try again later.' });
|
||||
}
|
||||
if (!deviceId) {
|
||||
return res.status(400).json({ error: 'Device ID is required' });
|
||||
}
|
||||
if (!clientName) {
|
||||
return res.status(400).json({ error: 'Client name is required' });
|
||||
}
|
||||
if (!screenSlug) {
|
||||
return res.status(400).json({ error: 'Screen is required' });
|
||||
}
|
||||
|
||||
const status = await bindDeviceToScreen(pool, deviceId, clientName, screenSlug, playerRuntime.isClientNameAvailableOnScreen, playerRuntime, onboardingStore);
|
||||
res.json({
|
||||
deviceId: deviceId,
|
||||
clientName: status ? status.client_name : clientName,
|
||||
screenId: status && status.screen_id ? status.screen_id : null,
|
||||
screenSlug: status ? status.screen_slug : screenSlug,
|
||||
screenName: status && status.screen_name ? status.screen_name : null,
|
||||
playerUrl: status && status.screen_slug ? `${getPublicBaseUrl(req)}/screen/${encodeURIComponent(status.screen_slug)}` : `${getPublicBaseUrl(req)}/screen/${encodeURIComponent(screenSlug)}`,
|
||||
queued: Boolean(status && status.queued)
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
normalizeDeviceId: normalizeDeviceId,
|
||||
getPublicBaseUrl: getPublicBaseUrl,
|
||||
getOnboardingStatus: getOnboardingStatus,
|
||||
commitDeviceBinding: commitDeviceBinding,
|
||||
bindDeviceToScreen: bindDeviceToScreen,
|
||||
registerPlayerOnboardingRoutes: registerPlayerOnboardingRoutes
|
||||
};
|
||||
@@ -0,0 +1,88 @@
|
||||
<script>
|
||||
(function () {
|
||||
var deviceKey = "pulse-signage-player-device-id";
|
||||
var clientNameKey = "pulse-signage-player-client-name";
|
||||
var screenKey = "pulse-signage-player-screen-slug";
|
||||
var deviceId = {{DEVICE_ID_JSON}};
|
||||
var form = document.getElementById("onboarding-form");
|
||||
var message = document.getElementById("onboarding-message");
|
||||
var screenSelect = document.getElementById("onboarding-screen-select");
|
||||
function setMessage(value) { if (message) { message.textContent = value || ""; } }
|
||||
function parseResponseError(response) {
|
||||
return response.text().then(function (text) {
|
||||
var fallbackMessage = text && text.trim() ? text.trim() : "Unable to save onboarding.";
|
||||
try {
|
||||
var payload = JSON.parse(text);
|
||||
return payload && payload.error ? payload.error : fallbackMessage;
|
||||
} catch (_error) {
|
||||
return fallbackMessage;
|
||||
}
|
||||
});
|
||||
}
|
||||
function loadScreens() {
|
||||
return fetch("/api/onboarding/screens", { cache: "no-store" })
|
||||
.then(function (response) { return response.ok ? response.json() : null; })
|
||||
.then(function (payload) {
|
||||
var screens = payload && Array.isArray(payload.screens) ? payload.screens : [];
|
||||
if (!screenSelect) { return screens; }
|
||||
while (screenSelect.firstChild) { screenSelect.removeChild(screenSelect.firstChild); }
|
||||
var placeholder = document.createElement("option");
|
||||
placeholder.value = "";
|
||||
placeholder.textContent = "Select a screen";
|
||||
screenSelect.appendChild(placeholder);
|
||||
screens.forEach(function (screen) {
|
||||
var option = document.createElement("option");
|
||||
option.value = String(screen && screen.slug ? screen.slug : "");
|
||||
option.textContent = String(screen && (screen.name || screen.slug) ? (screen.name || screen.slug) : "Screen");
|
||||
screenSelect.appendChild(option);
|
||||
});
|
||||
return screens;
|
||||
});
|
||||
}
|
||||
if (!deviceId) { setMessage("Missing device id. Scan the QR code from the player screen again."); return; }
|
||||
try { window.localStorage.setItem(deviceKey, deviceId); } catch (_error) {}
|
||||
loadScreens().then(function () {
|
||||
try {
|
||||
var storedClientName = window.localStorage.getItem(clientNameKey) || "";
|
||||
var storedScreenSlug = window.localStorage.getItem(screenKey) || "";
|
||||
var clientNameInput = form.querySelector("input[name=\"clientName\"]");
|
||||
if (clientNameInput && storedClientName) { clientNameInput.value = storedClientName; }
|
||||
if (screenSelect && storedScreenSlug) { screenSelect.value = storedScreenSlug; }
|
||||
} catch (_error) {}
|
||||
});
|
||||
form.addEventListener("submit", function (event) {
|
||||
event.preventDefault();
|
||||
var formData = new FormData(form);
|
||||
var clientName = String(formData.get("clientName") || "").trim();
|
||||
var screenSlug = String(formData.get("screenSlug") || "").trim();
|
||||
if (!clientName) { setMessage("Client name is required."); return; }
|
||||
if (!screenSlug) { setMessage("Screen is required."); return; }
|
||||
setMessage("Saving client...");
|
||||
fetch("/api/onboarding", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json", Accept: "application/json" },
|
||||
body: JSON.stringify({ deviceId: deviceId, clientName: clientName, screenSlug: screenSlug })
|
||||
})
|
||||
.then(function (response) {
|
||||
if (response.ok) {
|
||||
return response.json();
|
||||
}
|
||||
return parseResponseError(response).then(function (messageText) {
|
||||
throw new Error(messageText);
|
||||
});
|
||||
})
|
||||
.then(function (payload) {
|
||||
if (!payload || !payload.screenSlug) { throw new Error("Unable to save onboarding."); }
|
||||
if (payload.clientName) { try { window.localStorage.setItem(clientNameKey, payload.clientName); } catch (_error) {} }
|
||||
try { window.localStorage.setItem(screenKey, payload.screenSlug); } catch (_error) {}
|
||||
setMessage("Onboarding complete.");
|
||||
if (form) {
|
||||
Array.prototype.slice.call(form.querySelectorAll("input, select, button")).forEach(function (control) {
|
||||
control.disabled = true;
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(function (error) { setMessage(error && error.message ? error.message : "Unable to save onboarding."); });
|
||||
});
|
||||
}());
|
||||
</script>
|
||||
@@ -0,0 +1,141 @@
|
||||
<script>
|
||||
(function () {
|
||||
var deviceKey = "pulse-signage-player-device-id";
|
||||
var clientNameKey = "pulse-signage-player-client-name";
|
||||
function getClientNameStorageKey(_screenSlug) {
|
||||
return clientNameKey;
|
||||
}
|
||||
var screenKey = "pulse-signage-player-screen-slug";
|
||||
var qr = document.getElementById("onboarding-qr");
|
||||
var status = document.getElementById("onboarding-status");
|
||||
var localForm = document.getElementById("onboarding-local-form");
|
||||
var localMessage = document.getElementById("onboarding-message");
|
||||
var localScreenSelect = document.getElementById("onboarding-screen-select");
|
||||
function parseResponseError(response) {
|
||||
return response.text().then(function (text) {
|
||||
var fallbackMessage = text && text.trim() ? text.trim() : "Unable to save onboarding.";
|
||||
try {
|
||||
var payload = JSON.parse(text);
|
||||
return payload && payload.error ? payload.error : fallbackMessage;
|
||||
} catch (_error) {
|
||||
return fallbackMessage;
|
||||
}
|
||||
});
|
||||
}
|
||||
function getDeviceId() {
|
||||
var stored = "";
|
||||
try { stored = window.localStorage.getItem(deviceKey) || ""; } catch (_error) { stored = ""; }
|
||||
if (stored) { return stored; }
|
||||
var next = (window.crypto && window.crypto.randomUUID ? window.crypto.randomUUID() : "device-" + Date.now() + "-" + Math.random().toString(16).slice(2));
|
||||
try { window.localStorage.setItem(deviceKey, next); } catch (_error2) {}
|
||||
return next;
|
||||
}
|
||||
function setStatus(message) { if (status) { status.textContent = message; } }
|
||||
function setLocalMessage(message) { if (localMessage) { localMessage.textContent = message || ""; } }
|
||||
function setSelectOptions(select, screens, selectedSlug) {
|
||||
if (!select) { return; }
|
||||
while (select.firstChild) { select.removeChild(select.firstChild); }
|
||||
var placeholder = document.createElement("option");
|
||||
placeholder.value = "";
|
||||
placeholder.textContent = "Select a screen";
|
||||
select.appendChild(placeholder);
|
||||
(Array.isArray(screens) ? screens : []).forEach(function (screen) {
|
||||
var option = document.createElement("option");
|
||||
option.value = String(screen && screen.slug ? screen.slug : "");
|
||||
option.textContent = String(screen && (screen.name || screen.slug) ? (screen.name || screen.slug) : "Screen");
|
||||
if (selectedSlug && String(option.value) === String(selectedSlug)) {
|
||||
option.selected = true;
|
||||
}
|
||||
select.appendChild(option);
|
||||
});
|
||||
}
|
||||
function loadScreens(selectedSlug) {
|
||||
return fetch("/api/onboarding/screens", { cache: "no-store" })
|
||||
.then(function (response) { return response.ok ? response.json() : null; })
|
||||
.then(function (payload) {
|
||||
var screens = payload && Array.isArray(payload.screens) ? payload.screens : [];
|
||||
setSelectOptions(localScreenSelect, screens, selectedSlug);
|
||||
return screens;
|
||||
})
|
||||
.catch(function () { setSelectOptions(localScreenSelect, [], selectedSlug); return []; });
|
||||
}
|
||||
function loadQr(deviceId) {
|
||||
if (qr) { qr.src = "/api/onboarding/qr?deviceId=" + encodeURIComponent(deviceId); }
|
||||
}
|
||||
function submitOnboarding(deviceId, clientName, screenSlug) {
|
||||
return fetch("/api/onboarding", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json", Accept: "application/json" },
|
||||
body: JSON.stringify({ deviceId: deviceId, clientName: clientName, screenSlug: screenSlug })
|
||||
})
|
||||
.then(function (response) {
|
||||
if (response.ok) {
|
||||
return response.json();
|
||||
}
|
||||
return parseResponseError(response).then(function (messageText) {
|
||||
throw new Error(messageText);
|
||||
});
|
||||
})
|
||||
.then(function (payload) {
|
||||
if (!payload || !payload.screenSlug) { throw new Error("Unable to save onboarding."); }
|
||||
if (payload.clientName) { try { window.localStorage.setItem(clientNameKey, payload.clientName); } catch (_error) {} }
|
||||
if (payload.clientName && payload.screenSlug) { try { window.localStorage.setItem(getClientNameStorageKey(payload.screenSlug), payload.clientName); } catch (_error2) {} }
|
||||
try { window.localStorage.setItem(screenKey, payload.screenSlug); } catch (_error) {}
|
||||
setLocalMessage("Onboarding complete.");
|
||||
if (localForm) {
|
||||
Array.prototype.slice.call(localForm.querySelectorAll("input, select, button")).forEach(function (control) {
|
||||
control.disabled = true;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
function redirectIfOnboarded(deviceId) {
|
||||
return fetch("/api/onboarding/status?deviceId=" + encodeURIComponent(deviceId), { cache: "no-store" })
|
||||
.then(function (response) { return response.ok ? response.json() : null; })
|
||||
.then(function (payload) {
|
||||
if (payload && payload.onboarded && payload.screenSlug) {
|
||||
if (payload.clientName) { try { window.localStorage.setItem(clientNameKey, payload.clientName); } catch (_error) {} }
|
||||
if (payload.clientName && payload.screenSlug) { try { window.localStorage.setItem(getClientNameStorageKey(payload.screenSlug), payload.clientName); } catch (_error2) {} }
|
||||
try { window.localStorage.setItem(screenKey, payload.screenSlug); } catch (_error) {}
|
||||
window.location.replace("/screen/" + encodeURIComponent(payload.screenSlug));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
})
|
||||
.catch(function () { return false; });
|
||||
}
|
||||
var deviceId = getDeviceId();
|
||||
if (localForm) {
|
||||
localForm.addEventListener("submit", function (event) {
|
||||
event.preventDefault();
|
||||
var formData = new FormData(localForm);
|
||||
var clientName = String(formData.get("clientName") || "").trim();
|
||||
var screenSlug = String(formData.get("screenSlug") || "").trim();
|
||||
if (!clientName) { setLocalMessage("Client name is required."); return; }
|
||||
if (!screenSlug) { setLocalMessage("Screen is required."); return; }
|
||||
setLocalMessage("Saving client...");
|
||||
submitOnboarding(deviceId, clientName, screenSlug).catch(function (error) {
|
||||
setLocalMessage(error && error.message ? error.message : "Unable to save onboarding.");
|
||||
});
|
||||
});
|
||||
}
|
||||
loadScreens().then(function () {
|
||||
try {
|
||||
var storedScreenSlug = window.localStorage.getItem(screenKey) || "";
|
||||
var storedClientName = window.localStorage.getItem(clientNameKey) || "";
|
||||
if (!storedClientName && storedScreenSlug) { storedClientName = window.localStorage.getItem(getClientNameStorageKey(storedScreenSlug)) || ""; }
|
||||
if (storedClientName && localForm) {
|
||||
var clientNameInput = localForm.querySelector("input[name=\"clientName\"]");
|
||||
if (clientNameInput && !clientNameInput.value) { clientNameInput.value = storedClientName; }
|
||||
}
|
||||
if (storedScreenSlug && localScreenSelect) { localScreenSelect.value = storedScreenSlug; }
|
||||
} catch (_error) {}
|
||||
});
|
||||
redirectIfOnboarded(deviceId).then(function (redirected) {
|
||||
if (redirected) { return; }
|
||||
loadQr(deviceId);
|
||||
setStatus("Waiting for onboarding to finish.");
|
||||
window.setInterval(function () { redirectIfOnboarded(deviceId); }, 2000);
|
||||
});
|
||||
}());
|
||||
</script>
|
||||
@@ -0,0 +1,98 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
function isTransientDbError(error) {
|
||||
const code = String(error && error.code ? error.code : '').trim();
|
||||
return [
|
||||
'ECONNREFUSED',
|
||||
'ECONNRESET',
|
||||
'ETIMEDOUT',
|
||||
'EPIPE',
|
||||
'ENOTFOUND',
|
||||
'PROTOCOL_CONNECTION_LOST',
|
||||
'POOL_CLOSED',
|
||||
'ERR_POOL_CLOSED'
|
||||
].indexOf(code) !== -1;
|
||||
}
|
||||
|
||||
function createOnboardingStore(filePath) {
|
||||
const normalizedFilePath = String(filePath || '').trim();
|
||||
|
||||
async function readEntries() {
|
||||
try {
|
||||
const raw = await fs.promises.readFile(normalizedFilePath, 'utf8');
|
||||
const parsed = JSON.parse(raw);
|
||||
return Array.isArray(parsed) ? parsed : [];
|
||||
} catch (error) {
|
||||
if (error && error.code === 'ENOENT') {
|
||||
return [];
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function writeEntries(entries) {
|
||||
await fs.promises.mkdir(path.dirname(normalizedFilePath), { recursive: true });
|
||||
const tempPath = `${normalizedFilePath}.tmp`;
|
||||
await fs.promises.writeFile(tempPath, JSON.stringify(Array.isArray(entries) ? entries : [], null, 2), 'utf8');
|
||||
await fs.promises.rename(tempPath, normalizedFilePath);
|
||||
}
|
||||
|
||||
async function enqueueBinding(entry) {
|
||||
const normalizedEntry = {
|
||||
deviceId: String(entry && entry.deviceId ? entry.deviceId : '').trim(),
|
||||
clientName: String(entry && entry.clientName ? entry.clientName : '').trim(),
|
||||
screenSlug: String(entry && entry.screenSlug ? entry.screenSlug : '').trim(),
|
||||
queuedAt: String(entry && entry.queuedAt ? entry.queuedAt : new Date().toISOString())
|
||||
};
|
||||
|
||||
if (!normalizedEntry.deviceId || !normalizedEntry.clientName || !normalizedEntry.screenSlug) {
|
||||
return readEntries();
|
||||
}
|
||||
|
||||
const entries = await readEntries();
|
||||
const nextEntries = entries.filter(function (queuedEntry) {
|
||||
return String(queuedEntry && queuedEntry.deviceId ? queuedEntry.deviceId : '').trim() !== normalizedEntry.deviceId;
|
||||
});
|
||||
nextEntries.push(normalizedEntry);
|
||||
await writeEntries(nextEntries);
|
||||
return nextEntries;
|
||||
}
|
||||
|
||||
async function flushBindings(applyBinding) {
|
||||
const entries = await readEntries();
|
||||
if (!entries.length) {
|
||||
return { flushed: 0, remaining: 0 };
|
||||
}
|
||||
|
||||
const remaining = [];
|
||||
let flushed = 0;
|
||||
for (let index = 0; index < entries.length; index += 1) {
|
||||
const entry = entries[index];
|
||||
try {
|
||||
await applyBinding(entry);
|
||||
flushed += 1;
|
||||
} catch (error) {
|
||||
if (isTransientDbError(error)) {
|
||||
remaining.push.apply(remaining, entries.slice(index));
|
||||
break;
|
||||
}
|
||||
remaining.push.apply(remaining, entries.slice(index + 1));
|
||||
}
|
||||
}
|
||||
|
||||
await writeEntries(remaining);
|
||||
return { flushed: flushed, remaining: remaining.length };
|
||||
}
|
||||
|
||||
return {
|
||||
enqueueBinding: enqueueBinding,
|
||||
flushBindings: flushBindings,
|
||||
readEntries: readEntries
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
createOnboardingStore: createOnboardingStore,
|
||||
isTransientDbError: isTransientDbError
|
||||
};
|
||||
Reference in New Issue
Block a user