Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9f08ccfea1 | ||
|
|
370ec81c33 | ||
|
|
5e7df5e55c | ||
|
|
cfca5bfe3b | ||
|
|
5eff70755b |
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "pulse-signage",
|
||||
"version": "1.4.1",
|
||||
"version": "1.4.6",
|
||||
"private": false,
|
||||
"description": "Pulse Signage application with MySQL and media uploads",
|
||||
"repository": {
|
||||
|
||||
@@ -95,6 +95,79 @@ async function addUserAuditColumns(pool, tableName) {
|
||||
await addForeignKeyIfMissing(pool, tableName, 'modified_by', `fk_${tableName}_modified_by`, 'users', 'id', 'SET NULL');
|
||||
}
|
||||
|
||||
async function hasSingleColumnUniqueIndex(pool, tableName, columnName) {
|
||||
const [rows] = await pool.query(
|
||||
`SELECT INDEX_NAME, COUNT(*) AS column_count
|
||||
FROM information_schema.statistics
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name = ?
|
||||
AND non_unique = 0
|
||||
AND column_name = ?
|
||||
GROUP BY INDEX_NAME`,
|
||||
[tableName, columnName]
|
||||
);
|
||||
|
||||
return (rows || []).some(function (row) {
|
||||
return Number(row.column_count) === 1;
|
||||
});
|
||||
}
|
||||
|
||||
async function addUniqueIndexIfMissing(pool, tableName, columnName, indexName) {
|
||||
const hasUniqueIndex = await hasSingleColumnUniqueIndex(pool, tableName, columnName);
|
||||
if (hasUniqueIndex) {
|
||||
return;
|
||||
}
|
||||
|
||||
await pool.query(`ALTER TABLE \`${tableName}\` ADD UNIQUE KEY \`${indexName}\` (\`${columnName}\`)`);
|
||||
}
|
||||
|
||||
async function dedupePermissionRows(pool) {
|
||||
const [rows] = await pool.query('SELECT id, permission_key FROM permissions ORDER BY id ASC');
|
||||
const canonicalIdByKey = new Map();
|
||||
const duplicateRowsByKey = new Map();
|
||||
|
||||
for (const row of rows || []) {
|
||||
const permissionKey = getPermissionKey(row);
|
||||
const permissionId = Number(row.id);
|
||||
if (!permissionKey || !Number.isInteger(permissionId) || permissionId <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!canonicalIdByKey.has(permissionKey)) {
|
||||
canonicalIdByKey.set(permissionKey, permissionId);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!duplicateRowsByKey.has(permissionKey)) {
|
||||
duplicateRowsByKey.set(permissionKey, []);
|
||||
}
|
||||
duplicateRowsByKey.get(permissionKey).push(permissionId);
|
||||
}
|
||||
|
||||
if (!duplicateRowsByKey.size) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const [permissionKey, duplicateIds] of duplicateRowsByKey.entries()) {
|
||||
const canonicalId = canonicalIdByKey.get(permissionKey);
|
||||
for (const duplicateId of duplicateIds) {
|
||||
await pool.query(
|
||||
'UPDATE IGNORE role_permissions SET permission_id = ? WHERE permission_id = ?',
|
||||
[canonicalId, duplicateId]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const duplicateIds = [];
|
||||
for (const duplicateList of duplicateRowsByKey.values()) {
|
||||
duplicateIds.push.apply(duplicateIds, duplicateList);
|
||||
}
|
||||
|
||||
if (duplicateIds.length) {
|
||||
await pool.query('DELETE FROM permissions WHERE id IN (?)', [duplicateIds]);
|
||||
}
|
||||
}
|
||||
|
||||
async function pruneStaleOnboardingDevices(pool) {
|
||||
await pool.query(
|
||||
`DELETE FROM player_onboarding_devices
|
||||
@@ -129,11 +202,20 @@ function getLegacyPermissionTargets(permissionKey) {
|
||||
|
||||
const sectionKey = parts[0];
|
||||
const actionKey = parts[1];
|
||||
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);
|
||||
@@ -172,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;
|
||||
@@ -192,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')) {
|
||||
if (currentKey.endsWith('.view') || currentKey.endsWith('.manage') || currentKey.endsWith('.edit') || currentKey === 'screens.allow') {
|
||||
for (const targetKey of targetKeys) {
|
||||
addRoleTarget(Number(row.role_id), targetKey);
|
||||
}
|
||||
@@ -203,8 +293,21 @@ async function backfillLegacyRbacSchema(pool) {
|
||||
|
||||
for (const row of permissionRows || []) {
|
||||
const currentKey = getPermissionKey(row);
|
||||
if (currentKey.endsWith('.view') || currentKey.endsWith('.manage')) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -277,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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -289,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;
|
||||
}
|
||||
@@ -541,6 +644,8 @@ async function ensureSchema(pool) {
|
||||
await addColumnIfMissing(pool, 'permissions', 'modified_at', 'TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP');
|
||||
await addUserAuditColumns(pool, 'permissions');
|
||||
await dropColumnIfPresent(pool, 'permissions', 'perm_key');
|
||||
await dedupePermissionRows(pool);
|
||||
await addUniqueIndexIfMissing(pool, 'permissions', 'permission_key', 'uq_permissions_permission_key');
|
||||
|
||||
await pool.query(`
|
||||
CREATE TABLE IF NOT EXISTS role_permissions (
|
||||
|
||||
+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 and send screen commands.' },
|
||||
{ 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) {
|
||||
|
||||
+20
-1
@@ -12,7 +12,7 @@ const registerAdminPagesRoutes = require('./web/routes/admin-pages');
|
||||
const registerAdminAccountRoutes = require('./web/routes/admin-account');
|
||||
const registerAdminUsersRoutes = require('./web/routes/admin-users');
|
||||
const registerAdminManageRoutes = require('./web/routes/admin-manage');
|
||||
const registerAdminScreenCommandRoutes = require('./web/routes/admin-screen-commands');
|
||||
const registerAdminScreenCommandRoutes = require('./web/routes/admin-client-commands');
|
||||
const registerAdminContentRoutes = require('./web/routes/admin-content');
|
||||
const { createWebBootstrap } = require('./web/bootstrap');
|
||||
const { requirePermission } = require('./rbac');
|
||||
@@ -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
|
||||
@@ -225,6 +226,24 @@ async function start() {
|
||||
});
|
||||
|
||||
|
||||
app.use(function (req, res, next) {
|
||||
const pathName = String(req.originalUrl || '');
|
||||
const wantsHtml = !pathName.startsWith('/api/') && (!req.accepts || req.accepts('html'));
|
||||
|
||||
if (!wantsHtml) {
|
||||
return next();
|
||||
}
|
||||
|
||||
return res.status(404).send(pages.renderErrorPage({
|
||||
statusCode: 404,
|
||||
title: 'Not found',
|
||||
errorTitle: 'Oops! Page not found.',
|
||||
message: 'We could not find the page you were looking for.',
|
||||
backUrl: req.currentUser ? '/admin' : '/login',
|
||||
backLabel: req.currentUser ? 'Back to dashboard' : 'Sign in'
|
||||
}, req.currentUser));
|
||||
});
|
||||
|
||||
app.use(function (error, req, res, _next) {
|
||||
console.error(error);
|
||||
const statusCode = Number(error && (error.statusCode || error.status)) || 500;
|
||||
|
||||
@@ -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/screens/' + 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/screens/' + 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/screens/' + 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/screens/' + 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/screens/' + 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) {
|
||||
@@ -119,7 +119,7 @@
|
||||
if (connectionInput) {
|
||||
connectionInput.value = client.id || '';
|
||||
}
|
||||
pauseForm.action = '/admin/screens/' + encodeURIComponent(client.screen_slug) + '/commands';
|
||||
pauseForm.action = '/admin/clients/' + encodeURIComponent(client.screen_slug) + '/commands';
|
||||
}
|
||||
|
||||
var reloadButton = cell.querySelector('button[data-action="reload"]');
|
||||
@@ -132,7 +132,7 @@
|
||||
if (reloadInput) {
|
||||
reloadInput.value = client.id || '';
|
||||
}
|
||||
reloadForm.action = '/admin/screens/' + encodeURIComponent(client.screen_slug) + '/commands';
|
||||
reloadForm.action = '/admin/clients/' + encodeURIComponent(client.screen_slug) + '/commands';
|
||||
reloadForm.setAttribute('data-confirm-message', 'Reloading will restart the player page. Continue?');
|
||||
}
|
||||
}
|
||||
@@ -162,7 +162,7 @@
|
||||
if (blackoutConnectionInput) {
|
||||
blackoutConnectionInput.value = client.id || '';
|
||||
}
|
||||
blackoutForm.action = '/admin/screens/' + encodeURIComponent(client.screen_slug) + '/commands';
|
||||
blackoutForm.action = '/admin/clients/' + encodeURIComponent(client.screen_slug) + '/commands';
|
||||
}
|
||||
|
||||
var previousButton = cell.querySelector('button[data-action="previous"]');
|
||||
@@ -182,7 +182,7 @@
|
||||
if (previousConnectionInput) {
|
||||
previousConnectionInput.value = client.id || '';
|
||||
}
|
||||
previousForm.action = '/admin/screens/' + encodeURIComponent(client.screen_slug) + '/commands';
|
||||
previousForm.action = '/admin/clients/' + encodeURIComponent(client.screen_slug) + '/commands';
|
||||
}
|
||||
|
||||
var nextButton = cell.querySelector('button[data-action="next"]');
|
||||
@@ -202,12 +202,34 @@
|
||||
if (nextConnectionInput) {
|
||||
nextConnectionInput.value = client.id || '';
|
||||
}
|
||||
nextForm.action = '/admin/screens/' + encodeURIComponent(client.screen_slug) + '/commands';
|
||||
nextForm.action = '/admin/clients/' + encodeURIComponent(client.screen_slug) + '/commands';
|
||||
}
|
||||
}
|
||||
|
||||
function syncClientActionCell(row, client, hasActionsColumn) {
|
||||
if (!row || !row.cells) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!hasActionsColumn) {
|
||||
if (row.cells.length > 6) {
|
||||
row.deleteCell(row.cells.length - 1);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
var actionCell = row.cells.length > 6 ? row.cells[6] : null;
|
||||
if (!actionCell) {
|
||||
actionCell = row.insertCell(-1);
|
||||
actionCell.setAttribute('data-label', 'Actions');
|
||||
actionCell.className = 'text-end';
|
||||
}
|
||||
|
||||
updateClientActionCell(actionCell, client);
|
||||
}
|
||||
|
||||
function renderClientRow(client, hasActionsColumn) {
|
||||
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>';
|
||||
@@ -302,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>';
|
||||
@@ -317,9 +339,7 @@
|
||||
row.cells[3].innerHTML = clientIp;
|
||||
row.cells[4].innerHTML = viewport;
|
||||
row.cells[5].innerHTML = connectedAt;
|
||||
if (hasActionsColumn && row.cells.length >= 7) {
|
||||
updateClientActionCell(row.cells[6], client);
|
||||
}
|
||||
syncClientActionCell(row, client, hasActionsColumn);
|
||||
}
|
||||
|
||||
var referenceNode = tbody.children[index] || null;
|
||||
@@ -396,7 +416,7 @@
|
||||
body.append('deviceId', String(deviceId || '').trim());
|
||||
body.append('clientName', String(clientName || '').trim());
|
||||
|
||||
return fetch('/admin/screens/' + encodeURIComponent(String(screenSlug || '').trim()) + '/commands', {
|
||||
return fetch('/admin/clients/' + encodeURIComponent(String(screenSlug || '').trim()) + '/commands', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8',
|
||||
|
||||
@@ -6,7 +6,7 @@ module.exports = function registerAdminScreenCommandRoutes(app, deps) {
|
||||
const withClientNameReservation = deps.withClientNameReservation;
|
||||
const requirePermission = deps.requirePermission;
|
||||
|
||||
app.post('/admin/screens/:slug/commands', requirePermission('clients.allow'), async function (req, res, next) {
|
||||
app.post('/admin/clients/:slug/commands', requirePermission('clients.allow'), async function (req, res, next) {
|
||||
try {
|
||||
const slug = String(req.params.slug || '').trim();
|
||||
const command = String((req.body && req.body.command) || req.query.command || '').trim().toLowerCase();
|
||||
@@ -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) {
|
||||
@@ -234,13 +235,20 @@ module.exports = function registerAdminRbacRoutes(app, deps) {
|
||||
return res.status(404).send('Role not found.');
|
||||
}
|
||||
|
||||
res.send(pages.renderRbacEditPage(viewModel.role, req.query.message ? String(req.query.message) : '', req.currentUser, viewModel.permissionGroups, viewModel.users));
|
||||
const currentUserId = req.currentUser ? Number(req.currentUser.id) : null;
|
||||
const users = Array.isArray(viewModel.users)
|
||||
? viewModel.users.filter(function (user) {
|
||||
return Number(user && user.id) !== currentUserId;
|
||||
})
|
||||
: [];
|
||||
|
||||
res.send(pages.renderRbacEditPage(viewModel.role, req.query.message ? String(req.query.message) : '', req.currentUser, viewModel.permissionGroups, users));
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
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) {
|
||||
@@ -254,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) {
|
||||
@@ -302,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>
|
||||
@@ -60,29 +60,29 @@
|
||||
{{#if (hasPermission currentUser 'clients.allow')}}
|
||||
<td data-label="Actions" class="text-end">
|
||||
<div class="actions justify-content-end">
|
||||
<form method="post" action="/admin/screens/{{screen_slug}}/commands" class="inline-form" data-confirm-message="Reloading will restart the player page. Continue?" data-async-command>
|
||||
<form method="post" action="/admin/clients/{{screen_slug}}/commands" class="inline-form" data-confirm-message="Reloading will restart the player page. Continue?" data-async-command>
|
||||
<input type="hidden" name="command" value="reload" />
|
||||
<input type="hidden" name="connectionId" value="{{id}}" />
|
||||
<button type="submit" class="btn btn-sm btn-danger">Reload</button>
|
||||
</form>
|
||||
<form method="post" action="/admin/screens/{{screen_slug}}/commands" class="inline-form" data-async-command>
|
||||
<form method="post" action="/admin/clients/{{screen_slug}}/commands" class="inline-form" data-async-command>
|
||||
<input type="hidden" name="command" value="previous" />
|
||||
<input type="hidden" name="connectionId" value="{{id}}" />
|
||||
<button type="submit" class="btn btn-sm btn-warning" aria-label="Previous slide">◀</button>
|
||||
</form>
|
||||
<form method="post" action="/admin/screens/{{screen_slug}}/commands" class="inline-form" data-async-command>
|
||||
<form method="post" action="/admin/clients/{{screen_slug}}/commands" class="inline-form" data-async-command>
|
||||
<input type="hidden" name="command" value="next" />
|
||||
<input type="hidden" name="connectionId" value="{{id}}" />
|
||||
<button type="submit" class="btn btn-sm btn-warning" aria-label="Next slide">▶</button>
|
||||
</form>
|
||||
<form method="post" action="/admin/screens/{{screen_slug}}/commands" class="inline-form" data-async-command>
|
||||
<form method="post" action="/admin/clients/{{screen_slug}}/commands" class="inline-form" data-async-command>
|
||||
<input type="hidden" name="command" value="pause" />
|
||||
<input type="hidden" name="connectionId" value="{{id}}" />
|
||||
<button type="submit" class="btn btn-sm btn-info">
|
||||
<i class="bi bi-pause-fill me-1" aria-hidden="false"></i>Pause
|
||||
</button>
|
||||
</form>
|
||||
<form method="post" action="/admin/screens/{{screen_slug}}/commands" class="inline-form" data-async-command>
|
||||
<form method="post" action="/admin/clients/{{screen_slug}}/commands" class="inline-form" data-async-command>
|
||||
<input type="hidden" name="command" value="blackout" />
|
||||
<input type="hidden" name="connectionId" value="{{id}}" />
|
||||
<button type="submit" class="btn btn-sm {{#if blackout}}btn-success{{else}}btn-secondary{{/if}}">
|
||||
|
||||
@@ -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}}
|
||||
|
||||
+32
-33
@@ -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>
|
||||
@@ -61,7 +60,7 @@
|
||||
<td>
|
||||
<input class="form-check-input" type="checkbox" name="user_ids[]" value="{{id}}" {{#if isSelected}}checked{{/if}} />
|
||||
</td>
|
||||
<td><strong>{{name}}</strong></td>
|
||||
<td>{{name}}</td>
|
||||
<td class="text-body-secondary">{{username}}</td>
|
||||
<td class="text-body-secondary">{{#if roleNames}}{{roleNames}}{{else}}No roles assigned{{/if}}</td>
|
||||
</tr>
|
||||
@@ -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