Fix duplicate onboarding client names
This commit is contained in:
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "pulse-signage",
|
"name": "pulse-signage",
|
||||||
"version": "1.3.3",
|
"version": "1.3.4",
|
||||||
"private": false,
|
"private": false,
|
||||||
"description": "Pulse Signage application with MySQL and media uploads",
|
"description": "Pulse Signage application with MySQL and media uploads",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
const crypto = require('crypto');
|
||||||
|
|
||||||
function normalizeClientName(value) {
|
function normalizeClientName(value) {
|
||||||
return String(value || '').trim();
|
return String(value || '').trim();
|
||||||
}
|
}
|
||||||
@@ -68,9 +70,49 @@ async function isClientNameAvailable(pool, clientName, excludeDeviceId, liveConn
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildClientNameLockName(clientName) {
|
||||||
|
return `ps_client_name_${crypto.createHash('sha1').update(String(clientName || '').trim().toLowerCase()).digest('hex')}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function withClientNameReservation(pool, clientName, handler) {
|
||||||
|
if (!pool || typeof pool.getConnection !== 'function') {
|
||||||
|
return handler();
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizedName = normalizeClientName(clientName);
|
||||||
|
if (!normalizedName) {
|
||||||
|
return handler();
|
||||||
|
}
|
||||||
|
|
||||||
|
const connection = await pool.getConnection();
|
||||||
|
const lockName = buildClientNameLockName(normalizedName);
|
||||||
|
let lockAcquired = false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const [lockRows] = await connection.query('SELECT GET_LOCK(?, 5) AS lock_result', [lockName]);
|
||||||
|
const lockResult = lockRows && lockRows[0] ? Number(lockRows[0].lock_result) : 0;
|
||||||
|
if (lockResult !== 1) {
|
||||||
|
const error = new Error('Client name is busy. Please try again.');
|
||||||
|
error.statusCode = 409;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
lockAcquired = true;
|
||||||
|
return await handler();
|
||||||
|
} finally {
|
||||||
|
if (lockAcquired) {
|
||||||
|
try {
|
||||||
|
await connection.query('SELECT RELEASE_LOCK(?)', [lockName]);
|
||||||
|
} catch (_error) {}
|
||||||
|
}
|
||||||
|
connection.release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
normalizeClientName: normalizeClientName,
|
normalizeClientName: normalizeClientName,
|
||||||
normalizeDeviceId: normalizeDeviceId,
|
normalizeDeviceId: normalizeDeviceId,
|
||||||
collectLiveConnections: collectLiveConnections,
|
collectLiveConnections: collectLiveConnections,
|
||||||
isClientNameAvailable: isClientNameAvailable
|
isClientNameAvailable: isClientNameAvailable,
|
||||||
|
withClientNameReservation: withClientNameReservation
|
||||||
};
|
};
|
||||||
+19
-19
@@ -1,4 +1,4 @@
|
|||||||
const { isClientNameAvailable } = require('../client-name-check');
|
const { isClientNameAvailable, withClientNameReservation } = require('../client-name-check');
|
||||||
const { isTransientDbError } = require('./onboarding-store');
|
const { isTransientDbError } = require('./onboarding-store');
|
||||||
function normalizeDeviceId(value) {
|
function normalizeDeviceId(value) {
|
||||||
return String(value || '').trim().replace(/[^a-zA-Z0-9_-]/g, '').slice(0, 128);
|
return String(value || '').trim().replace(/[^a-zA-Z0-9_-]/g, '').slice(0, 128);
|
||||||
@@ -48,27 +48,27 @@ async function commitDeviceBinding(pool, deviceId, clientName, screenSlug, isNam
|
|||||||
throw new Error('Screen is required.');
|
throw new Error('Screen is required.');
|
||||||
}
|
}
|
||||||
|
|
||||||
const [screenRows] = await pool.query('SELECT id, name, slug FROM screens WHERE slug = ?', [normalizedScreenSlug]);
|
return withClientNameReservation(pool, normalizedClientName, async function () {
|
||||||
if (!screenRows.length) {
|
const [screenRows] = await pool.query('SELECT id, name, slug FROM screens WHERE slug = ?', [normalizedScreenSlug]);
|
||||||
throw new Error('Screen not found.');
|
if (!screenRows.length) {
|
||||||
}
|
throw new Error('Screen not found.');
|
||||||
const screen = screenRows[0];
|
}
|
||||||
|
const screen = screenRows[0];
|
||||||
|
|
||||||
const available = typeof isClientNameAvailableOnScreen === 'function'
|
const available = await isClientNameAvailable(pool, normalizedClientName, normalizedDeviceId, liveConnections);
|
||||||
? await isClientNameAvailable(pool, normalizedClientName, normalizedDeviceId, liveConnections)
|
if (!available) {
|
||||||
: true;
|
const error = new Error('Client name already exists.');
|
||||||
if (!available) {
|
error.statusCode = 400;
|
||||||
const error = new Error('Client name already exists.');
|
throw error;
|
||||||
error.statusCode = 400;
|
}
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
|
|
||||||
await pool.query(
|
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',
|
'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]
|
[normalizedDeviceId, normalizedClientName, screen.id]
|
||||||
);
|
);
|
||||||
|
|
||||||
return getOnboardingStatus(pool, normalizedDeviceId);
|
return getOnboardingStatus(pool, normalizedDeviceId);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function bindDeviceToScreen(pool, deviceId, clientName, screenSlug, isNameAvailableOnScreen, playerRuntime, onboardingStore) {
|
async function bindDeviceToScreen(pool, deviceId, clientName, screenSlug, isNameAvailableOnScreen, playerRuntime, onboardingStore) {
|
||||||
|
|||||||
+3
-2
@@ -16,7 +16,7 @@ const registerAdminScreenCommandRoutes = require('./web/routes/admin-screen-comm
|
|||||||
const registerAdminContentRoutes = require('./web/routes/admin-content');
|
const registerAdminContentRoutes = require('./web/routes/admin-content');
|
||||||
const { createWebBootstrap } = require('./web/bootstrap');
|
const { createWebBootstrap } = require('./web/bootstrap');
|
||||||
const { createPlayerActionService } = require('./web/player-actions');
|
const { createPlayerActionService } = require('./web/player-actions');
|
||||||
const { isClientNameAvailable } = require('./client-name-check');
|
const { isClientNameAvailable, withClientNameReservation } = require('./client-name-check');
|
||||||
const { createSessionService } = require('./web/session');
|
const { createSessionService } = require('./web/session');
|
||||||
const {
|
const {
|
||||||
formatDashboardDate,
|
formatDashboardDate,
|
||||||
@@ -179,7 +179,8 @@ async function start() {
|
|||||||
pool: pool,
|
pool: pool,
|
||||||
forwardPlayerCommand: playerActionService.forwardPlayerCommand,
|
forwardPlayerCommand: playerActionService.forwardPlayerCommand,
|
||||||
getScreenConnections: playerActionService.getScreenConnections,
|
getScreenConnections: playerActionService.getScreenConnections,
|
||||||
isClientNameAvailable: isClientNameAvailable
|
isClientNameAvailable: isClientNameAvailable,
|
||||||
|
withClientNameReservation: withClientNameReservation
|
||||||
});
|
});
|
||||||
|
|
||||||
registerAdminContentRoutes(app, {
|
registerAdminContentRoutes(app, {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ module.exports = function registerAdminScreenCommandRoutes(app, deps) {
|
|||||||
const forwardPlayerCommand = deps.forwardPlayerCommand;
|
const forwardPlayerCommand = deps.forwardPlayerCommand;
|
||||||
const getScreenConnections = deps.getScreenConnections;
|
const getScreenConnections = deps.getScreenConnections;
|
||||||
const isClientNameAvailable = deps.isClientNameAvailable;
|
const isClientNameAvailable = deps.isClientNameAvailable;
|
||||||
|
const withClientNameReservation = deps.withClientNameReservation;
|
||||||
|
|
||||||
app.post('/admin/screens/:slug/commands', async function (req, res, next) {
|
app.post('/admin/screens/:slug/commands', async function (req, res, next) {
|
||||||
try {
|
try {
|
||||||
@@ -56,40 +57,66 @@ module.exports = function registerAdminScreenCommandRoutes(app, deps) {
|
|||||||
unchanged: true
|
unchanged: true
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
let liveConnections = [];
|
if (typeof withClientNameReservation !== 'function') {
|
||||||
try {
|
return res.status(500).json({ error: 'Client name reservation is unavailable.' });
|
||||||
const [screenSlugs] = await pool.query('SELECT slug FROM screens ORDER BY slug ASC');
|
|
||||||
const liveResults = await Promise.all((screenSlugs || []).map(async function (row) {
|
|
||||||
const screenSlug = String(row && row.slug ? row.slug : '').trim();
|
|
||||||
if (!screenSlug || typeof getScreenConnections !== 'function') {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const liveResponse = await getScreenConnections(screenSlug);
|
|
||||||
return Array.isArray(liveResponse && liveResponse.connections) ? liveResponse.connections : [];
|
|
||||||
} catch (_error) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
liveConnections = liveResults.flat();
|
|
||||||
} catch (_error) {
|
|
||||||
liveConnections = [];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const available = typeof isClientNameAvailable === 'function'
|
return withClientNameReservation(pool, clientName, async function () {
|
||||||
? await isClientNameAvailable(pool, clientName, deviceId, liveConnections)
|
let liveConnections = [];
|
||||||
: true;
|
try {
|
||||||
if (!available) {
|
const [screenSlugs] = await pool.query('SELECT slug FROM screens ORDER BY slug ASC');
|
||||||
return res.status(409).json({ error: 'Client name already exists.' });
|
const liveResults = await Promise.all((screenSlugs || []).map(async function (row) {
|
||||||
}
|
const screenSlug = String(row && row.slug ? row.slug : '').trim();
|
||||||
|
if (!screenSlug || typeof getScreenConnections !== 'function') {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const liveResponse = await getScreenConnections(screenSlug);
|
||||||
|
return Array.isArray(liveResponse && liveResponse.connections) ? liveResponse.connections : [];
|
||||||
|
} catch (_error) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
liveConnections = liveResults.flat();
|
||||||
|
} catch (_error) {
|
||||||
|
liveConnections = [];
|
||||||
|
}
|
||||||
|
|
||||||
if (!onboardingRow) {
|
const available = await isClientNameAvailable(pool, clientName, deviceId, liveConnections);
|
||||||
await forwardPlayerCommand(slug, {
|
if (!available) {
|
||||||
command: command,
|
return res.status(409).json({ error: 'Client name already exists.' });
|
||||||
clientName: clientName,
|
}
|
||||||
clientId: connectionId || deviceId || null,
|
|
||||||
deviceId: deviceId || null
|
if (!onboardingRow) {
|
||||||
}, connectionId || deviceId || undefined);
|
await forwardPlayerCommand(slug, {
|
||||||
|
command: command,
|
||||||
|
clientName: clientName,
|
||||||
|
clientId: connectionId || deviceId || null,
|
||||||
|
deviceId: deviceId || null
|
||||||
|
}, connectionId || deviceId || undefined);
|
||||||
|
|
||||||
|
return res.json({
|
||||||
|
screen: screenRows[0],
|
||||||
|
screenSlug: slug,
|
||||||
|
command: command,
|
||||||
|
connectionId: connectionId || null,
|
||||||
|
deviceId: deviceId,
|
||||||
|
clientName: clientName,
|
||||||
|
ok: true,
|
||||||
|
liveOnly: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const [updateResult] = await pool.query(
|
||||||
|
`UPDATE player_onboarding_devices pod
|
||||||
|
JOIN screens s ON s.id = pod.screen_id
|
||||||
|
SET pod.client_name = ?, pod.modified_at = CURRENT_TIMESTAMP
|
||||||
|
WHERE s.slug = ? AND pod.device_id = ?`,
|
||||||
|
[clientName, slug, deviceId]
|
||||||
|
);
|
||||||
|
if (!updateResult.affectedRows) {
|
||||||
|
return res.status(404).json({ error: 'Client not found' });
|
||||||
|
}
|
||||||
|
|
||||||
return res.json({
|
return res.json({
|
||||||
screen: screenRows[0],
|
screen: screenRows[0],
|
||||||
@@ -98,30 +125,8 @@ module.exports = function registerAdminScreenCommandRoutes(app, deps) {
|
|||||||
connectionId: connectionId || null,
|
connectionId: connectionId || null,
|
||||||
deviceId: deviceId,
|
deviceId: deviceId,
|
||||||
clientName: clientName,
|
clientName: clientName,
|
||||||
ok: true,
|
ok: true
|
||||||
liveOnly: true
|
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
const [updateResult] = await pool.query(
|
|
||||||
`UPDATE player_onboarding_devices pod
|
|
||||||
JOIN screens s ON s.id = pod.screen_id
|
|
||||||
SET pod.client_name = ?, pod.modified_at = CURRENT_TIMESTAMP
|
|
||||||
WHERE s.slug = ? AND pod.device_id = ?`,
|
|
||||||
[clientName, slug, deviceId]
|
|
||||||
);
|
|
||||||
if (!updateResult.affectedRows) {
|
|
||||||
return res.status(404).json({ error: 'Client not found' });
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.json({
|
|
||||||
screen: screenRows[0],
|
|
||||||
screenSlug: slug,
|
|
||||||
command: command,
|
|
||||||
connectionId: connectionId || null,
|
|
||||||
deviceId: deviceId,
|
|
||||||
clientName: clientName,
|
|
||||||
ok: true
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user