From 9f08ccfea13ce3efb745987e4823b1dd0688b432 Mon Sep 17 00:00:00 2001 From: Mark Rapson Date: Wed, 22 Jul 2026 01:26:04 +0100 Subject: [PATCH] Bump version to 1.4.6 --- package.json | 2 +- src/db.js | 41 ++++++++++-- src/rbac.js | 25 +++---- src/web.js | 1 + src/web/public/css/theme-custom.css | 22 ++++++- src/web/public/js/dashboard/dashboard-page.js | 6 +- src/web/routes/admin-content.js | 12 ++-- src/web/routes/admin-manage.js | 16 ++--- src/web/routes/admin-pages.js | 8 +-- src/web/routes/admin-rbac.js | 66 ++++++++++++++++--- src/web/routes/admin-users.js | 8 +-- src/web/views/canvas-sizes/list.hbs | 37 ++++++++--- src/web/views/clients/list.hbs | 2 +- src/web/views/playlists/list.hbs | 36 +++++++--- src/web/views/rbac/edit.hbs | 63 +++++++++--------- src/web/views/rbac/list.hbs | 34 ++++++---- src/web/views/screens/list.hbs | 38 ++++++++--- src/web/views/slides/list.hbs | 36 +++++++--- src/web/views/templates/list.hbs | 37 ++++++++--- src/web/views/users/list.hbs | 36 ++++++---- 20 files changed, 363 insertions(+), 163 deletions(-) diff --git a/package.json b/package.json index 76c0064..a139747 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pulse-signage", - "version": "1.4.5", + "version": "1.4.6", "private": false, "description": "Pulse Signage application with MySQL and media uploads", "repository": { diff --git a/src/db.js b/src/db.js index 9b1f1e1..5564df3 100644 --- a/src/db.js +++ b/src/db.js @@ -205,11 +205,17 @@ function getLegacyPermissionTargets(permissionKey) { if (normalizedKey === 'screens.allow') { return ['clients.allow']; } + if (actionKey === 'edit') { + return [`${sectionKey}.update`]; + } + if (actionKey === 'update') { + return [`${sectionKey}.update`]; + } if (actionKey === 'view') { return [`${sectionKey}.read`]; } if (actionKey === 'manage') { - return [`${sectionKey}.read`, `${sectionKey}.create`, `${sectionKey}.edit`, `${sectionKey}.delete`]; + return [`${sectionKey}.read`, `${sectionKey}.create`, `${sectionKey}.update`, `${sectionKey}.delete`]; } return [normalizedKey].filter(Boolean); @@ -248,6 +254,14 @@ async function backfillLegacyRbacSchema(pool) { JOIN permissions p ON p.id = rp.permission_id` ); + const permissionIdByKey = new Map(); + for (const row of permissionRows || []) { + const currentKey = getPermissionKey(row); + if (currentKey) { + permissionIdByKey.set(currentKey, Number(row.id)); + } + } + const rolePermissionTargets = new Map(); const desiredPermissionKeys = new Set(PERMISSIONS.map(function (permission) { return permission.key; @@ -268,7 +282,7 @@ async function backfillLegacyRbacSchema(pool) { for (const row of rolePermissionRows || []) { const currentKey = getPermissionKey(row); const targetKeys = getLegacyPermissionTargets(currentKey); - if (currentKey.endsWith('.view') || currentKey.endsWith('.manage') || currentKey === 'screens.allow') { + if (currentKey.endsWith('.view') || currentKey.endsWith('.manage') || currentKey.endsWith('.edit') || currentKey === 'screens.allow') { for (const targetKey of targetKeys) { addRoleTarget(Number(row.role_id), targetKey); } @@ -279,8 +293,21 @@ async function backfillLegacyRbacSchema(pool) { for (const row of permissionRows || []) { const currentKey = getPermissionKey(row); - if (currentKey.endsWith('.view') || currentKey.endsWith('.manage') || currentKey === 'screens.allow') { - legacyPermissionRowIds.push(Number(row.id)); + const permissionId = Number(row.id); + if (currentKey.endsWith('.edit')) { + const targetKey = currentKey.replace(/\.edit$/, '.update'); + const targetPermissionId = permissionIdByKey.get(targetKey); + if (targetPermissionId) { + legacyPermissionRowIds.push(permissionId); + } else if (targetKey) { + await pool.query('UPDATE permissions SET permission_key = ? WHERE id = ?', [targetKey, permissionId]); + permissionIdByKey.delete(currentKey); + permissionIdByKey.set(targetKey, permissionId); + } + continue; + } + if (currentKey.endsWith('.view') || currentKey.endsWith('.manage') || currentKey.endsWith('.edit') || currentKey === 'screens.allow') { + legacyPermissionRowIds.push(permissionId); } } @@ -353,11 +380,11 @@ async function backfillLegacyRbacSchema(pool) { } const [currentPermissionRows] = await pool.query('SELECT id, permission_key FROM permissions'); - const permissionIdByKey = new Map(); + const permissionIdByKeyAfterBackfill = new Map(); for (const row of currentPermissionRows || []) { const currentKey = getPermissionKey(row); if (currentKey) { - permissionIdByKey.set(currentKey, Number(row.id)); + permissionIdByKeyAfterBackfill.set(currentKey, Number(row.id)); } } @@ -365,7 +392,7 @@ async function backfillLegacyRbacSchema(pool) { for (const [roleId, permissionKeys] of rolePermissionTargets.entries()) { const expandedPermissionKeys = normalizePermissionKeys(Array.from(permissionKeys.values())); for (const permissionKey of expandedPermissionKeys) { - const permissionId = permissionIdByKey.get(permissionKey); + const permissionId = permissionIdByKeyAfterBackfill.get(permissionKey); if (!permissionId) { continue; } diff --git a/src/rbac.js b/src/rbac.js index 1589ee9..3728dad 100644 --- a/src/rbac.js +++ b/src/rbac.js @@ -27,7 +27,7 @@ const PERMISSION_SECTIONS = [ actions: [ { key: 'read', name: 'Read', description: 'View the screen list and open screen details.' }, { key: 'create', name: 'Create', description: 'Create new screens.' }, - { key: 'edit', name: 'Update', description: 'Edit screens.' }, + { key: 'update', name: 'Update', description: 'Update screens.' }, { key: 'delete', name: 'Delete', description: 'Delete screens.' } ] }, @@ -39,7 +39,7 @@ const PERMISSION_SECTIONS = [ actions: [ { key: 'read', name: 'Read', description: 'View playlists and playlist contents.' }, { key: 'create', name: 'Create', description: 'Create new playlists.' }, - { key: 'edit', name: 'Update', description: 'Edit playlists and playlist slides.' }, + { key: 'update', name: 'Update', description: 'Update playlists and playlist slides.' }, { key: 'delete', name: 'Delete', description: 'Delete playlists.' } ] }, @@ -51,7 +51,7 @@ const PERMISSION_SECTIONS = [ actions: [ { key: 'read', name: 'Read', description: 'View slides.' }, { key: 'create', name: 'Create', description: 'Create new slides.' }, - { key: 'edit', name: 'Update', description: 'Edit slide content.' }, + { key: 'update', name: 'Update', description: 'Update slide content.' }, { key: 'delete', name: 'Delete', description: 'Delete slides.' } ] }, @@ -63,7 +63,7 @@ const PERMISSION_SECTIONS = [ actions: [ { key: 'read', name: 'Read', description: 'View slide templates.' }, { key: 'create', name: 'Create', description: 'Create new slide templates.' }, - { key: 'edit', name: 'Update', description: 'Edit slide templates.' }, + { key: 'update', name: 'Update', description: 'Update slide templates.' }, { key: 'delete', name: 'Delete', description: 'Delete slide templates.' } ] }, @@ -75,7 +75,7 @@ const PERMISSION_SECTIONS = [ actions: [ { key: 'read', name: 'Read', description: 'View canvas sizes.' }, { key: 'create', name: 'Create', description: 'Create new canvas sizes.' }, - { key: 'edit', name: 'Update', description: 'Edit canvas sizes.' }, + { key: 'update', name: 'Update', description: 'Update canvas sizes.' }, { key: 'delete', name: 'Delete', description: 'Delete canvas sizes.' } ] }, @@ -87,7 +87,7 @@ const PERMISSION_SECTIONS = [ actions: [ { key: 'read', name: 'Read', description: 'View users and role assignments.' }, { key: 'create', name: 'Create', description: 'Create new users.' }, - { key: 'edit', name: 'Update', description: 'Edit users, passwords, and role assignments.' }, + { key: 'update', name: 'Update', description: 'Update users, passwords, and role assignments.' }, { key: 'delete', name: 'Delete', description: 'Delete users.' } ] }, @@ -99,7 +99,7 @@ const PERMISSION_SECTIONS = [ actions: [ { key: 'read', name: 'Read', description: 'View roles and permissions.' }, { key: 'create', name: 'Create', description: 'Create new roles.' }, - { key: 'edit', name: 'Update', description: 'Edit role details and permissions.' }, + { key: 'update', name: 'Update', description: 'Update role details and permissions.' }, { key: 'delete', name: 'Delete', description: 'Delete roles.' } ] } @@ -139,22 +139,23 @@ function normalizePermissionKeys(permissionKeys) { return; } - normalized.push(normalizedPermissionKey); - const parts = normalizedPermissionKey.split('.'); if (parts.length !== 2) { + normalized.push(normalizedPermissionKey); return; } const sectionKey = parts[0]; const actionKey = parts[1]; - if (actionKey === 'create' || actionKey === 'edit' || actionKey === 'delete') { + normalized.push(`${sectionKey}.${actionKey}`); + + if (actionKey === 'create' || actionKey === 'update' || actionKey === 'delete') { normalized.push(`${sectionKey}.read`); } if (actionKey === 'manage') { normalized.push(`${sectionKey}.read`); normalized.push(`${sectionKey}.create`); - normalized.push(`${sectionKey}.edit`); + normalized.push(`${sectionKey}.update`); normalized.push(`${sectionKey}.delete`); } if (actionKey === 'view') { @@ -177,7 +178,7 @@ function hasPermission(currentUser, permissionKey) { ? currentUser.permissions : []; - return permissionKeys.map(normalizePermissionKey).includes(normalizedPermissionKey); + return normalizePermissionKeys(permissionKeys).includes(normalizedPermissionKey); } function hasAnyPermission(currentUser, permissionKeys) { diff --git a/src/web.js b/src/web.js index ff0b452..ef429d9 100644 --- a/src/web.js +++ b/src/web.js @@ -197,6 +197,7 @@ async function start() { pages: pages, getAuditUserId: getAuditUserId, rbacData: rbacData, + readArrayField: readArrayField, permissions: require('./rbac').PERMISSIONS, normalizePermissionKeys: require('./rbac').normalizePermissionKeys, requirePermission: requirePermission diff --git a/src/web/public/css/theme-custom.css b/src/web/public/css/theme-custom.css index 1fc76b5..379bb5b 100644 --- a/src/web/public/css/theme-custom.css +++ b/src/web/public/css/theme-custom.css @@ -158,6 +158,16 @@ box-shadow: none; } +.card-header { + position: relative; +} + +.card-header .card-tools { + position: absolute; + right: 20px; + top: 12px; +} + .admin-form-card { box-shadow: none; } @@ -199,7 +209,6 @@ .dashboard-hero-copy { margin: 0.75rem 0 0; - max-width: 42rem; color: var(--bs-secondary-color); } @@ -341,6 +350,10 @@ color: var(--bs-secondary-color); } +.connected-updated-secondary { + font-style: italic; +} + .connection-count, td[data-label="Name"], td[data-label="Title"], @@ -1155,6 +1168,13 @@ td[data-label="Slides"] { white-space: nowrap; } +table.table > :not(caption) > thead > tr > th:last-child, +table.table > :not(caption) > tbody > tr > td:last-child, +td[data-label="Actions"] { + width: 1%; + white-space: nowrap; +} + td[data-label="Actions"] { text-align: right; } diff --git a/src/web/public/js/dashboard/dashboard-page.js b/src/web/public/js/dashboard/dashboard-page.js index 54dd669..ec4f9e7 100644 --- a/src/web/public/js/dashboard/dashboard-page.js +++ b/src/web/public/js/dashboard/dashboard-page.js @@ -91,7 +91,7 @@ var reloadConfirmMessage = 'Reloading will restart the player page. Continue?'; var blackoutCommandValue = blackout ? 'false' : 'true'; - return '
'; + return '
'; } function updateClientActionCell(cell, client) { @@ -229,7 +229,7 @@ } function renderClientRow(client, hasActionsColumn) { - var connectedAt = client.connectedAt ? '
' + escapeHtml(client.connectedAtLabel || formatDashboardDate(client.connectedAt) || client.connectedAt) + '
' + (client.lastSeenAt ? '
' + escapeHtml(client.lastSeenAtLabel || formatDashboardDate(client.lastSeenAt) || client.lastSeenAt) + '
' : '') : 'Unknown'; + var connectedAt = client.connectedAt ? '
' + escapeHtml(client.connectedAtLabel || formatDashboardDate(client.connectedAt) || client.connectedAt) + '
' + (client.lastSeenAt ? '
' + escapeHtml(client.lastSeenAtLabel || formatDashboardDate(client.lastSeenAt) || client.lastSeenAt) + '
' : '') : 'Unknown'; var clientIpValue = normalizeDisplayIp(client.clientIp); var clientIp = clientIpValue ? escapeHtml(clientIpValue) : 'Unknown'; var viewport = client.viewport && client.viewport.width && client.viewport.height ? escapeHtml(client.viewport.width + 'x' + client.viewport.height) : 'Unknown'; @@ -324,7 +324,7 @@ row.setAttribute('data-client-device-id', client.deviceId || ''); row.setAttribute('data-client-screen-slug', client.screen_slug || ''); if (row.cells && row.cells.length >= 6) { - var connectedAt = client.connectedAt ? '
' + escapeHtml(client.connectedAtLabel || formatDashboardDate(client.connectedAt) || client.connectedAt) + '
' + (client.lastSeenAt ? '
' + escapeHtml(client.lastSeenAtLabel || formatDashboardDate(client.lastSeenAt) || client.lastSeenAt) + '
' : '') : 'Unknown'; + var connectedAt = client.connectedAt ? '
' + escapeHtml(client.connectedAtLabel || formatDashboardDate(client.connectedAt) || client.connectedAt) + '
' + (client.lastSeenAt ? '
' + escapeHtml(client.lastSeenAtLabel || formatDashboardDate(client.lastSeenAt) || client.lastSeenAt) + '
' : '') : 'Unknown'; var clientIpValue = normalizeDisplayIp(client.clientIp); var clientIp = clientIpValue ? escapeHtml(clientIpValue) : 'Unknown'; var viewport = client.viewport && client.viewport.width && client.viewport.height ? escapeHtml(client.viewport.width + 'x' + client.viewport.height) : 'Unknown'; diff --git a/src/web/routes/admin-content.js b/src/web/routes/admin-content.js index a476675..e544908 100644 --- a/src/web/routes/admin-content.js +++ b/src/web/routes/admin-content.js @@ -74,7 +74,7 @@ module.exports = function registerAdminContentRoutes(app, deps) { } }); - app.get('/admin/slides/:id/edit', requirePermission('slides.edit'), async function (req, res, next) { + app.get('/admin/slides/:id/edit', requirePermission('slides.update'), async function (req, res, next) { try { const slide = await common.fetchSlideById(pool, Number(req.params.id)); if (!slide) { @@ -111,7 +111,7 @@ module.exports = function registerAdminContentRoutes(app, deps) { } }); - app.post('/admin/slides/:id', requirePermission('slides.edit'), upload.any(), async function (req, res, next) { + app.post('/admin/slides/:id', requirePermission('slides.update'), upload.any(), async function (req, res, next) { try { const slide = await common.fetchSlideById(pool, Number(req.params.id)); if (!slide) { @@ -227,7 +227,7 @@ module.exports = function registerAdminContentRoutes(app, deps) { } }); - app.get('/admin/templates/:id/edit', requirePermission('templates.edit'), async function (req, res, next) { + app.get('/admin/templates/:id/edit', requirePermission('templates.update'), async function (req, res, next) { try { const template = await common.fetchTemplateById(pool, Number(req.params.id)); if (!template) { @@ -240,7 +240,7 @@ module.exports = function registerAdminContentRoutes(app, deps) { } }); - app.post('/admin/templates/:id', requirePermission('templates.edit'), upload.any(), async function (req, res, next) { + app.post('/admin/templates/:id', requirePermission('templates.update'), upload.any(), async function (req, res, next) { try { const template = await common.fetchTemplateById(pool, Number(req.params.id)); if (!template) { @@ -340,7 +340,7 @@ module.exports = function registerAdminContentRoutes(app, deps) { } }); - app.get('/admin/canvas-sizes/:id/edit', requirePermission('canvas-sizes.edit'), async function (req, res, next) { + app.get('/admin/canvas-sizes/:id/edit', requirePermission('canvas-sizes.update'), async function (req, res, next) { try { const canvasSize = await common.fetchCanvasSizeById(pool, Number(req.params.id)); if (!canvasSize) { @@ -352,7 +352,7 @@ module.exports = function registerAdminContentRoutes(app, deps) { } }); - app.post('/admin/canvas-sizes/:id', requirePermission('canvas-sizes.edit'), async function (req, res, next) { + app.post('/admin/canvas-sizes/:id', requirePermission('canvas-sizes.update'), async function (req, res, next) { try { const canvasSize = await common.fetchCanvasSizeById(pool, Number(req.params.id)); if (!canvasSize) { diff --git a/src/web/routes/admin-manage.js b/src/web/routes/admin-manage.js index 8c54c7f..d1f86c3 100644 --- a/src/web/routes/admin-manage.js +++ b/src/web/routes/admin-manage.js @@ -91,7 +91,7 @@ module.exports = function registerAdminManageRoutes(app, deps) { } }); - app.post('/admin/playlists/:id', requirePermission('playlists.edit'), async function (req, res, next) { + app.post('/admin/playlists/:id', requirePermission('playlists.update'), async function (req, res, next) { const connection = await pool.getConnection(); try { const name = String(req.body.name || '').trim(); @@ -273,7 +273,7 @@ module.exports = function registerAdminManageRoutes(app, deps) { } }); - app.post('/admin/playlists/:id/slides', requirePermission('playlists.edit'), async function (req, res, next) { + app.post('/admin/playlists/:id/slides', requirePermission('playlists.update'), async function (req, res, next) { try { const playlist = await common.fetchPlaylistById(pool, Number(req.params.id)); if (!playlist) { @@ -308,7 +308,7 @@ module.exports = function registerAdminManageRoutes(app, deps) { } }); - app.post('/admin/playlists/:id/slides/:playlistSlideId', requirePermission('playlists.edit'), async function (req, res, next) { + app.post('/admin/playlists/:id/slides/:playlistSlideId', requirePermission('playlists.update'), async function (req, res, next) { try { const playlist = await common.fetchPlaylistById(pool, Number(req.params.id)); if (!playlist) { @@ -332,7 +332,7 @@ module.exports = function registerAdminManageRoutes(app, deps) { } }); - app.post('/admin/playlists/:id/slides/:playlistSlideId/move', requirePermission('playlists.edit'), async function (req, res, next) { + app.post('/admin/playlists/:id/slides/:playlistSlideId/move', requirePermission('playlists.update'), async function (req, res, next) { const connection = await pool.getConnection(); try { const playlist = await common.fetchPlaylistById(connection, Number(req.params.id)); @@ -379,7 +379,7 @@ module.exports = function registerAdminManageRoutes(app, deps) { } }); - app.get('/admin/playlists/:id/slides/:playlistSlideId/config', requirePermission('playlists.edit'), async function (req, res, next) { + app.get('/admin/playlists/:id/slides/:playlistSlideId/config', requirePermission('playlists.update'), async function (req, res, next) { try { const playlist = await common.fetchPlaylistById(pool, Number(req.params.id)); if (!playlist) { @@ -416,7 +416,7 @@ module.exports = function registerAdminManageRoutes(app, deps) { } }); - app.post('/admin/playlists/:id/slides/:playlistSlideId/config', requirePermission('playlists.edit'), async function (req, res, next) { + app.post('/admin/playlists/:id/slides/:playlistSlideId/config', requirePermission('playlists.update'), async function (req, res, next) { try { const playlist = await common.fetchPlaylistById(pool, Number(req.params.id)); if (!playlist) { @@ -496,7 +496,7 @@ module.exports = function registerAdminManageRoutes(app, deps) { } }); - app.post('/admin/playlists/:id/slides/:playlistSlideId/delete', requirePermission('playlists.edit'), async function (req, res, next) { + app.post('/admin/playlists/:id/slides/:playlistSlideId/delete', requirePermission('playlists.update'), async function (req, res, next) { try { const playlist = await common.fetchPlaylistById(pool, Number(req.params.id)); if (!playlist) { @@ -541,7 +541,7 @@ module.exports = function registerAdminManageRoutes(app, deps) { } }); - app.post('/admin/screens/:id', requirePermission('screens.edit'), async function (req, res, next) { + app.post('/admin/screens/:id', requirePermission('screens.update'), async function (req, res, next) { try { const name = String(req.body.name || '').trim(); if (!name) { diff --git a/src/web/routes/admin-pages.js b/src/web/routes/admin-pages.js index d11b4cd..7e751ba 100644 --- a/src/web/routes/admin-pages.js +++ b/src/web/routes/admin-pages.js @@ -30,7 +30,7 @@ module.exports = function registerAdminPagesRoutes(app, deps) { } }); - app.get('/admin/screens', requireQueryPermission('screens.read', 'screens.edit'), async function (req, res, next) { + app.get('/admin/screens', requireQueryPermission('screens.read', 'screens.update'), async function (req, res, next) { try { const data = await buildDashboardState(pool); if (req.query.edit) { @@ -47,7 +47,7 @@ module.exports = function registerAdminPagesRoutes(app, deps) { } }); - app.get('/admin/screens/:id/edit', requirePermission('screens.edit'), async function (req, res, next) { + app.get('/admin/screens/:id/edit', requirePermission('screens.update'), async function (req, res, next) { try { const screen = await common.fetchScreenById(pool, Number(req.params.id)); if (!screen) { @@ -60,7 +60,7 @@ module.exports = function registerAdminPagesRoutes(app, deps) { } }); - app.get('/admin/playlists', requireQueryPermission('playlists.read', 'playlists.edit'), async function (req, res, next) { + app.get('/admin/playlists', requireQueryPermission('playlists.read', 'playlists.update'), async function (req, res, next) { try { const data = await common.fetchAdminData(pool); if (req.query.edit) { @@ -80,7 +80,7 @@ module.exports = function registerAdminPagesRoutes(app, deps) { res.send(pages.renderPlaylistFormPage(req.query.message ? String(req.query.message) : '', req.currentUser)); }); - app.get('/admin/playlists/:id/edit', requirePermission('playlists.edit'), async function (req, res, next) { + app.get('/admin/playlists/:id/edit', requirePermission('playlists.update'), async function (req, res, next) { try { const playlist = await common.fetchPlaylistById(pool, Number(req.params.id)); if (!playlist) { diff --git a/src/web/routes/admin-rbac.js b/src/web/routes/admin-rbac.js index e228d33..bb04515 100644 --- a/src/web/routes/admin-rbac.js +++ b/src/web/routes/admin-rbac.js @@ -4,6 +4,7 @@ module.exports = function registerAdminRbacRoutes(app, deps) { const getAuditUserId = deps.getAuditUserId; const rbacData = deps.rbacData; const permissions = Array.isArray(deps.permissions) ? deps.permissions : []; + const readArrayField = deps.readArrayField; const normalizePermissionKeys = deps.normalizePermissionKeys; const requirePermission = deps.requirePermission; @@ -222,7 +223,7 @@ module.exports = function registerAdminRbacRoutes(app, deps) { } }); - app.get('/admin/rbac/:id/edit', requirePermission('rbac.edit'), async function (req, res, next) { + app.get('/admin/rbac/:id/edit', requirePermission('rbac.update'), async function (req, res, next) { try { const roleId = Number(req.params.id); if (!Number.isInteger(roleId) || roleId <= 0) { @@ -247,7 +248,7 @@ module.exports = function registerAdminRbacRoutes(app, deps) { } }); - app.post('/admin/rbac/:id', requirePermission('rbac.edit'), async function (req, res, next) { + app.post('/admin/rbac/:id', requirePermission('rbac.update'), async function (req, res, next) { try { const roleId = Number(req.params.id); if (!Number.isInteger(roleId) || roleId <= 0) { @@ -261,21 +262,70 @@ module.exports = function registerAdminRbacRoutes(app, deps) { const name = String(req.body.name || '').trim(); const description = String(req.body.description || '').trim(); + const shouldSyncPermissions = Object.prototype.hasOwnProperty.call(req.body || {}, 'permissions_present'); + const shouldSyncUsers = Object.prototype.hasOwnProperty.call(req.body || {}, 'users_present'); + const selectedPermissionKeys = shouldSyncPermissions + ? readArrayField(req.body, ['permission_keys[]', 'permission_keys']) + : []; + const selectedUserIds = shouldSyncUsers + ? readArrayField(req.body, ['user_ids[]', 'user_ids']) + : []; + const normalizedPermissionKeys = shouldSyncPermissions ? normalizePermissionKeys(selectedPermissionKeys) : []; + const normalizedUserIds = shouldSyncUsers ? normalizeSelectedIds(selectedUserIds) : []; + if (!name) { return res.redirect('/admin/rbac/' + roleId + '/edit?message=' + encodeURIComponent('Role name is required.')); } - await pool.query( - 'UPDATE roles SET name = ?, description = ?, modified_by = ? WHERE id = ?', - [name, description || null, getAuditUserId(req), roleId] - ); + const validPermissionKeys = new Set(permissions.map(function (permission) { + return String(permission.key || '').trim(); + })); + if (shouldSyncPermissions && normalizedPermissionKeys.some(function (permissionKey) { + return !validPermissionKeys.has(permissionKey); + })) { + return res.redirect('/admin/rbac/' + roleId + '/edit?message=' + encodeURIComponent('One or more selected permissions are invalid.')); + } + + let availableUsers = []; + if (shouldSyncUsers) { + availableUsers = await rbacData.fetchUsersWithRoles(pool); + } + const validUserIds = new Set(availableUsers.map(function (user) { + return Number(user.id); + })); + if (shouldSyncUsers && normalizedUserIds.some(function (userId) { + return !validUserIds.has(userId); + })) { + return res.redirect('/admin/rbac/' + roleId + '/edit?message=' + encodeURIComponent('One or more selected users are invalid.')); + } + + const connection = await pool.getConnection(); + try { + await connection.beginTransaction(); + await connection.query( + 'UPDATE roles SET name = ?, description = ?, modified_by = ? WHERE id = ?', + [name, description || null, getAuditUserId(req), roleId] + ); + if (shouldSyncPermissions) { + await rbacData.syncRolePermissions(connection, roleId, normalizedPermissionKeys); + } + if (shouldSyncUsers) { + await rbacData.syncRoleUsers(connection, roleId, normalizedUserIds); + } + await connection.commit(); + } catch (error) { + await connection.rollback(); + throw error; + } finally { + connection.release(); + } res.redirect('/admin/rbac/' + roleId + '/edit?message=' + encodeURIComponent('Role updated.')); } catch (error) { next(error); } }); - app.post('/admin/rbac/:id/permissions', requirePermission('rbac.edit'), async function (req, res, next) { + app.post('/admin/rbac/:id/permissions', requirePermission('rbac.update'), async function (req, res, next) { try { const roleId = Number(req.params.id); if (!Number.isInteger(roleId) || roleId <= 0) { @@ -309,7 +359,7 @@ module.exports = function registerAdminRbacRoutes(app, deps) { } }); - app.post('/admin/rbac/:id/users', requirePermission('rbac.edit'), async function (req, res, next) { + app.post('/admin/rbac/:id/users', requirePermission('rbac.update'), async function (req, res, next) { try { const roleId = Number(req.params.id); if (!Number.isInteger(roleId) || roleId <= 0) { diff --git a/src/web/routes/admin-users.js b/src/web/routes/admin-users.js index 1bfc100..7342c6c 100644 --- a/src/web/routes/admin-users.js +++ b/src/web/routes/admin-users.js @@ -75,7 +75,7 @@ module.exports = function registerAdminUsersRoutes(app, deps) { }); }); - app.get('/admin/users/:id/edit', requirePermission('users.edit'), async function (req, res, next) { + app.get('/admin/users/:id/edit', requirePermission('users.update'), async function (req, res, next) { try { const userId = Number(req.params.id); if (!Number.isInteger(userId) || userId <= 0) { @@ -164,7 +164,7 @@ module.exports = function registerAdminUsersRoutes(app, deps) { } }); - app.post('/admin/users/:id/roles', requirePermission('users.edit'), async function (req, res, next) { + app.post('/admin/users/:id/roles', requirePermission('users.update'), async function (req, res, next) { try { const userId = Number(req.params.id); if (!Number.isInteger(userId) || userId <= 0) { @@ -192,7 +192,7 @@ module.exports = function registerAdminUsersRoutes(app, deps) { } }); - app.post('/admin/users/:id/username', requirePermission('users.edit'), async function (req, res, next) { + app.post('/admin/users/:id/username', requirePermission('users.update'), async function (req, res, next) { try { const userId = Number(req.params.id); const name = String(req.body.name || '').trim(); @@ -226,7 +226,7 @@ module.exports = function registerAdminUsersRoutes(app, deps) { } }); - app.post('/admin/users/:id/password', requirePermission('users.edit'), async function (req, res, next) { + app.post('/admin/users/:id/password', requirePermission('users.update'), async function (req, res, next) { try { const userId = Number(req.params.id); const password = String(req.body.password || ''); diff --git a/src/web/views/canvas-sizes/list.hbs b/src/web/views/canvas-sizes/list.hbs index 025c9c0..00a4a90 100644 --- a/src/web/views/canvas-sizes/list.hbs +++ b/src/web/views/canvas-sizes/list.hbs @@ -8,13 +8,22 @@

Saved sizes

- + {{#if (hasPermission currentUser 'canvas-sizes.create')}} + + {{/if}}
- + + + + + + + + {{#if canvasSizes.length}} {{#each canvasSizes}} @@ -23,12 +32,20 @@ {{/each}} diff --git a/src/web/views/clients/list.hbs b/src/web/views/clients/list.hbs index 7143cbd..198d22c 100644 --- a/src/web/views/clients/list.hbs +++ b/src/web/views/clients/list.hbs @@ -51,7 +51,7 @@ {{#if connectedAt}}
{{connectedAtLabel}}
{{#if lastSeenAt}} -
{{lastSeenAtLabel}}
+
{{lastSeenAtLabel}}
{{/if}} {{else}} Unknown diff --git a/src/web/views/playlists/list.hbs b/src/web/views/playlists/list.hbs index 1aef53b..2e7afc1 100644 --- a/src/web/views/playlists/list.hbs +++ b/src/web/views/playlists/list.hbs @@ -8,13 +8,21 @@

Existing playlists

- + {{#if (hasPermission currentUser 'playlists.create')}} + + {{/if}}
NameDimensionsTemplatesActions
NameDimensionsTemplatesActions
{{width}}x{{height}} {{templateCount}} -
- Edit -
- -
-
+ {{#if (anyPermission ../currentUser 'canvas-sizes.update' 'canvas-sizes.delete')}} +
+ {{#if (hasPermission ../currentUser 'canvas-sizes.update')}} + Edit + {{/if}} + {{#if (hasPermission ../currentUser 'canvas-sizes.delete')}} +
+ +
+ {{/if}} +
+ {{else}} + - + {{/if}}
- + + + + + + + {{#if playlists.length}} {{#each playlists}} @@ -22,12 +30,20 @@ {{/each}} diff --git a/src/web/views/rbac/edit.hbs b/src/web/views/rbac/edit.hbs index 3e6932f..d6cfd49 100644 --- a/src/web/views/rbac/edit.hbs +++ b/src/web/views/rbac/edit.hbs @@ -5,13 +5,16 @@ -
-
-
-
-

Role details

-
-
+ + + + +
+
+
+
+

Role details

+
@@ -22,24 +25,20 @@
- - -
-
-

Users

-
+ +
+
+

Users

+

Choose the users who should belong to this role.

@@ -74,17 +73,15 @@
Create at least one user before assigning this role.
{{/if}} -
- -
-
- -
-
-
-

Permissions

+
-
+
+ +
+
+
+

Permissions

+

Each section below is a resource. Pick the actions this role should have for that resource.

{{#if permissionGroups.length}} @@ -125,10 +122,12 @@
- +
-
\ No newline at end of file + + +
\ No newline at end of file diff --git a/src/web/views/rbac/list.hbs b/src/web/views/rbac/list.hbs index b63030c..df77e72 100644 --- a/src/web/views/rbac/list.hbs +++ b/src/web/views/rbac/list.hbs @@ -10,9 +10,11 @@

Existing roles

-
- Add role -
+ {{#if (hasPermission currentUser 'rbac.create')}} +
+ Add role +
+ {{/if}}
NameSlidesActions
NameSlidesActions
{{name}} {{slideCount}} slide{{#unless slideCountIsOne}}s{{/unless}} -
- Edit -
- -
-
+ {{#if (anyPermission ../currentUser 'playlists.update' 'playlists.delete')}} +
+ {{#if (hasPermission ../currentUser 'playlists.update')}} + Edit + {{/if}} + {{#if (hasPermission ../currentUser 'playlists.delete')}} +
+ +
+ {{/if}} +
+ {{else}} + - + {{/if}}
@@ -46,16 +48,22 @@ {{/each}} diff --git a/src/web/views/screens/list.hbs b/src/web/views/screens/list.hbs index e369a84..2746307 100644 --- a/src/web/views/screens/list.hbs +++ b/src/web/views/screens/list.hbs @@ -8,13 +8,23 @@

Existing screens

- + {{#if (hasPermission currentUser 'screens.create')}} + + {{/if}}
{{user_count}} {{permission_count}} -
- Edit - {{#unless user_count}} -
- -
- {{else}} - In use - {{/unless}} -
+ {{#if (anyPermission ../currentUser 'rbac.update' 'rbac.delete')}} +
+ {{#if (hasPermission ../currentUser 'rbac.update')}} + Edit + {{/if}} + {{#if (hasPermission ../currentUser 'rbac.delete')}} + {{#unless user_count}} +
+ +
+ {{else}} + In use + {{/unless}} + {{/if}} +
+ {{/if}}
- + + + + + + + + + {{#if screens.length}} {{#each screens}} @@ -30,12 +40,20 @@ {{/if}} {{/each}} diff --git a/src/web/views/slides/list.hbs b/src/web/views/slides/list.hbs index ef4af1c..4f3c59a 100644 --- a/src/web/views/slides/list.hbs +++ b/src/web/views/slides/list.hbs @@ -8,13 +8,21 @@

Existing slides

- + {{#if (hasPermission currentUser 'slides.create')}} + + {{/if}}
NamePlayer URLPlaylistConnected clientsActions
NamePlayer URLPlaylistConnected clientsActions
-
- Edit -
- -
-
+ {{#if (anyPermission ../currentUser 'screens.update' 'screens.delete')}} +
+ {{#if (hasPermission ../currentUser 'screens.update')}} + Edit + {{/if}} + {{#if (hasPermission ../currentUser 'screens.delete')}} +
+ +
+ {{/if}} +
+ {{else}} + - + {{/if}}
- + + + + + + + {{#if slides.length}} {{#each slides}} @@ -22,12 +30,20 @@ {{/each}} diff --git a/src/web/views/templates/list.hbs b/src/web/views/templates/list.hbs index 6fab76a..24cef44 100644 --- a/src/web/views/templates/list.hbs +++ b/src/web/views/templates/list.hbs @@ -8,13 +8,22 @@

Existing templates

- + {{#if (hasPermission currentUser 'templates.create')}} + + {{/if}}
TitleTemplateActions
TitleTemplateActions
{{title}} {{template_name}} -
- Edit -
- -
-
+ {{#if (anyPermission ../currentUser 'slides.update' 'slides.delete')}} +
+ {{#if (hasPermission ../currentUser 'slides.update')}} + Edit + {{/if}} + {{#if (hasPermission ../currentUser 'slides.delete')}} +
+ +
+ {{/if}} +
+ {{else}} + - + {{/if}}
- + + + + + + + + {{#if templates.length}} {{#each templates}} @@ -23,12 +32,20 @@ {{/each}} diff --git a/src/web/views/users/list.hbs b/src/web/views/users/list.hbs index edea1b0..b03a456 100644 --- a/src/web/views/users/list.hbs +++ b/src/web/views/users/list.hbs @@ -8,9 +8,11 @@

Existing users

-
- Add user -
+ {{#if (hasPermission currentUser 'users.create')}} +
+ Add user +
+ {{/if}}
NameCanvasRegionsActions
NameCanvasRegionsActions
{{canvas_size_width}}x{{canvas_size_height}} {{regionCount}} -
- Edit -
- -
-
+ {{#if (anyPermission ../currentUser 'templates.update' 'templates.delete')}} +
+ {{#if (hasPermission ../currentUser 'templates.update')}} + Edit + {{/if}} + {{#if (hasPermission ../currentUser 'templates.delete')}} +
+ +
+ {{/if}} +
+ {{else}} + - + {{/if}}
@@ -61,16 +63,24 @@ {{/each}}
-
- {{#unless isCurrentUser}} - Edit -
- -
- {{else}} - Protected - {{/unless}} -
+ {{#if (anyPermission ../currentUser 'users.update' 'users.delete')}} +
+ {{#unless isCurrentUser}} + {{#if (hasPermission ../currentUser 'users.update')}} + Edit + {{/if}} + {{#if (hasPermission ../currentUser 'users.delete')}} +
+ +
+ {{/if}} + {{else}} + Protected + {{/unless}} +
+ {{else}} + - + {{/if}}