Files
pulse-signage/src/web/routes/admin/client-commands.js
T

269 lines
11 KiB
JavaScript

// Admin client command routes for connected screens.
const { commitDeviceBinding } = require('#src/player/onboarding');
module.exports = function registerScreenCommandRoutes(app, deps) {
const pool = deps.pool;
const common = deps.common;
const forwardPlayerCommand = deps.forwardPlayerCommand;
const getScreenConnections = deps.getScreenConnections;
const isClientNameAvailable = deps.isClientNameAvailable;
const withClientNameReservation = deps.withClientNameReservation;
const broadcastDashboardState = deps.broadcastDashboardState;
const requirePermission = deps.requirePermission;
app.post('/clients/:slug/commands', requirePermission('clients.allow'), async function (req, res, next) {
try {
const slug = String(req.params.slug || '').trim();
const command = String((req.body && req.body.command) || req.query.command || '').trim().toLowerCase();
const connectionId = String((req.body && (req.body.connectionId || req.body.clientId)) || req.query.connectionId || req.query.clientId || '').trim();
const blackoutValue = req.body && Object.prototype.hasOwnProperty.call(req.body, 'blackout')
? req.body.blackout
: req.query.blackout;
if (!slug) {
return res.status(400).json({ error: 'Screen slug is required' });
}
if (!command) {
return res.status(400).json({ error: 'Command is required' });
}
const [screenRows] = await pool.query('SELECT id, name, slug FROM d_screens WHERE slug = ?', [slug]);
if (!screenRows.length) {
return res.status(404).json({ error: 'Screen not found' });
}
if (command === 'setclientname') {
const deviceId = String((req.body && (req.body.deviceId || req.body.clientId)) || req.query.deviceId || req.query.clientId || '').trim();
const clientName = String((req.body && req.body.clientName) || req.query.clientName || '').trim();
if (!deviceId) {
return res.status(400).json({ error: 'Device ID is required' });
}
if (!clientName) {
return res.status(400).json({ error: 'Client name is required' });
}
const [currentRows] = await pool.query(
`SELECT client_name
FROM d_onboarding_devices
WHERE device_id = ?
LIMIT 1`,
[deviceId]
);
const onboardingRow = currentRows[0] || null;
const currentName = String(onboardingRow && onboardingRow.client_name ? onboardingRow.client_name : '').trim();
if (currentName && currentName.toLowerCase() === clientName.toLowerCase()) {
return res.json({
screen: screenRows[0],
screenSlug: slug,
command: command,
connectionId: connectionId || null,
deviceId: deviceId,
clientName: currentName,
ok: true,
unchanged: true
});
}
if (typeof withClientNameReservation !== 'function') {
return res.status(500).json({ error: 'Client name reservation is unavailable.' });
}
return withClientNameReservation(pool, clientName, async function () {
let liveConnections = [];
try {
const [screenSlugs] = await pool.query('SELECT slug FROM d_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 = await isClientNameAvailable(pool, clientName, deviceId, liveConnections);
if (!available) {
return res.status(409).json({ error: 'Client name already exists.' });
}
if (!onboardingRow) {
await forwardPlayerCommand(slug, {
command: command,
clientName: clientName,
clientId: connectionId || deviceId || null,
deviceId: deviceId || null
}, connectionId || deviceId || undefined);
if (typeof broadcastDashboardState === 'function') {
await broadcastDashboardState();
}
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 d_onboarding_devices pod
JOIN d_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
});
});
}
if (command === 'moveclient') {
const deviceId = String((req.body && (req.body.deviceId || req.body.clientId)) || req.query.deviceId || req.query.clientId || '').trim();
const clientName = String((req.body && req.body.clientName) || req.query.clientName || '').trim();
const targetScreenSlug = String((req.body && (req.body.targetScreenSlug || req.body.screenSlug)) || req.query.targetScreenSlug || req.query.screenSlug || '').trim();
if (!deviceId) {
return res.status(400).json({ error: 'Device ID is required' });
}
if (!targetScreenSlug) {
return res.status(400).json({ error: 'Target screen is required' });
}
const [currentRows] = await pool.query(
`SELECT d.client_name, s.slug AS current_screen_slug
FROM d_onboarding_devices d
LEFT JOIN d_screens s ON s.id = d.screen_id
WHERE d.device_id = ?
LIMIT 1`,
[deviceId]
);
const onboardingRow = currentRows[0] || null;
const resolvedClientName = String(clientName || onboardingRow && onboardingRow.client_name || '').trim();
const currentScreenSlug = String(onboardingRow && onboardingRow.current_screen_slug || '').trim();
if (!resolvedClientName) {
return res.status(400).json({ error: 'Client name is required' });
}
if (currentScreenSlug && currentScreenSlug === targetScreenSlug) {
return res.json({
screen: screenRows[0],
screenSlug: slug,
command: command,
connectionId: connectionId || null,
deviceId: deviceId,
clientName: resolvedClientName,
targetScreenSlug: targetScreenSlug,
ok: true,
unchanged: true
});
}
if (typeof commitDeviceBinding !== 'function') {
return res.status(500).json({ error: 'Client binding is unavailable.' });
}
let liveConnections = [];
try {
const [screenSlugs] = await pool.query('SELECT slug FROM d_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 status = await commitDeviceBinding(pool, deviceId, resolvedClientName, targetScreenSlug, isClientNameAvailable, liveConnections);
const targetPlayerRecord = typeof common.fetchScreenPlayerRecord === 'function'
? await common.fetchScreenPlayerRecord(pool, targetScreenSlug)
: null;
const targetBaseUrl = String(
targetPlayerRecord && targetPlayerRecord.public_base_url ||
(typeof common.fetchPlayerPublicBaseUrl === 'function' ? await common.fetchPlayerPublicBaseUrl(pool) : '') ||
''
).trim().replace(/\/$/, '');
const targetPlayerUrl = targetBaseUrl ? `${targetBaseUrl}/screen/${encodeURIComponent(targetScreenSlug)}` : `/screen/${encodeURIComponent(targetScreenSlug)}`;
await forwardPlayerCommand(slug, {
command: 'redirect',
url: targetPlayerUrl
}, connectionId || deviceId || undefined);
if (typeof broadcastDashboardState === 'function') {
await broadcastDashboardState();
}
return res.json({
screen: screenRows[0],
screenSlug: slug,
command: command,
connectionId: connectionId || null,
deviceId: deviceId,
clientName: status ? status.client_name : resolvedClientName,
targetScreenSlug: targetScreenSlug,
playerUrl: targetPlayerUrl,
ok: true
});
}
const commandPayload = req.body && typeof req.body === 'object' && !Array.isArray(req.body)
? Object.assign({}, req.body, { command: command })
: { command: command };
if (command === 'blackout' && blackoutValue !== undefined && commandPayload && typeof commandPayload === 'object') {
commandPayload.blackout = blackoutValue;
}
const result = connectionId
? await forwardPlayerCommand(slug, commandPayload, connectionId)
: await forwardPlayerCommand(slug, commandPayload);
if (typeof broadcastDashboardState === 'function') {
await broadcastDashboardState();
}
return res.json(Object.assign({
screen: screenRows[0],
screenSlug: slug,
command: command,
connectionId: connectionId || null
}, result && typeof result === 'object' ? result : {}));
} catch (error) {
next(error);
}
});
};