Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5e7df5e55c | ||
|
|
cfca5bfe3b | ||
|
|
5eff70755b |
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "pulse-signage",
|
"name": "pulse-signage",
|
||||||
"version": "1.4.1",
|
"version": "1.4.2",
|
||||||
"private": false,
|
"private": false,
|
||||||
"description": "Pulse Signage application with MySQL and media uploads",
|
"description": "Pulse Signage application with MySQL and media uploads",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
|||||||
@@ -95,6 +95,79 @@ async function addUserAuditColumns(pool, tableName) {
|
|||||||
await addForeignKeyIfMissing(pool, tableName, 'modified_by', `fk_${tableName}_modified_by`, 'users', 'id', 'SET NULL');
|
await addForeignKeyIfMissing(pool, tableName, 'modified_by', `fk_${tableName}_modified_by`, 'users', 'id', 'SET NULL');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function hasSingleColumnUniqueIndex(pool, tableName, columnName) {
|
||||||
|
const [rows] = await pool.query(
|
||||||
|
`SELECT INDEX_NAME, COUNT(*) AS column_count
|
||||||
|
FROM information_schema.statistics
|
||||||
|
WHERE table_schema = DATABASE()
|
||||||
|
AND table_name = ?
|
||||||
|
AND non_unique = 0
|
||||||
|
AND column_name = ?
|
||||||
|
GROUP BY INDEX_NAME`,
|
||||||
|
[tableName, columnName]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (rows || []).some(function (row) {
|
||||||
|
return Number(row.column_count) === 1;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function addUniqueIndexIfMissing(pool, tableName, columnName, indexName) {
|
||||||
|
const hasUniqueIndex = await hasSingleColumnUniqueIndex(pool, tableName, columnName);
|
||||||
|
if (hasUniqueIndex) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await pool.query(`ALTER TABLE \`${tableName}\` ADD UNIQUE KEY \`${indexName}\` (\`${columnName}\`)`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function dedupePermissionRows(pool) {
|
||||||
|
const [rows] = await pool.query('SELECT id, permission_key FROM permissions ORDER BY id ASC');
|
||||||
|
const canonicalIdByKey = new Map();
|
||||||
|
const duplicateRowsByKey = new Map();
|
||||||
|
|
||||||
|
for (const row of rows || []) {
|
||||||
|
const permissionKey = getPermissionKey(row);
|
||||||
|
const permissionId = Number(row.id);
|
||||||
|
if (!permissionKey || !Number.isInteger(permissionId) || permissionId <= 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!canonicalIdByKey.has(permissionKey)) {
|
||||||
|
canonicalIdByKey.set(permissionKey, permissionId);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!duplicateRowsByKey.has(permissionKey)) {
|
||||||
|
duplicateRowsByKey.set(permissionKey, []);
|
||||||
|
}
|
||||||
|
duplicateRowsByKey.get(permissionKey).push(permissionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!duplicateRowsByKey.size) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [permissionKey, duplicateIds] of duplicateRowsByKey.entries()) {
|
||||||
|
const canonicalId = canonicalIdByKey.get(permissionKey);
|
||||||
|
for (const duplicateId of duplicateIds) {
|
||||||
|
await pool.query(
|
||||||
|
'UPDATE IGNORE role_permissions SET permission_id = ? WHERE permission_id = ?',
|
||||||
|
[canonicalId, duplicateId]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const duplicateIds = [];
|
||||||
|
for (const duplicateList of duplicateRowsByKey.values()) {
|
||||||
|
duplicateIds.push.apply(duplicateIds, duplicateList);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (duplicateIds.length) {
|
||||||
|
await pool.query('DELETE FROM permissions WHERE id IN (?)', [duplicateIds]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function pruneStaleOnboardingDevices(pool) {
|
async function pruneStaleOnboardingDevices(pool) {
|
||||||
await pool.query(
|
await pool.query(
|
||||||
`DELETE FROM player_onboarding_devices
|
`DELETE FROM player_onboarding_devices
|
||||||
@@ -541,6 +614,8 @@ async function ensureSchema(pool) {
|
|||||||
await addColumnIfMissing(pool, 'permissions', 'modified_at', 'TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP');
|
await addColumnIfMissing(pool, 'permissions', 'modified_at', 'TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP');
|
||||||
await addUserAuditColumns(pool, 'permissions');
|
await addUserAuditColumns(pool, 'permissions');
|
||||||
await dropColumnIfPresent(pool, 'permissions', 'perm_key');
|
await dropColumnIfPresent(pool, 'permissions', 'perm_key');
|
||||||
|
await dedupePermissionRows(pool);
|
||||||
|
await addUniqueIndexIfMissing(pool, 'permissions', 'permission_key', 'uq_permissions_permission_key');
|
||||||
|
|
||||||
await pool.query(`
|
await pool.query(`
|
||||||
CREATE TABLE IF NOT EXISTS role_permissions (
|
CREATE TABLE IF NOT EXISTS role_permissions (
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ const PERMISSION_SECTIONS = [
|
|||||||
{ key: 'read', name: 'Read', description: 'View the screen list and open screen details.' },
|
{ key: 'read', name: 'Read', description: 'View the screen list and open screen details.' },
|
||||||
{ key: 'create', name: 'Create', description: 'Create new screens.' },
|
{ key: 'create', name: 'Create', description: 'Create new screens.' },
|
||||||
{ key: 'edit', name: 'Update', description: 'Edit screens and send screen commands.' },
|
{ key: 'edit', name: 'Update', description: 'Edit screens and send screen commands.' },
|
||||||
|
{ key: 'allow', name: 'Allow', description: 'Use the connected client actions on the screen page.' },
|
||||||
{ key: 'delete', name: 'Delete', description: 'Delete screens.' }
|
{ key: 'delete', name: 'Delete', description: 'Delete screens.' }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
+18
@@ -225,6 +225,24 @@ async function start() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
app.use(function (req, res, next) {
|
||||||
|
const pathName = String(req.originalUrl || '');
|
||||||
|
const wantsHtml = !pathName.startsWith('/api/') && (!req.accepts || req.accepts('html'));
|
||||||
|
|
||||||
|
if (!wantsHtml) {
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(404).send(pages.renderErrorPage({
|
||||||
|
statusCode: 404,
|
||||||
|
title: 'Not found',
|
||||||
|
errorTitle: 'Oops! Page not found.',
|
||||||
|
message: 'We could not find the page you were looking for.',
|
||||||
|
backUrl: req.currentUser ? '/admin' : '/login',
|
||||||
|
backLabel: req.currentUser ? 'Back to dashboard' : 'Sign in'
|
||||||
|
}, req.currentUser));
|
||||||
|
});
|
||||||
|
|
||||||
app.use(function (error, req, res, _next) {
|
app.use(function (error, req, res, _next) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
const statusCode = Number(error && (error.statusCode || error.status)) || 500;
|
const statusCode = Number(error && (error.statusCode || error.status)) || 500;
|
||||||
|
|||||||
@@ -206,6 +206,28 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function syncClientActionCell(row, client, hasActionsColumn) {
|
||||||
|
if (!row || !row.cells) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasActionsColumn) {
|
||||||
|
if (row.cells.length > 6) {
|
||||||
|
row.deleteCell(row.cells.length - 1);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var actionCell = row.cells.length > 6 ? row.cells[6] : null;
|
||||||
|
if (!actionCell) {
|
||||||
|
actionCell = row.insertCell(-1);
|
||||||
|
actionCell.setAttribute('data-label', 'Actions');
|
||||||
|
actionCell.className = 'text-end';
|
||||||
|
}
|
||||||
|
|
||||||
|
updateClientActionCell(actionCell, client);
|
||||||
|
}
|
||||||
|
|
||||||
function renderClientRow(client, hasActionsColumn) {
|
function renderClientRow(client, hasActionsColumn) {
|
||||||
var connectedAt = client.connectedAt ? '<div>' + escapeHtml(client.connectedAtLabel || formatDashboardDate(client.connectedAt) || client.connectedAt) + '</div>' + (client.lastSeenAt ? '<div class="subtle">' + escapeHtml(client.lastSeenAtLabel || formatDashboardDate(client.lastSeenAt) || client.lastSeenAt) + '</div>' : '') : '<span class="empty">Unknown</span>';
|
var connectedAt = client.connectedAt ? '<div>' + escapeHtml(client.connectedAtLabel || formatDashboardDate(client.connectedAt) || client.connectedAt) + '</div>' + (client.lastSeenAt ? '<div class="subtle">' + escapeHtml(client.lastSeenAtLabel || formatDashboardDate(client.lastSeenAt) || client.lastSeenAt) + '</div>' : '') : '<span class="empty">Unknown</span>';
|
||||||
var clientIpValue = normalizeDisplayIp(client.clientIp);
|
var clientIpValue = normalizeDisplayIp(client.clientIp);
|
||||||
@@ -317,9 +339,7 @@
|
|||||||
row.cells[3].innerHTML = clientIp;
|
row.cells[3].innerHTML = clientIp;
|
||||||
row.cells[4].innerHTML = viewport;
|
row.cells[4].innerHTML = viewport;
|
||||||
row.cells[5].innerHTML = connectedAt;
|
row.cells[5].innerHTML = connectedAt;
|
||||||
if (hasActionsColumn && row.cells.length >= 7) {
|
syncClientActionCell(row, client, hasActionsColumn);
|
||||||
updateClientActionCell(row.cells[6], client);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var referenceNode = tbody.children[index] || null;
|
var referenceNode = tbody.children[index] || null;
|
||||||
|
|||||||
@@ -234,7 +234,14 @@ module.exports = function registerAdminRbacRoutes(app, deps) {
|
|||||||
return res.status(404).send('Role not found.');
|
return res.status(404).send('Role not found.');
|
||||||
}
|
}
|
||||||
|
|
||||||
res.send(pages.renderRbacEditPage(viewModel.role, req.query.message ? String(req.query.message) : '', req.currentUser, viewModel.permissionGroups, viewModel.users));
|
const currentUserId = req.currentUser ? Number(req.currentUser.id) : null;
|
||||||
|
const users = Array.isArray(viewModel.users)
|
||||||
|
? viewModel.users.filter(function (user) {
|
||||||
|
return Number(user && user.id) !== currentUserId;
|
||||||
|
})
|
||||||
|
: [];
|
||||||
|
|
||||||
|
res.send(pages.renderRbacEditPage(viewModel.role, req.query.message ? String(req.query.message) : '', req.currentUser, viewModel.permissionGroups, users));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
next(error);
|
next(error);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ module.exports = function registerAdminScreenCommandRoutes(app, deps) {
|
|||||||
const withClientNameReservation = deps.withClientNameReservation;
|
const withClientNameReservation = deps.withClientNameReservation;
|
||||||
const requirePermission = deps.requirePermission;
|
const requirePermission = deps.requirePermission;
|
||||||
|
|
||||||
app.post('/admin/screens/:slug/commands', requirePermission('clients.allow'), async function (req, res, next) {
|
app.post('/admin/screens/:slug/commands', requirePermission('screens.allow'), async function (req, res, next) {
|
||||||
try {
|
try {
|
||||||
const slug = String(req.params.slug || '').trim();
|
const slug = String(req.params.slug || '').trim();
|
||||||
const command = String((req.body && req.body.command) || req.query.command || '').trim().toLowerCase();
|
const command = String((req.body && req.body.command) || req.query.command || '').trim().toLowerCase();
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
<h3 class="card-title">Active connections</h3>
|
<h3 class="card-title">Active connections</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body table-responsive p-0">
|
<div class="card-body table-responsive p-0">
|
||||||
<table id="dashboard-clients-table" class="table table-striped w-100 mb-0" data-has-actions-column="{{#if (hasPermission currentUser 'clients.allow')}}true{{else}}false{{/if}}">
|
<table id="dashboard-clients-table" class="table table-striped w-100 mb-0" data-has-actions-column="{{#if (hasPermission currentUser 'screens.allow')}}true{{else}}false{{/if}}">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Client</th>
|
<th>Client</th>
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
<th>IP</th>
|
<th>IP</th>
|
||||||
<th>Viewport</th>
|
<th>Viewport</th>
|
||||||
<th>Connected/Updated</th>
|
<th>Connected/Updated</th>
|
||||||
{{#if (hasPermission currentUser 'clients.allow')}}<th>Actions</th>{{/if}}
|
{{#if (hasPermission currentUser 'screens.allow')}}<th>Actions</th>{{/if}}
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody id="dashboard-clients-table-body">
|
<tbody id="dashboard-clients-table-body">
|
||||||
@@ -57,7 +57,7 @@
|
|||||||
<span class="empty">Unknown</span>
|
<span class="empty">Unknown</span>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</td>
|
</td>
|
||||||
{{#if (hasPermission currentUser 'clients.allow')}}
|
{{#if (hasPermission currentUser 'screens.allow')}}
|
||||||
<td data-label="Actions" class="text-end">
|
<td data-label="Actions" class="text-end">
|
||||||
<div class="actions justify-content-end">
|
<div class="actions justify-content-end">
|
||||||
<form method="post" action="/admin/screens/{{screen_slug}}/commands" class="inline-form" data-confirm-message="Reloading will restart the player page. Continue?" data-async-command>
|
<form method="post" action="/admin/screens/{{screen_slug}}/commands" class="inline-form" data-confirm-message="Reloading will restart the player page. Continue?" data-async-command>
|
||||||
@@ -95,7 +95,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
{{/each}}
|
{{/each}}
|
||||||
{{else}}
|
{{else}}
|
||||||
<tr><td colspan="{{#if (hasPermission currentUser 'clients.allow')}}7{{else}}6{{/if}}" class="empty">No connected clients yet.</td></tr>
|
<tr><td colspan="{{#if (hasPermission currentUser 'screens.allow')}}7{{else}}6{{/if}}" class="empty">No connected clients yet.</td></tr>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
@@ -61,7 +61,7 @@
|
|||||||
<td>
|
<td>
|
||||||
<input class="form-check-input" type="checkbox" name="user_ids[]" value="{{id}}" {{#if isSelected}}checked{{/if}} />
|
<input class="form-check-input" type="checkbox" name="user_ids[]" value="{{id}}" {{#if isSelected}}checked{{/if}} />
|
||||||
</td>
|
</td>
|
||||||
<td><strong>{{name}}</strong></td>
|
<td>{{name}}</td>
|
||||||
<td class="text-body-secondary">{{username}}</td>
|
<td class="text-body-secondary">{{username}}</td>
|
||||||
<td class="text-body-secondary">{{#if roleNames}}{{roleNames}}{{else}}No roles assigned{{/if}}</td>
|
<td class="text-body-secondary">{{#if roleNames}}{{roleNames}}{{else}}No roles assigned{{/if}}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
Reference in New Issue
Block a user