Compare commits

...
3 Commits
Author SHA1 Message Date
lzstealth 5a08ccddbb Release v2.6.11 2026-08-08 15:11:11 +01:00
lzstealth 2c97fe81d1 Release v2.6.10 2026-08-08 14:14:52 +01:00
lzstealth ee3b1b51bf Release v2.6.9 2026-08-08 14:08:16 +01:00
15 changed files with 203 additions and 73 deletions
+20
View File
@@ -2,6 +2,26 @@
All notable changes to this project will be documented in this file.
## 2.6.11 - 2026-08-08
### Fixed
- Role edits now save selected permissions from the shared RBAC form, so changes on the role page persist when you submit the form.
- RBAC permission sections now stay open independently in the accordion, so opening one section no longer closes the others.
## 2.6.10 - 2026-08-08
### Fixed
- The onboarding landing page and form no longer restore a previously selected screen, so the screen picker always starts clean while still keeping the saved client name.
## 2.6.9 - 2026-08-08
### Fixed
- The connected-clients view no longer exposes or sorts by client IP, so the table stays focused on the player identity, screen, and playback state.
- The connected-clients dashboard row renderer now keeps the actions column aligned after removing the IP column, so row updates no longer append a duplicate actions cell.
## 2.6.8 - 2026-08-08
### Fixed
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "pulse-signage",
"version": "2.6.8",
"version": "2.6.11",
"private": false,
"description": "Pulse Signage application with MySQL and media storage",
"repository": {
+4 -7
View File
@@ -40,8 +40,7 @@ function normalizeRemoteAddress(value) {
function formatPlayerConnectionLabel(deviceId, remoteAddress) {
const normalizedDeviceId = String(deviceId || '').trim() || 'unknown-player';
const normalizedRemoteAddress = normalizeRemoteAddress(remoteAddress);
return normalizedRemoteAddress ? `${normalizedDeviceId} (ip ${normalizedRemoteAddress})` : normalizedDeviceId;
return normalizedDeviceId;
}
function normalizeProxyBaseUrl(value) {
@@ -359,7 +358,7 @@ async function start() {
return;
}
logBridge(`Player ${formatPlayerConnectionLabel(socket.playerDeviceId, socket.bridgeRemoteAddress)} has disconnected`);
logBridge(`Player ${formatPlayerConnectionLabel(socket.playerDeviceId)} has disconnected`);
}
function resolveMediaPath(fileName) {
@@ -828,7 +827,7 @@ async function start() {
});
playerSockets.set(deviceId, socket);
logBridge(`Player ${formatPlayerConnectionLabel(deviceId, socket.bridgeRemoteAddress)} has connected`);
logBridge(`Player ${formatPlayerConnectionLabel(deviceId)} has connected`);
socket.send(JSON.stringify({ type: 'registered', ok: true, player: player }));
return;
}
@@ -875,14 +874,12 @@ async function start() {
}
if (!verifyRequestAuth(request)) {
const remoteAddress = normalizeRemoteAddress(request && request.socket && request.socket.remoteAddress);
logBridge(remoteAddress ? `Player (ip ${remoteAddress}) denied with wrong shared secret` : 'Player denied with wrong shared secret');
logBridge('Player denied with wrong shared secret');
socket.destroy();
return;
}
playersWs.handleUpgrade(request, socket, head, function (ws) {
ws.bridgeRemoteAddress = normalizeRemoteAddress(request && request.socket && request.socket.remoteAddress);
playersWs.emit('connection', ws, request);
});
});
+1
View File
@@ -114,6 +114,7 @@ async function start() {
Accept: 'application/json'
}, authHeaders)
});
return Boolean(response && response.ok);
} catch (_error) {
return false;
@@ -56,10 +56,8 @@
loadScreens().then(function () {
try {
var storedClientName = getSessionStorageItem(clientNameKey) || "";
var storedScreenSlug = window.localStorage.getItem(screenKey) || "";
var clientNameInput = form.querySelector("input[name=\"clientName\"]");
if (clientNameInput && storedClientName) { clientNameInput.value = storedClientName; }
if (screenSelect && storedScreenSlug) { screenSelect.value = storedScreenSlug; }
} catch (_error) {}
});
form.addEventListener("submit", function (event) {
@@ -132,14 +132,11 @@
}
loadScreens().then(function () {
try {
var storedScreenSlug = window.localStorage.getItem(screenKey) || "";
var storedClientName = getSessionStorageItem(clientNameKey) || "";
if (!storedClientName && storedScreenSlug) { storedClientName = getSessionStorageItem(getClientNameStorageKey(storedScreenSlug)) || ""; }
if (storedClientName && localForm) {
var clientNameInput = localForm.querySelector("input[name=\"clientName\"]");
if (clientNameInput) { clientNameInput.value = storedClientName; }
}
if (storedScreenSlug && localScreenSelect) { localScreenSelect.value = storedScreenSlug; }
} catch (_error) {}
});
redirectIfOnboarded(deviceId).then(function (redirected) {
+71 -14
View File
@@ -45,6 +45,76 @@ function createPlayerRuntime(options) {
return ip;
}
function firstHeaderValue(value) {
return String(value || '').trim().split(',')[0].trim();
}
function isPrivateOrReservedIp(ip) {
const normalized = normalizeClientIp(ip);
if (!normalized) {
return true;
}
const lower = normalized.toLowerCase();
if (lower === 'localhost' || lower === '::1') {
return true;
}
if (/^10\./.test(lower) || /^192\.168\./.test(lower) || /^127\./.test(lower) || /^169\.254\./.test(lower)) {
return true;
}
if (/^172\.(1[6-9]|2\d|3[0-1])\./.test(lower)) {
return true;
}
if (lower.startsWith('fc') || lower.startsWith('fd') || lower.startsWith('fe80:') || lower.startsWith('::ffff:127.')) {
return true;
}
return false;
}
function pickForwardedIp(candidates) {
const normalizedCandidates = Array.isArray(candidates)
? candidates.map(function (candidate) {
return normalizeClientIp(candidate);
}).filter(Boolean)
: [];
const publicCandidate = normalizedCandidates.find(function (candidate) {
return !isPrivateOrReservedIp(candidate);
});
return publicCandidate || normalizedCandidates[0] || null;
}
function resolveRequestIp(request) {
const forwardedFor = String(request && request.headers && request.headers['x-forwarded-for'] || '').split(',');
const forwardedForIp = pickForwardedIp(forwardedFor);
if (forwardedForIp) {
return forwardedForIp;
}
const realIp = pickForwardedIp([firstHeaderValue(request && request.headers && request.headers['x-real-ip'])]);
if (realIp) {
return realIp;
}
const forwarded = firstHeaderValue(request && request.headers && request.headers.forwarded);
if (forwarded) {
const forwardedMatches = Array.from(forwarded.matchAll(/(?:^|,\s*|;\s*)for=(?:"?\[?)([^"\];,\s]+)/gi)).map(function (match) {
return match[1];
});
const forwardedIp = pickForwardedIp(forwardedMatches);
if (forwardedIp) {
return forwardedIp;
}
}
return normalizeClientIp(request && request.socket && request.socket.remoteAddress);
}
function parseCookies(cookieHeader) {
return String(cookieHeader || '').split(/;\s*/).reduce(function (cookies, pair) {
if (!pair) {
@@ -145,7 +215,6 @@ function createPlayerRuntime(options) {
const clientName = String(connection.clientName || '').trim();
const clientId = String(connection.clientId || '').trim();
const userAgent = String(connection.userAgent || '').trim();
const clientIp = String(connection.clientIp || '').trim();
const viewport = connection.viewport && typeof connection.viewport === 'object'
? connection.viewport
: null;
@@ -161,10 +230,6 @@ function createPlayerRuntime(options) {
labelParts.push(`id ${clientId.slice(-6)}`);
}
if (clientIp) {
labelParts.push(clientIp);
}
if (viewport && Number.isFinite(Number(viewport.width)) && Number.isFinite(Number(viewport.height))) {
labelParts.push(`${Number(viewport.width)}x${Number(viewport.height)}`);
}
@@ -198,8 +263,6 @@ function createPlayerRuntime(options) {
blackout: Boolean(connection.blackout),
currentSlideId: connection.currentSlide && connection.currentSlide.id ? connection.currentSlide.id : null,
currentSlideTitle: connection.currentSlide && connection.currentSlide.title ? connection.currentSlide.title : null,
clientIp: connection.clientIp || null,
remoteAddress: connection.remoteAddress || null,
connectedAt: connection.connectedAt ? connection.connectedAt.toISOString() : null,
lastSeenAt: connection.lastSeenAt ? connection.lastSeenAt.toISOString() : null
};
@@ -431,9 +494,6 @@ function createPlayerRuntime(options) {
return;
}
const remoteAddress = request.socket && request.socket.remoteAddress ? request.socket.remoteAddress : null;
const forwardedFor = normalizeClientIp(String(request.headers['x-forwarded-for'] || '').split(',')[0]);
const normalizedRemoteAddress = normalizeClientIp(remoteAddress);
const connectionId = crypto.randomUUID();
const connection = {
id: connectionId,
@@ -448,9 +508,7 @@ function createPlayerRuntime(options) {
paused: false,
blackout: false,
playerPublicBaseUrl: null,
clientIp: forwardedFor || normalizedRemoteAddress,
remoteAddress: normalizedRemoteAddress,
label: forwardedFor || normalizedRemoteAddress || 'connected client',
label: 'connected client',
connectedAt: new Date(),
lastSeenAt: new Date()
};
@@ -491,7 +549,6 @@ function createPlayerRuntime(options) {
}
connection.paused = Boolean(payload.paused);
connection.blackout = Boolean(payload.blackout);
connection.clientIp = payload.clientIp ? normalizeClientIp(payload.clientIp) || connection.clientIp : connection.clientIp;
connection.currentSlide = payload.currentSlide && typeof payload.currentSlide === 'object' ? {
id: payload.currentSlide.id || null,
title: payload.currentSlide.title || '',
+7 -4
View File
@@ -194,18 +194,21 @@ const PERMISSION_SECTIONS = [
}
];
const PERMISSIONS = PERMISSION_SECTIONS.flatMap(function (section) {
return (Array.isArray(section.permissions) ? section.permissions : []).flatMap(function (resource) {
return (Array.isArray(resource.permissions) ? resource.permissions : []).map(function (action, index) {
const PERMISSIONS = PERMISSION_SECTIONS.flatMap(function (section, sectionIndex) {
return (Array.isArray(section.permissions) ? section.permissions : []).flatMap(function (resource, resourceIndex) {
return (Array.isArray(resource.permissions) ? resource.permissions : []).map(function (action, actionIndex) {
return {
key: `${resource.key}.${action.key}`,
name: resource.name,
sectionOrder: section.order,
sectionIndex: sectionIndex,
actionName: action.name,
permissionOrder: index + 1,
permissionOrder: actionIndex + 1,
permissionIndex: actionIndex,
sectionName: section.sectionName,
resourceKey: resource.key,
resourceOrder: resource.order,
resourceIndex: resourceIndex,
resourceName: resource.name,
actionKey: action.key,
description: action.description
+5 -20
View File
@@ -245,8 +245,6 @@
client && client.slug,
client && client.screen_slug,
client && client.screen_name,
client && client.ipAddress,
client && client.clientIp,
client && client.status,
client && client.currentSlideTitle
];
@@ -264,7 +262,6 @@
client: function (client) { return String(client && (getClientDisplayName(client) || client.client_name || client.name || client.clientId) || '').trim(); },
screen: function (client) { return String(client && (client.screen_name || client.screen_slug) || '').trim(); },
slide: function (client) { return String(client && client.currentSlideTitle || '').trim(); },
ip: function (client) { return String(client && client.clientIp || '').trim(); },
viewport: function (client) {
var viewport = client && client.viewport;
if (!viewport || !viewport.width || !viewport.height) {
@@ -283,12 +280,6 @@
? [normalizedSortKey]
: ['client'];
if (sortKeys[0] === 'client') {
sortKeys.push('ip');
} else if (sortKeys[0] === 'ip') {
sortKeys.push('client');
}
return (Array.isArray(clients) ? clients.slice() : []).sort(function (leftClient, rightClient) {
for (var index = 0; index < sortKeys.length; index += 1) {
var sortKeyName = sortKeys[index];
@@ -539,13 +530,13 @@
}
if (!hasActionsColumn) {
if (row.cells.length > 6) {
if (row.cells.length > 5) {
row.deleteCell(row.cells.length - 1);
}
return;
}
var actionCell = row.cells.length > 6 ? row.cells[6] : null;
var actionCell = row.cells.length > 5 ? row.cells[5] : null;
if (!actionCell) {
actionCell = row.insertCell(-1);
actionCell.setAttribute('data-label', 'Actions');
@@ -557,8 +548,6 @@
function renderClientRow(client, hasActionsColumn) {
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>';
var clientNameValue = getClientDisplayName(client);
var clientName = clientNameValue ? escapeHtml(clientNameValue) : '<span class="empty">Unknown</span>';
@@ -571,7 +560,6 @@
'<td data-label="Client"><div>' + clientName + '</div></td>',
'<td data-label="Current Screen"><div>' + screenName + '</div></td>',
'<td data-label="Current Slide">' + currentSlide + '</td>',
'<td data-label="IP">' + clientIp + '</td>',
'<td data-label="Viewport">' + viewport + '</td>',
'<td data-label="Connected/Updated">' + connectedAt + '</td>',
actionCell,
@@ -580,13 +568,11 @@
}
function updateClientRowCells(row, client, hasActionsColumn) {
if (!row || !row.cells || row.cells.length < 6) {
if (!row || !row.cells || row.cells.length < 5) {
return;
}
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>';
var clientNameValue = getClientDisplayName(client);
var clientName = clientNameValue ? escapeHtml(clientNameValue) : '<span class="empty">Unknown</span>';
@@ -603,9 +589,8 @@
setCellHtml(row.cells[0], '<div>' + clientName + '</div>');
setCellHtml(row.cells[1], '<div>' + screenName + '</div>');
setCellHtml(row.cells[2], currentSlide);
setCellHtml(row.cells[3], clientIp);
setCellHtml(row.cells[4], viewport);
setCellHtml(row.cells[5], connectedAt);
setCellHtml(row.cells[3], viewport);
setCellHtml(row.cells[4], connectedAt);
syncClientActionCell(row, client, hasActionsColumn);
}
+24 -4
View File
@@ -19,6 +19,11 @@
const LIST_PAGE_SIZE = 25;
function toSortIndex(value) {
const number = Number(value);
return Number.isFinite(number) ? number : 999;
}
function slugifyRoleKey(name) {
const value = String(name || '').trim().toLowerCase();
const slug = value.replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, ROLE_KEY_MAX_LENGTH);
@@ -73,8 +78,11 @@
resourceKey: definition ? definition.resourceKey : String(permission.section_name || '').trim().toLowerCase().replace(/[^a-z0-9]+/g, '-'),
resourceName: definition ? definition.resourceName : permission.section_name,
categoryName: definition ? definition.sectionName : permission.section_name,
sectionIndex: definition ? toSortIndex(definition.sectionIndex) : 999,
sectionOrder: definition ? definition.sectionOrder : 999,
resourceIndex: definition ? toSortIndex(definition.resourceIndex) : 999,
resourceOrder: definition ? Number(definition.resourceOrder) || 999 : 999,
permissionIndex: definition ? toSortIndex(definition.permissionIndex) : 999,
permissionOrder: definition ? Number(definition.permissionOrder) || 999 : 999,
actionKey: definition ? definition.actionKey : 'read',
actionLabel: definition ? definition.actionName : getActionLabel(permission.actionKey),
@@ -103,7 +111,9 @@
id: sectionKey || 'permissions',
title: String(permission.resourceName || permission.categoryName || 'Permissions').trim(),
categoryName: String(permission.categoryName || '').trim(),
sectionIndex: toSortIndex(permission.sectionIndex),
sectionOrder: Number(permission.sectionOrder) || 999,
resourceIndex: toSortIndex(permission.resourceIndex),
permissions: []
};
groupIndex.set(sectionKey, group);
@@ -114,8 +124,13 @@
groups.forEach(function (group) {
group.permissions.sort(function (left, right) {
const leftOrder = Number(left.permissionOrder) || 999;
const rightOrder = Number(right.permissionOrder) || 999;
const leftIndex = toSortIndex(left.permissionIndex);
const rightIndex = toSortIndex(right.permissionIndex);
if (leftIndex !== rightIndex) {
return leftIndex - rightIndex;
}
const leftOrder = toSortIndex(left.permissionOrder);
const rightOrder = toSortIndex(right.permissionOrder);
if (leftOrder !== rightOrder) {
return leftOrder - rightOrder;
}
@@ -124,8 +139,13 @@
});
groups.sort(function (left, right) {
const leftOrder = Number(left.sectionOrder) || 999;
const rightOrder = Number(right.sectionOrder) || 999;
const leftIndex = toSortIndex(left.sectionIndex);
const rightIndex = toSortIndex(right.sectionIndex);
if (leftIndex !== rightIndex) {
return leftIndex - rightIndex;
}
const leftOrder = toSortIndex(left.sectionOrder);
const rightOrder = toSortIndex(right.sectionOrder);
if (leftOrder !== rightOrder) {
return leftOrder - rightOrder;
}
@@ -4,6 +4,11 @@ function normalizeSectionId(value) {
return String(value || '').trim().toLowerCase().replace(/[^a-z0-9]+/g, '-');
}
function toSortIndex(value) {
const number = Number(value);
return Number.isFinite(number) ? number : 999;
}
function buildPermissionSections(permissionGroups) {
const sections = [];
const sectionIndex = new Map();
@@ -23,7 +28,8 @@ function buildPermissionSections(permissionGroups) {
sectionIndex.set(sectionKey, {
id: sectionKey,
title: sectionTitle,
sectionOrder: Number(group && group.sectionOrder) || 999,
sourceIndex: toSortIndex(group && group.sectionIndex),
sectionOrder: toSortIndex(group && group.sectionOrder),
groups: []
});
sections.push(sectionIndex.get(sectionKey));
@@ -47,8 +53,14 @@ function buildPermissionSections(permissionGroups) {
});
section.groups.sort(function (left, right) {
const leftOrder = Number(left.resourceOrder) || 999;
const rightOrder = Number(right.resourceOrder) || 999;
const leftIndex = toSortIndex(left.resourceIndex);
const rightIndex = toSortIndex(right.resourceIndex);
if (leftIndex !== rightIndex) {
return leftIndex - rightIndex;
}
const leftOrder = toSortIndex(left.resourceOrder);
const rightOrder = toSortIndex(right.resourceOrder);
if (leftOrder !== rightOrder) {
return leftOrder - rightOrder;
}
@@ -65,8 +77,14 @@ function buildPermissionSections(permissionGroups) {
});
sections.sort(function (left, right) {
const leftOrder = Number(left.sectionOrder) || 999;
const rightOrder = Number(right.sectionOrder) || 999;
const leftIndex = toSortIndex(left.sourceIndex);
const rightIndex = toSortIndex(right.sourceIndex);
if (leftIndex !== rightIndex) {
return leftIndex - rightIndex;
}
const leftOrder = toSortIndex(left.sectionOrder);
const rightOrder = toSortIndex(right.sectionOrder);
if (leftOrder !== rightOrder) {
return leftOrder - rightOrder;
}
@@ -134,5 +152,6 @@ function buildRbacEditViewModel(role, message, currentUser, permissionGroups, us
module.exports = {
buildRbacAddViewModel: buildRbacAddViewModel,
buildRbacEditViewModel: buildRbacEditViewModel
buildRbacEditViewModel: buildRbacEditViewModel,
buildPermissionSections: buildPermissionSections
};
-8
View File
@@ -18,7 +18,6 @@ module.exports = function registerClientsRoutes(app, deps) {
client: function (client) { return String(client && (client.client_name || client.name || client.clientId) || '').trim(); },
screen: function (client) { return String(client && (client.screen_name || client.screen_slug) || '').trim(); },
slide: function (client) { return String(client && client.currentSlideTitle || '').trim(); },
ip: function (client) { return String(client && client.clientIp || '').trim(); },
viewport: function (client) {
const viewport = client && client.viewport;
if (!viewport || !viewport.width || !viewport.height) {
@@ -37,12 +36,6 @@ module.exports = function registerClientsRoutes(app, deps) {
? [normalizedSortKey]
: ['client'];
if (sortKeys[0] === 'client') {
sortKeys.push('ip');
} else if (sortKeys[0] === 'ip') {
sortKeys.push('client');
}
return (Array.isArray(clients) ? clients.slice() : []).sort(function (leftClient, rightClient) {
for (let index = 0; index < sortKeys.length; index += 1) {
const sortKeyName = sortKeys[index];
@@ -71,7 +64,6 @@ module.exports = function registerClientsRoutes(app, deps) {
'deviceId',
'slug',
'screen_slug',
'ipAddress',
'status'
]);
const filteredClients = (data.clients || []).filter(matchesSearch);
+2 -1
View File
@@ -6,6 +6,7 @@
</div>
<form id="{{formId}}" method="post" action="{{formAction}}" {{{formAttrs}}}>
<input type="hidden" name="permissions_present" value="1" />
<input type="hidden" name="users_present" value="1" />
<div class="row g-3">
@@ -94,7 +95,7 @@
<span class="fw-semibold">{{title}}</span>
</button>
</h2>
<div id="permission-section-collapse-{{id}}" class="accordion-collapse collapse {{#if @first}}show{{/if}}" aria-labelledby="permission-section-heading-{{id}}" data-bs-parent="#role-permissions-accordion">
<div id="permission-section-collapse-{{id}}" class="accordion-collapse collapse {{#if @first}}show{{/if}}" aria-labelledby="permission-section-heading-{{id}}">
<div class="accordion-body p-0">
<div class="table-responsive">
<table class="table table-striped align-middle mb-0 rbac-permissions-table" style="table-layout: fixed; width: 100%;">
+1 -3
View File
@@ -88,7 +88,6 @@
<th data-table-sort-key="client">Client</th>
<th data-table-sort-key="screen">Current Screen Group</th>
<th data-table-sort-key="slide">Current Slide</th>
<th data-table-sort-key="ip">IP</th>
<th data-table-sort-key="viewport">Viewport</th>
<th data-table-sort-key="connected">Connected/Updated</th>
{{#if (hasPermission currentUser 'clients.allow')}}<th>Actions</th>{{/if}}
@@ -111,7 +110,6 @@
<span class="empty">No slide currently showing</span>
{{/if}}
</td>
<td data-label="IP">{{#if clientIp}}{{clientIp}}{{else}}<span class="empty">Unknown</span>{{/if}}</td>
<td data-label="Viewport">
{{#if viewport}}
{{viewport.width}}x{{viewport.height}}
@@ -169,7 +167,7 @@
</tr>
{{/each}}
{{else}}
<tr data-table-search-empty-default><td colspan="{{#if (hasPermission currentUser 'clients.allow')}}7{{else}}6{{/if}}" class="empty">No connected clients yet.</td></tr>
<tr data-table-search-empty-default><td colspan="{{#if (hasPermission currentUser 'clients.allow')}}6{{else}}5{{/if}}" class="empty">No connected clients yet.</td></tr>
{{/if}}
</tbody>
</table>
+42
View File
@@ -4,14 +4,17 @@ const fs = require('node:fs');
const rbacPermissionsScript = fs.readFileSync(require.resolve('../src/web/public/js/rbac-permissions.js'), 'utf8');
const rbacFormTemplate = fs.readFileSync(require.resolve('../src/web/views/settings/rbac/form.hbs'), 'utf8');
const { buildPermissionSections } = require('../src/web/routes/settings/rbac/form-view-model');
const rbacFormViewModel = fs.readFileSync(require.resolve('../src/web/routes/settings/rbac/form-view-model.js'), 'utf8');
const rbacSource = fs.readFileSync(require.resolve('../src/rbac.js'), 'utf8');
test('rbac form exposes bulk permission controls', () => {
assert.match(rbacFormTemplate, /name="permissions_present" value="1"/);
assert.match(rbacFormTemplate, /accordion accordion-flush/);
assert.match(rbacFormTemplate, /card card-outline card-secondary overflow-hidden/);
assert.match(rbacFormTemplate, /accordion-item overflow-hidden" data-permission-section/);
assert.match(rbacFormTemplate, /accordion-body p-0/);
assert.ok(!rbacFormTemplate.includes('data-bs-parent="#role-permissions-accordion"'));
assert.match(rbacFormTemplate, /table-layout: fixed; width: 100%;/);
assert.match(rbacFormTemplate, /<col style="width: 5\.5rem;" \/>/);
assert.match(rbacFormTemplate, /class="text-center"/);
@@ -51,6 +54,45 @@ test('rbac permissions script supports bulk permission selection', () => {
assert.ok(!rbacPermissionsScript.includes('data-permission-section-select-none'));
});
test('rbac accordion sections follow source order', () => {
const sections = buildPermissionSections([
{
categoryName: 'Settings',
sectionIndex: 3,
sectionOrder: 40,
resourceIndex: 1,
resourceOrder: 20,
title: 'Later resource',
permissions: []
},
{
categoryName: 'Main navigation',
sectionIndex: 0,
sectionOrder: 10,
resourceIndex: 1,
resourceOrder: 20,
title: 'Clients',
permissions: []
},
{
categoryName: 'Main navigation',
sectionIndex: 0,
sectionOrder: 10,
resourceIndex: 0,
resourceOrder: 10,
title: 'Dashboard',
permissions: []
}
]);
assert.deepEqual(sections.map(function (section) {
return section.title;
}), ['Main navigation', 'Settings']);
assert.deepEqual(sections[0].groups.map(function (group) {
return group.title;
}), ['Dashboard', 'Clients']);
});
test('rbac duplicate route exists', () => {
const rbacRoutes = fs.readFileSync(require.resolve('../src/web/routes/admin/rbac.js'), 'utf8');
const duplicateHelpers = fs.readFileSync(require.resolve('../src/web/routes/settings/rbac/duplicate.js'), 'utf8');