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 @@
- | Name | Dimensions | Templates | Actions |
+
+
+ | Name |
+ Dimensions |
+ Templates |
+ Actions |
+
+
{{#if canvasSizes.length}}
{{#each canvasSizes}}
@@ -23,12 +32,20 @@
{{width}}x{{height}} |
{{templateCount}} |
-
+ {{#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}}
|
{{/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 @@
- | Name | Slides | Actions |
+
+
+ | Name |
+ Slides |
+ Actions |
+
+
{{#if playlists.length}}
{{#each playlists}}
@@ -22,12 +30,20 @@
{{name}} |
{{slideCount}} slide{{#unless slideCountIsOne}}s{{/unless}} |
-
+ {{#if (anyPermission ../currentUser 'playlists.update' 'playlists.delete')}}
+
+ {{#if (hasPermission ../currentUser 'playlists.update')}}
+ Edit
+ {{/if}}
+ {{#if (hasPermission ../currentUser 'playlists.delete')}}
+
+ {{/if}}
+
+ {{else}}
+ -
+ {{/if}}
|
{{/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 @@
-