This commit is contained in:
+1
-1
@@ -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": {
|
||||
|
||||
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>';
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 || '');
|
||||
|
||||
@@ -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}}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -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
@@ -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}}
|
||||
|
||||
@@ -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}}
|
||||
|
||||
@@ -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}}
|
||||
|
||||
@@ -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}}
|
||||
|
||||
@@ -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}}
|
||||
|
||||
Reference in New Issue
Block a user