// Permission section definitions and normalization helpers for RBAC. const PERMISSION_SECTIONS = [ { sectionName: 'Main navigation', order: 10, permissions: [ { key: 'dashboard', order: 10, name: 'Dashboard', permissions: [ { key: 'read', name: 'Read', description: 'Access the dashboard overview.' }, { key: 'allow', name: 'Allow', description: 'Send global player commands.' } ] }, { key: 'clients', order: 20, name: 'Connected clients', permissions: [ { key: 'read', name: 'Read', description: 'View connected player clients and live status.' }, { key: 'allow', name: 'Allow', description: 'Use the connected client command buttons.' } ] } ] }, { sectionName: 'Content', order: 20, permissions: [ { key: 'screens', order: 10, name: 'Screens', permissions: [ { key: 'read', name: 'Read', description: 'View the screen list and open screen details.' }, { key: 'create', name: 'Create', description: 'Create new screens.' }, { key: 'update', name: 'Update', description: 'Update screens.' }, { key: 'delete', name: 'Delete', description: 'Delete screens.' } ] }, { key: 'playlists', order: 20, name: 'Playlists', permissions: [ { key: 'read', name: 'Read', description: 'View playlists and playlist contents.' }, { key: 'create', name: 'Create', description: 'Create new playlists.' }, { key: 'update', name: 'Update', description: 'Update playlists and playlist slides.' }, { key: 'delete', name: 'Delete', description: 'Delete playlists.' } ] }, { key: 'slides', order: 30, name: 'Slides', permissions: [ { key: 'read', name: 'Read', description: 'View slides.' }, { key: 'create', name: 'Create', description: 'Create new slides.' }, { key: 'update', name: 'Update', description: 'Update slide content.' }, { key: 'delete', name: 'Delete', description: 'Delete slides.' } ] }, { key: 'templates', order: 40, name: 'Slide templates', permissions: [ { key: 'read', name: 'Read', description: 'View slide templates.' }, { key: 'create', name: 'Create', description: 'Create new slide templates.' }, { key: 'update', name: 'Update', description: 'Update slide templates.' }, { key: 'delete', name: 'Delete', description: 'Delete slide templates.' } ] }, { key: 'canvas-sizes', order: 50, name: 'Canvas sizes', permissions: [ { key: 'read', name: 'Read', description: 'View canvas sizes.' }, { key: 'create', name: 'Create', description: 'Create new canvas sizes.' }, { key: 'update', name: 'Update', description: 'Update canvas sizes.' }, { key: 'delete', name: 'Delete', description: 'Delete canvas sizes.' } ] }, { key: 'announcements', order: 60, name: 'Announcements', permissions: [ { key: 'read', name: 'Read', description: 'View announcements.' }, { key: 'create', name: 'Create', description: 'Create new announcements.' }, { key: 'update', name: 'Update', description: 'Update announcements.' }, { key: 'delete', name: 'Delete', description: 'Delete announcements.' } ] } ] }, { sectionName: 'Data Sources', order: 30, permissions: [ { key: 'rss-feeds', order: 10, name: 'RSS feeds', permissions: [ { key: 'read', name: 'Read', description: 'View configured RSS feeds.' }, { key: 'create', name: 'Create', description: 'Create new RSS feeds.' }, { key: 'update', name: 'Update', description: 'Update RSS feeds.' }, { key: 'delete', name: 'Delete', description: 'Delete RSS feeds.' } ] }, { key: 'api-sources', order: 20, name: 'API sources', permissions: [ { key: 'read', name: 'Read', description: 'View configured API sources.' }, { key: 'create', name: 'Create', description: 'Create new API sources.' }, { key: 'update', name: 'Update', description: 'Update API sources.' }, { key: 'delete', name: 'Delete', description: 'Delete API sources.' } ] }, { key: 'timetables', order: 30, name: 'Timetables', permissions: [ { key: 'read', name: 'Read', description: 'View configured timetable groups.' }, { key: 'create', name: 'Create', description: 'Create new timetable groups.' }, { key: 'update', name: 'Update', description: 'Update timetable groups and entries.' }, { key: 'delete', name: 'Delete', description: 'Delete timetable groups.' } ] } ] }, { sectionName: 'Settings', order: 40, permissions: [ { key: 'users', order: 10, name: 'Users', permissions: [ { key: 'read', name: 'Read', description: 'View users and role assignments.' }, { key: 'create', name: 'Create', description: 'Create new users.' }, { key: 'update', name: 'Update', description: 'Update users, passwords, and role assignments.' }, { key: 'delete', name: 'Delete', description: 'Delete users.' } ] }, { key: 'rbac', order: 20, name: 'Roles and permissions', permissions: [ { key: 'read', name: 'Read', description: 'View roles and permissions.' }, { key: 'create', name: 'Create', description: 'Create new roles.' }, { key: 'update', name: 'Update', description: 'Update role details and permissions.' }, { key: 'delete', name: 'Delete', description: 'Delete roles.' } ] }, { key: 'fonts', order: 30, name: 'Fonts', permissions: [ { key: 'read', name: 'Read', description: 'View managed fonts.' }, { key: 'create', name: 'Create', description: 'Upload managed fonts.' }, { key: 'delete', name: 'Delete', description: 'Remove managed fonts.' } ] }, { key: 'background-tasks', order: 40, name: 'Task Queue', permissions: [ { key: 'read', name: 'Read', description: 'View queued background tasks.' }, { key: 'allow', name: 'Allow', description: 'Manage queued background tasks and clear finished items.' } ] }, { key: 'scheduled-tasks', order: 50, name: 'Scheduled tasks', permissions: [ { key: 'read', name: 'Read', description: 'View scheduled refresh tasks.' }, { key: 'allow', name: 'Allow', description: 'Run scheduled refreshes manually.' } ] } ] } ]; 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: actionIndex + 1, permissionIndex: actionIndex, sectionName: section.sectionName, resourceKey: resource.key, resourceOrder: resource.order, resourceIndex: resourceIndex, resourceName: resource.name, actionKey: action.key, description: action.description }; }); }); }); const DEFAULT_ROLE = { key: 'administrators', name: 'Administrators', description: 'Full access to the admin interface.' }; function normalizePermissionKey(permissionKey) { return String(permissionKey || '').trim(); } const PERMISSION_DENIED_MESSAGE = 'You do not have permissions to do this.'; function normalizePermissionKeys(permissionKeys) { const normalized = []; (Array.isArray(permissionKeys) ? permissionKeys : []).forEach(function (permissionKey) { const normalizedPermissionKey = normalizePermissionKey(permissionKey); if (!normalizedPermissionKey) { return; } const parts = normalizedPermissionKey.split('.'); if (parts.length !== 2) { normalized.push(normalizedPermissionKey); return; } const sectionKey = parts[0]; const actionKey = parts[1]; 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}.update`); normalized.push(`${sectionKey}.delete`); } if (actionKey === 'view') { normalized.push(`${sectionKey}.read`); } }); return Array.from(new Set(normalized)); } function hasPermission(currentUser, permissionKey) { const normalizedPermissionKey = normalizePermissionKey(permissionKey); if (!normalizedPermissionKey || !currentUser) { return false; } const permissionKeys = Array.isArray(currentUser.permissionKeys) ? currentUser.permissionKeys : Array.isArray(currentUser.permissions) ? currentUser.permissions : []; return normalizePermissionKeys(permissionKeys).includes(normalizedPermissionKey); } function hasAnyPermission(currentUser, permissionKeys) { const normalizedPermissionKeys = normalizePermissionKeys(permissionKeys); if (!normalizedPermissionKeys.length || !currentUser) { return false; } return normalizedPermissionKeys.some(function (permissionKey) { return hasPermission(currentUser, permissionKey); }); } function requirePermission(permissionKey, options) { const normalizedPermissionKey = normalizePermissionKey(permissionKey); if (!normalizedPermissionKey) { throw new Error('requirePermission requires a permission key.'); } const setAuthMessageCookie = options && options.setAuthMessageCookie; return function (req, res, next) { if (!req.currentUser) { if (typeof setAuthMessageCookie === 'function') { setAuthMessageCookie(res, 'Please sign in to continue.'); return res.redirect('/login'); } return res.redirect('/login'); } if (hasPermission(req.currentUser, normalizedPermissionKey)) { return next(); } const error = new Error(PERMISSION_DENIED_MESSAGE); error.statusCode = 403; error.expose = true; next(error); }; } module.exports = { PERMISSIONS, PERMISSION_SECTIONS, DEFAULT_ROLE, hasPermission, hasAnyPermission, requirePermission, normalizePermissionKeys, PERMISSION_DENIED_MESSAGE };