Bump version to 1.4.6
Publish Docker Image / build-and-push (push) Successful in 51s

This commit is contained in:
2026-07-22 01:26:04 +01:00
parent 370ec81c33
commit 9f08ccfea1
20 changed files with 363 additions and 163 deletions
+1 -1
View File
@@ -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": {
+34 -7
View File
@@ -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;
}
+13 -12
View File
@@ -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) {
+1
View File
@@ -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
+21 -1
View File
@@ -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;
}
@@ -91,7 +91,7 @@
var reloadConfirmMessage = 'Reloading will restart the player page. Continue?';
var blackoutCommandValue = blackout ? 'false' : 'true';
return '<div class="d-flex flex-wrap gap-1"><form method="post" action="/admin/clients/' + encodeURIComponent(client.screen_slug) + '/commands" class="d-inline-block m-0" data-confirm-message="' + escapeHtml(reloadConfirmMessage) + '" data-async-command><input type="hidden" name="command" value="reload" /><input type="hidden" name="connectionId" value="' + escapeHtml(client.id) + '" /><button type="submit" class="btn btn-sm btn-danger" data-action="reload"><i class="bi bi-arrow-repeat me-1" aria-hidden="true"></i>Reload</button></form><form method="post" action="/admin/clients/' + encodeURIComponent(client.screen_slug) + '/commands" class="d-inline-block m-0" data-async-command><input type="hidden" name="command" value="previous" /><input type="hidden" name="connectionId" value="' + escapeHtml(client.id) + '" /><button type="submit" class="btn btn-sm btn-outline-secondary" data-action="previous" aria-label="Previous slide"><i class="bi bi-skip-backward-fill" aria-hidden="true"></i></button></form><form method="post" action="/admin/clients/' + encodeURIComponent(client.screen_slug) + '/commands" class="d-inline-block m-0" data-async-command><input type="hidden" name="command" value="next" /><input type="hidden" name="connectionId" value="' + escapeHtml(client.id) + '" /><button type="submit" class="btn btn-sm btn-outline-secondary" data-action="next" aria-label="Next slide"><i class="bi bi-skip-forward-fill" aria-hidden="true"></i></button></form><form method="post" action="/admin/clients/' + encodeURIComponent(client.screen_slug) + '/commands" class="d-inline-block m-0" data-async-command><input type="hidden" name="command" value="pause" /><input type="hidden" name="connectionId" value="' + escapeHtml(client.id) + '" /><button type="submit" class="' + pauseButtonClass + '" data-action="pause"><i class="bi bi-' + pauseButtonIcon + ' me-1" aria-hidden="true"></i>' + pauseButtonLabel + '</button></form><form method="post" action="/admin/clients/' + encodeURIComponent(client.screen_slug) + '/commands" class="d-inline-block m-0" data-async-command><input type="hidden" name="command" value="blackout" /><input type="hidden" name="blackout" value="' + blackoutCommandValue + '" /><input type="hidden" name="connectionId" value="' + escapeHtml(client.id) + '" /><button type="submit" class="' + blackoutButtonClass + '" data-action="blackout"><i class="bi bi-' + blackoutButtonIcon + ' me-1" aria-hidden="true"></i>' + blackoutButtonLabel + '</button></form></div>';
return '<div class="actions justify-content-end"><form method="post" action="/admin/clients/' + encodeURIComponent(client.screen_slug) + '/commands" class="d-inline-block m-0" data-confirm-message="' + escapeHtml(reloadConfirmMessage) + '" data-async-command><input type="hidden" name="command" value="reload" /><input type="hidden" name="connectionId" value="' + escapeHtml(client.id) + '" /><button type="submit" class="btn btn-sm btn-danger" data-action="reload"><i class="bi bi-arrow-repeat me-1" aria-hidden="true"></i>Reload</button></form><form method="post" action="/admin/clients/' + encodeURIComponent(client.screen_slug) + '/commands" class="d-inline-block m-0" data-async-command><input type="hidden" name="command" value="previous" /><input type="hidden" name="connectionId" value="' + escapeHtml(client.id) + '" /><button type="submit" class="btn btn-sm btn-outline-secondary" data-action="previous" aria-label="Previous slide"><i class="bi bi-skip-backward-fill" aria-hidden="true"></i></button></form><form method="post" action="/admin/clients/' + encodeURIComponent(client.screen_slug) + '/commands" class="d-inline-block m-0" data-async-command><input type="hidden" name="command" value="next" /><input type="hidden" name="connectionId" value="' + escapeHtml(client.id) + '" /><button type="submit" class="btn btn-sm btn-outline-secondary" data-action="next" aria-label="Next slide"><i class="bi bi-skip-forward-fill" aria-hidden="true"></i></button></form><form method="post" action="/admin/clients/' + encodeURIComponent(client.screen_slug) + '/commands" class="d-inline-block m-0" data-async-command><input type="hidden" name="command" value="pause" /><input type="hidden" name="connectionId" value="' + escapeHtml(client.id) + '" /><button type="submit" class="' + pauseButtonClass + '" data-action="pause"><i class="bi bi-' + pauseButtonIcon + ' me-1" aria-hidden="true"></i>' + pauseButtonLabel + '</button></form><form method="post" action="/admin/clients/' + encodeURIComponent(client.screen_slug) + '/commands" class="d-inline-block m-0" data-async-command><input type="hidden" name="command" value="blackout" /><input type="hidden" name="blackout" value="' + blackoutCommandValue + '" /><input type="hidden" name="connectionId" value="' + escapeHtml(client.id) + '" /><button type="submit" class="' + blackoutButtonClass + '" data-action="blackout"><i class="bi bi-' + blackoutButtonIcon + ' me-1" aria-hidden="true"></i>' + blackoutButtonLabel + '</button></form></div>';
}
function updateClientActionCell(cell, client) {
@@ -229,7 +229,7 @@
}
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 connected-updated-secondary">' + escapeHtml(client.lastSeenAtLabel || formatDashboardDate(client.lastSeenAt) || client.lastSeenAt) + '</div>' : '') : '<span class="empty">Unknown</span>';
var clientIpValue = normalizeDisplayIp(client.clientIp);
var clientIp = clientIpValue ? escapeHtml(clientIpValue) : '<span class="empty">Unknown</span>';
var viewport = client.viewport && client.viewport.width && client.viewport.height ? escapeHtml(client.viewport.width + 'x' + client.viewport.height) : '<span class="empty">Unknown</span>';
@@ -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 ? '<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 connected-updated-secondary">' + escapeHtml(client.lastSeenAtLabel || formatDashboardDate(client.lastSeenAt) || client.lastSeenAt) + '</div>' : '') : '<span class="empty">Unknown</span>';
var clientIpValue = normalizeDisplayIp(client.clientIp);
var clientIp = clientIpValue ? escapeHtml(clientIpValue) : '<span class="empty">Unknown</span>';
var viewport = client.viewport && client.viewport.width && client.viewport.height ? escapeHtml(client.viewport.width + 'x' + client.viewport.height) : '<span class="empty">Unknown</span>';
+6 -6
View File
@@ -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) {
+8 -8
View File
@@ -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) {
+4 -4
View File
@@ -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) {
+58 -8
View File
@@ -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) {
+4 -4
View File
@@ -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 || '');
+27 -10
View File
@@ -8,13 +8,22 @@
<div class="card card-outline card-primary">
<div class="card-header">
<h3 class="card-title">Saved sizes</h3>
<div class="card-tools">
<a class="btn btn-primary btn-sm" href="/admin/canvas-sizes/new">Add canvas size</a>
</div>
{{#if (hasPermission currentUser 'canvas-sizes.create')}}
<div class="card-tools">
<a class="btn btn-primary btn-sm" href="/admin/canvas-sizes/new">Add canvas size</a>
</div>
{{/if}}
</div>
<div class="card-body table-responsive p-0">
<table class="table table-striped w-100 mb-0">
<thead><tr><th>Name</th><th>Dimensions</th><th>Templates</th><th>Actions</th></tr></thead>
<thead>
<tr>
<th>Name</th>
<th>Dimensions</th>
<th>Templates</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{{#if canvasSizes.length}}
{{#each canvasSizes}}
@@ -23,12 +32,20 @@
<td data-label="Dimensions">{{width}}x{{height}}</td>
<td data-label="Templates">{{templateCount}}</td>
<td data-label="Actions">
<div class="actions">
<a class="btn btn-sm btn-primary" href="/admin/canvas-sizes/{{id}}/edit">Edit</a>
<form class="inline-form" method="post" action="/admin/canvas-sizes/{{id}}/delete" data-confirm-message="Delete this canvas size?">
<button class="btn btn-sm btn-danger" type="submit">Delete</button>
</form>
</div>
{{#if (anyPermission ../currentUser 'canvas-sizes.update' 'canvas-sizes.delete')}}
<div class="actions">
{{#if (hasPermission ../currentUser 'canvas-sizes.update')}}
<a class="btn btn-sm btn-primary" href="/admin/canvas-sizes/{{id}}/edit">Edit</a>
{{/if}}
{{#if (hasPermission ../currentUser 'canvas-sizes.delete')}}
<form class="inline-form" method="post" action="/admin/canvas-sizes/{{id}}/delete" data-confirm-message="Delete this canvas size?">
<button class="btn btn-sm btn-danger" type="submit">Delete</button>
</form>
{{/if}}
</div>
{{else}}
<span class="empty">-</span>
{{/if}}
</td>
</tr>
{{/each}}
+1 -1
View File
@@ -51,7 +51,7 @@
{{#if connectedAt}}
<div>{{connectedAtLabel}}</div>
{{#if lastSeenAt}}
<div class="subtle"><i>{{lastSeenAtLabel}}</i></div>
<div class="subtle connected-updated-secondary">{{lastSeenAtLabel}}</div>
{{/if}}
{{else}}
<span class="empty">Unknown</span>
+26 -10
View File
@@ -8,13 +8,21 @@
<div class="card card-outline card-primary">
<div class="card-header">
<h3 class="card-title">Existing playlists</h3>
<div class="card-tools">
<a class="btn btn-primary btn-sm" href="/admin/playlists/new">Create playlist</a>
</div>
{{#if (hasPermission currentUser 'playlists.create')}}
<div class="card-tools">
<a class="btn btn-primary btn-sm" href="/admin/playlists/new">Create playlist</a>
</div>
{{/if}}
</div>
<div class="card-body table-responsive p-0">
<table class="table table-striped w-100 mb-0">
<thead><tr><th>Name</th><th>Slides</th><th>Actions</th></tr></thead>
<thead>
<tr>
<th>Name</th>
<th>Slides</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{{#if playlists.length}}
{{#each playlists}}
@@ -22,12 +30,20 @@
<td data-label="Name">{{name}}</td>
<td data-label="Slides"><span class="chip">{{slideCount}} slide{{#unless slideCountIsOne}}s{{/unless}}</span></td>
<td data-label="Actions">
<div class="actions">
<a class="btn btn-sm btn-primary" href="/admin/playlists/{{id}}/edit">Edit</a>
<form class="inline-form" method="post" action="/admin/playlists/{{id}}/delete" data-confirm-message="Delete this playlist?">
<button class="btn btn-sm btn-danger" type="submit">Delete</button>
</form>
</div>
{{#if (anyPermission ../currentUser 'playlists.update' 'playlists.delete')}}
<div class="actions">
{{#if (hasPermission ../currentUser 'playlists.update')}}
<a class="btn btn-sm btn-primary" href="/admin/playlists/{{id}}/edit">Edit</a>
{{/if}}
{{#if (hasPermission ../currentUser 'playlists.delete')}}
<form class="inline-form" method="post" action="/admin/playlists/{{id}}/delete" data-confirm-message="Delete this playlist?">
<button class="btn btn-sm btn-danger" type="submit">Delete</button>
</form>
{{/if}}
</div>
{{else}}
<span class="empty">-</span>
{{/if}}
</td>
</tr>
{{/each}}
+31 -32
View File
@@ -5,13 +5,16 @@
</div>
</div>
<div class="row g-4">
<div class="col-12 col-xl-4 d-flex flex-column gap-4">
<div class="card card-outline card-primary admin-form-card">
<div class="card-header">
<h3 class="card-title">Role details</h3>
</div>
<form method="post" action="/admin/rbac/{{role.id}}" id="role-details-form" data-async-save data-async-save-close-url="/admin/rbac">
<form method="post" action="/admin/rbac/{{role.id}}" id="role-edit-form" data-async-save data-async-save-close-url="/admin/rbac">
<input type="hidden" name="permissions_present" value="1" />
<input type="hidden" name="users_present" value="1" />
<div class="row g-4">
<div class="col-12 col-xl-4 d-flex flex-column gap-4">
<div class="card card-outline card-primary admin-form-card">
<div class="card-header">
<h3 class="card-title">Role details</h3>
</div>
<div class="card-body">
<div class="mb-3">
<label for="role-name-edit" class="form-label">Role Name</label>
@@ -22,24 +25,20 @@
<textarea id="role-description-edit" name="description" class="form-control" rows="5">{{role.description}}</textarea>
</div>
</div>
<div class="card-footer d-flex justify-content-end">
<div class="card-footer d-flex justify-content-end gap-2 flex-wrap">
<div class="btn-group" role="group" aria-label="Role actions">
{{{saveActionButtons formId="role-details-form" saveLabel="Save" saveAndCloseLabel="Save and Close" saveAndCloseValue="close" showSaveAndNew=false}}}
{{{saveActionButtons formId="role-edit-form" saveLabel="Save" saveAndCloseLabel="Save and Close" saveAndCloseValue="close" showSaveAndNew=false}}}
</div>
{{#unless role.user_count}}
<form method="post" action="/admin/rbac/{{role.id}}/delete" class="inline-form" data-confirm-message="Delete {{role.name}}?">
<button type="submit" class="btn btn-danger">Delete</button>
</form>
<button type="submit" class="btn btn-danger" form="role-delete-form">Delete</button>
{{/unless}}
</div>
</form>
</div>
<div class="card card-outline card-secondary admin-form-card">
<div class="card-header">
<h3 class="card-title">Users</h3>
</div>
<form method="post" action="/admin/rbac/{{role.id}}/users" id="role-users-form" data-async-save data-async-save-close-url="/admin/rbac/{{role.id}}/edit">
<div class="card card-outline card-secondary admin-form-card">
<div class="card-header">
<h3 class="card-title">Users</h3>
</div>
<div class="card-body p-0">
<div class="px-3 pt-3">
<p class="text-body-secondary mb-3">Choose the users who should belong to this role.</p>
@@ -74,17 +73,15 @@
<div class="alert alert-warning mb-0">Create at least one user before assigning this role.</div>
</div>
{{/if}}
</div>
</form>
</div>
</div>
<div class="col-12 col-xl-8 d-flex flex-column gap-4">
<div class="card card-outline card-secondary admin-form-card">
<div class="card-header">
<h3 class="card-title">Permissions</h3>
</div>
</div>
<form method="post" action="/admin/rbac/{{role.id}}/permissions" id="role-permissions-form" data-async-save data-async-save-close-url="/admin/rbac/{{role.id}}/edit">
</div>
<div class="col-12 col-xl-8 d-flex flex-column gap-4">
<div class="card card-outline card-secondary admin-form-card">
<div class="card-header">
<h3 class="card-title">Permissions</h3>
</div>
<div class="card-body">
<p class="text-body-secondary mb-3">Each section below is a resource. Pick the actions this role should have for that resource.</p>
{{#if permissionGroups.length}}
@@ -125,10 +122,12 @@
</div>
<div class="card-footer d-flex justify-content-end">
<div class="btn-group" role="group" aria-label="Permission actions">
{{{saveActionButtons formId="role-permissions-form" saveLabel="Save" saveAndCloseLabel="Save and Close" saveAndCloseValue="close" showSaveAndNew=false}}}
{{{saveActionButtons formId="role-edit-form" saveLabel="Save" saveAndCloseLabel="Save and Close" saveAndCloseValue="close" showSaveAndNew=false}}}
</div>
</div>
</form>
</div>
</div>
</div>
</div>
</form>
<form method="post" action="/admin/rbac/{{role.id}}/delete" id="role-delete-form" class="d-none" data-confirm-message="Delete {{role.name}}?"></form>
+21 -13
View File
@@ -10,9 +10,11 @@
<div class="card card-outline card-primary admin-form-card">
<div class="card-header">
<h3 class="card-title">Existing roles</h3>
<div class="card-tools">
<a class="btn btn-primary btn-sm" href="/admin/rbac/new">Add role</a>
</div>
{{#if (hasPermission currentUser 'rbac.create')}}
<div class="card-tools">
<a class="btn btn-primary btn-sm" href="/admin/rbac/new">Add role</a>
</div>
{{/if}}
</div>
<div class="card-body table-responsive p-0">
<table class="table table-striped w-100 mb-0 users-table">
@@ -46,16 +48,22 @@
<td data-label="Users">{{user_count}}</td>
<td data-label="Permissions">{{permission_count}}</td>
<td data-label="Actions">
<div class="actions users-row-actions">
<a class="btn btn-sm btn-primary" href="/admin/rbac/{{id}}/edit">Edit</a>
{{#unless user_count}}
<form method="post" action="/admin/rbac/{{id}}/delete" class="inline-form" data-confirm-message="Delete {{name}}?">
<button type="submit" class="btn btn-sm btn-danger">Delete</button>
</form>
{{else}}
<span class="empty">In use</span>
{{/unless}}
</div>
{{#if (anyPermission ../currentUser 'rbac.update' 'rbac.delete')}}
<div class="actions users-row-actions">
{{#if (hasPermission ../currentUser 'rbac.update')}}
<a class="btn btn-sm btn-primary" href="/admin/rbac/{{id}}/edit">Edit</a>
{{/if}}
{{#if (hasPermission ../currentUser 'rbac.delete')}}
{{#unless user_count}}
<form method="post" action="/admin/rbac/{{id}}/delete" class="inline-form" data-confirm-message="Delete {{name}}?">
<button type="submit" class="btn btn-sm btn-danger">Delete</button>
</form>
{{else}}
<span class="empty">In use</span>
{{/unless}}
{{/if}}
</div>
{{/if}}
</td>
</tr>
{{/each}}
+28 -10
View File
@@ -8,13 +8,23 @@
<div class="card card-outline card-primary">
<div class="card-header">
<h3 class="card-title">Existing screens</h3>
<div class="card-tools">
<a class="btn btn-primary btn-sm" href="/admin/screens/new">Add screen</a>
</div>
{{#if (hasPermission currentUser 'screens.create')}}
<div class="card-tools">
<a class="btn btn-primary btn-sm" href="/admin/screens/new">Add screen</a>
</div>
{{/if}}
</div>
<div class="card-body table-responsive p-0">
<table class="table table-striped w-100 mb-0">
<thead><tr><th>Name</th><th>Player URL</th><th>Playlist</th><th>Connected clients</th><th>Actions</th></tr></thead>
<thead>
<tr>
<th>Name</th>
<th>Player URL</th>
<th>Playlist</th>
<th>Connected clients</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{{#if screens.length}}
{{#each screens}}
@@ -30,12 +40,20 @@
{{/if}}
</td>
<td data-label="Actions">
<div class="actions">
<a class="btn btn-sm btn-primary" href="/admin/screens/{{id}}/edit">Edit</a>
<form class="inline-form" method="post" action="/admin/screens/{{id}}/delete" data-confirm-message="Delete this screen?">
<button class="btn btn-sm btn-danger" type="submit">Delete</button>
</form>
</div>
{{#if (anyPermission ../currentUser 'screens.update' 'screens.delete')}}
<div class="actions">
{{#if (hasPermission ../currentUser 'screens.update')}}
<a class="btn btn-sm btn-primary" href="/admin/screens/{{id}}/edit">Edit</a>
{{/if}}
{{#if (hasPermission ../currentUser 'screens.delete')}}
<form class="inline-form" method="post" action="/admin/screens/{{id}}/delete" data-confirm-message="Delete this screen?">
<button class="btn btn-sm btn-danger" type="submit">Delete</button>
</form>
{{/if}}
</div>
{{else}}
<span class="empty">-</span>
{{/if}}
</td>
</tr>
{{/each}}
+26 -10
View File
@@ -8,13 +8,21 @@
<div class="card card-outline card-primary">
<div class="card-header">
<h3 class="card-title">Existing slides</h3>
<div class="card-tools">
<a class="btn btn-primary btn-sm" href="/admin/slides/new">Create slide</a>
</div>
{{#if (hasPermission currentUser 'slides.create')}}
<div class="card-tools">
<a class="btn btn-primary btn-sm" href="/admin/slides/new">Create slide</a>
</div>
{{/if}}
</div>
<div class="card-body table-responsive p-0">
<table class="table table-striped w-100 mb-0">
<thead><tr><th>Title</th><th>Template</th><th>Actions</th></tr></thead>
<thead>
<tr>
<th>Title</th>
<th>Template</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{{#if slides.length}}
{{#each slides}}
@@ -22,12 +30,20 @@
<td data-label="Title">{{title}}</td>
<td data-label="Template">{{template_name}}</td>
<td data-label="Actions">
<div class="actions">
<a class="btn btn-sm btn-primary" href="/admin/slides/{{id}}/edit">Edit</a>
<form class="inline-form" method="post" action="/admin/slides/{{id}}/delete" data-confirm-message="Delete this slide?">
<button class="btn btn-sm btn-danger" type="submit">Delete</button>
</form>
</div>
{{#if (anyPermission ../currentUser 'slides.update' 'slides.delete')}}
<div class="actions">
{{#if (hasPermission ../currentUser 'slides.update')}}
<a class="btn btn-sm btn-primary" href="/admin/slides/{{id}}/edit">Edit</a>
{{/if}}
{{#if (hasPermission ../currentUser 'slides.delete')}}
<form class="inline-form" method="post" action="/admin/slides/{{id}}/delete" data-confirm-message="Delete this slide?">
<button class="btn btn-sm btn-danger" type="submit">Delete</button>
</form>
{{/if}}
</div>
{{else}}
<span class="empty">-</span>
{{/if}}
</td>
</tr>
{{/each}}
+27 -10
View File
@@ -8,13 +8,22 @@
<div class="card card-outline card-primary">
<div class="card-header">
<h3 class="card-title">Existing templates</h3>
<div class="card-tools">
<a class="btn btn-primary btn-sm" href="/admin/templates/new">Create template</a>
</div>
{{#if (hasPermission currentUser 'templates.create')}}
<div class="card-tools">
<a class="btn btn-primary btn-sm" href="/admin/templates/new">Create template</a>
</div>
{{/if}}
</div>
<div class="card-body table-responsive p-0">
<table class="table table-striped w-100 mb-0">
<thead><tr><th>Name</th><th>Canvas</th><th>Regions</th><th>Actions</th></tr></thead>
<thead>
<tr>
<th>Name</th>
<th>Canvas</th>
<th>Regions</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{{#if templates.length}}
{{#each templates}}
@@ -23,12 +32,20 @@
<td data-label="Canvas">{{canvas_size_width}}x{{canvas_size_height}}</td>
<td data-label="Regions">{{regionCount}}</td>
<td data-label="Actions">
<div class="actions">
<a class="btn btn-sm btn-primary" href="/admin/templates/{{id}}/edit">Edit</a>
<form class="inline-form" method="post" action="/admin/templates/{{id}}/delete" data-confirm-message="Delete this template?">
<button class="btn btn-sm btn-danger" type="submit">Delete</button>
</form>
</div>
{{#if (anyPermission ../currentUser 'templates.update' 'templates.delete')}}
<div class="actions">
{{#if (hasPermission ../currentUser 'templates.update')}}
<a class="btn btn-sm btn-primary" href="/admin/templates/{{id}}/edit">Edit</a>
{{/if}}
{{#if (hasPermission ../currentUser 'templates.delete')}}
<form class="inline-form" method="post" action="/admin/templates/{{id}}/delete" data-confirm-message="Delete this template?">
<button class="btn btn-sm btn-danger" type="submit">Delete</button>
</form>
{{/if}}
</div>
{{else}}
<span class="empty">-</span>
{{/if}}
</td>
</tr>
{{/each}}
+23 -13
View File
@@ -8,9 +8,11 @@
<div class="card card-outline card-primary">
<div class="card-header">
<h3 class="card-title">Existing users</h3>
<div class="card-tools">
<a class="btn btn-primary btn-sm" href="/admin/users/new">Add user</a>
</div>
{{#if (hasPermission currentUser 'users.create')}}
<div class="card-tools">
<a class="btn btn-primary btn-sm" href="/admin/users/new">Add user</a>
</div>
{{/if}}
</div>
<div class="card-body table-responsive p-0">
<table class="table table-striped w-100 mb-0 users-table">
@@ -61,16 +63,24 @@
</div>
</td>
<td data-label="Actions">
<div class="actions users-row-actions">
{{#unless isCurrentUser}}
<a class="btn btn-sm btn-primary" href="/admin/users/{{id}}/edit">Edit</a>
<form method="post" action="/admin/users/{{id}}/delete" class="inline-form" data-confirm-message="Delete {{username}}?">
<button type="submit" class="btn btn-sm btn-danger">Delete</button>
</form>
{{else}}
<span class="empty">Protected</span>
{{/unless}}
</div>
{{#if (anyPermission ../currentUser 'users.update' 'users.delete')}}
<div class="actions users-row-actions">
{{#unless isCurrentUser}}
{{#if (hasPermission ../currentUser 'users.update')}}
<a class="btn btn-sm btn-primary" href="/admin/users/{{id}}/edit">Edit</a>
{{/if}}
{{#if (hasPermission ../currentUser 'users.delete')}}
<form method="post" action="/admin/users/{{id}}/delete" class="inline-form" data-confirm-message="Delete {{username}}?">
<button type="submit" class="btn btn-sm btn-danger">Delete</button>
</form>
{{/if}}
{{else}}
<span class="empty">Protected</span>
{{/unless}}
</div>
{{else}}
<span class="empty">-</span>
{{/if}}
</td>
</tr>
{{/each}}