Files
pulse-signage/src/web/routes/admin/rbac.js
T
lzstealth ea72747822
Publish Docker Image / build-and-push-existing-registry (./build/Dockerfile, web, pulse-signage-web) (push) Successful in 1m12s
Publish Docker Image / build-and-push-existing-registry (./build/Dockerfile.player, player, pulse-signage-player) (push) Successful in 34s
Release 2.8.2
2026-08-16 22:41:39 +01:00

626 lines
26 KiB
JavaScript

// Admin RBAC route registration and permission management.
const { DEFAULT_ROLE } = require('#src/rbac');
const { buildAuditChanges } = require('#src/data/audit-log');
module.exports = function registerRbacRoutes(app, deps) {
const pool = deps.pool;
const common = deps.common;
const pages = deps.pages;
const getAuditUserId = deps.getAuditUserId;
const recordRequestAuditEvent = deps.recordRequestAuditEvent;
const rbacData = deps.rbacData;
const permissions = Array.isArray(deps.permissions) ? deps.permissions : [];
const readArrayField = deps.readArrayField;
const normalizePermissionKeys = deps.normalizePermissionKeys;
const { buildPagination } = require('../../lib/pagination');
const requirePermission = deps.requirePermission;
const { buildDuplicateRoleName, buildDuplicateRole } = require('../settings/rbac/duplicate');
const ROLE_NAME_MAX_LENGTH = 255;
const ROLE_DESCRIPTION_MAX_LENGTH = 255;
const ROLE_KEY_MAX_LENGTH = 100;
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);
return slug || 'role';
}
function normalizeSelectedIds(values) {
return Array.from(new Set((Array.isArray(values) ? values : []).map(function (value) {
return Number(value);
}).filter(function (value) {
return Number.isInteger(value) && value > 0;
})));
}
function getActionLabel(actionKey) {
const normalizedActionKey = String(actionKey || '').trim().toLowerCase();
if (normalizedActionKey === 'edit') {
return 'Update';
}
if (normalizedActionKey === 'allow') {
return 'Allow';
}
if (!normalizedActionKey) {
return 'Read';
}
return normalizedActionKey.charAt(0).toUpperCase() + normalizedActionKey.slice(1);
}
async function createUniqueRoleKey(baseName) {
const baseKey = slugifyRoleKey(baseName);
let candidate = baseKey;
let suffix = 2;
while (true) {
const [rows] = await pool.query('SELECT id FROM a_roles WHERE role_key = ? LIMIT 1', [candidate]);
if (!rows.length) {
return candidate;
}
candidate = `${baseKey}-${suffix}`;
suffix += 1;
}
}
function mapPermissionsForView(permissionRows, selectedPermissionKeys) {
const selectedKeys = new Set(normalizePermissionKeys(selectedPermissionKeys));
const permissionDefinitions = new Map(permissions.map(function (permission) {
return [String(permission.key || '').trim(), permission];
}));
return (Array.isArray(permissionRows) ? permissionRows : []).map(function (permission) {
const definition = permissionDefinitions.get(String(permission.permission_key || '').trim()) || null;
return Object.assign({}, permission, {
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),
isSelected: selectedKeys.has(String(permission.permission_key || '').trim())
});
});
}
function mapUsersForView(userRows, selectedUserIds) {
const selectedIds = new Set(normalizeSelectedIds(selectedUserIds));
return (Array.isArray(userRows) ? userRows : []).map(function (user) {
return Object.assign({}, user, {
isSelected: selectedIds.has(Number(user.id))
});
});
}
function buildPermissionGroups(permissionRows) {
const groups = [];
const groupIndex = new Map();
(Array.isArray(permissionRows) ? permissionRows : []).forEach(function (permission) {
const sectionKey = String(permission.resourceKey || permission.sectionKey || permission.sectionName || '').trim().toLowerCase().replace(/[^a-z0-9]+/g, '-');
if (!groupIndex.has(sectionKey)) {
const group = {
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),
resourceOrder: Number(permission.resourceOrder) || 999,
permissions: []
};
groupIndex.set(sectionKey, group);
groups.push(group);
}
groupIndex.get(sectionKey).permissions.push(permission);
});
groups.forEach(function (group) {
group.permissions.sort(function (left, right) {
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;
}
return String(left.name || '').localeCompare(String(right.name || ''));
});
});
groups.sort(function (left, right) {
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;
}
return String(left.title || '').localeCompare(String(right.title || ''));
});
return groups;
}
async function buildRoleCreateViewModel(formValues, selectedPermissionKeys, selectedUserIds, page, search, sort, direction, currentUserId) {
const permissionRows = await rbacData.fetchPermissions(pool);
const data = await rbacData.fetchUsersWithRolesPage(pool, page, LIST_PAGE_SIZE, search, sort, direction, {
excludeUserId: currentUserId
});
return {
formValues: {
name: common.validateMaxLength(formValues && formValues.name || '', ROLE_NAME_MAX_LENGTH, 'Role name'),
description: common.validateMaxLength(formValues && formValues.description || '', ROLE_DESCRIPTION_MAX_LENGTH, 'Role description')
},
permissionGroups: buildPermissionGroups(mapPermissionsForView(permissionRows, selectedPermissionKeys)),
users: mapUsersForView(data.users, selectedUserIds),
pagination: buildPagination(data.totalItems, data.currentPage, 'page', { search: search, sort: sort, direction: direction }, LIST_PAGE_SIZE, 'users', 'User pages')
};
}
async function loadRoleViewModel(roleId) {
const role = await rbacData.fetchRoleById(pool, roleId);
if (!role) {
return null;
}
const permissionRows = await rbacData.fetchPermissions(pool);
const selectedPermissionKeys = await rbacData.fetchRolePermissionKeys(pool, role.id);
const selectedUserIds = await rbacData.fetchRoleUserIds(pool, role.id);
return {
role: Object.assign({}, role, {
permissionKeys: selectedPermissionKeys,
permissionCount: Number(role.permission_count) || 0,
userCount: Number(role.user_count) || 0
}),
permissionGroups: buildPermissionGroups(mapPermissionsForView(permissionRows, selectedPermissionKeys)),
selectedUserIds: selectedUserIds
};
}
app.get('/settings/roles', requirePermission('rbac.read'), async function (req, res, next) {
try {
const page = Math.max(1, Math.floor(Number(req.query.page) || 1));
const search = common.getSearchQuery(req);
const sort = common.getSortQuery(req);
const direction = common.getSortDirectionQuery(req);
const data = await rbacData.fetchRolesPage(pool, page, LIST_PAGE_SIZE, search, sort, direction);
res.send(pages.renderRbacPage({
roles: data.roles || [],
pagination: buildPagination(data.totalItems, data.currentPage, 'page', { search: search, sort: sort, direction: direction }, LIST_PAGE_SIZE, 'roles', 'Role pages')
}, req.query.message ? String(req.query.message) : '', req.currentUser));
} catch (error) {
next(error);
}
});
app.get('/settings/roles/new', requirePermission('rbac.create'), function (req, res, next) {
const page = Math.max(1, Math.floor(Number(req.query.page) || 1));
const search = common.getSearchQuery(req);
const sort = common.getSortQuery(req);
const direction = common.getSortDirectionQuery(req);
const currentUserId = req.currentUser ? Number(req.currentUser.id) : null;
buildRoleCreateViewModel({
name: String(req.query.name || '').trim(),
description: String(req.query.description || '').trim()
}, [], [], page, search, sort, direction, currentUserId).then(function (viewModel) {
res.send(pages.renderRbacAddPage(req.query.message ? String(req.query.message) : '', req.currentUser, viewModel.formValues, viewModel.permissionGroups, viewModel.users, viewModel.pagination, 'primary'));
}).catch(function (error) {
next(error);
});
});
app.get('/settings/roles/:id/duplicate', requirePermission('rbac.read'), requirePermission('rbac.create'), async function (req, res, next) {
try {
const roleId = Number(req.params.id);
if (!Number.isInteger(roleId) || roleId <= 0) {
return res.status(400).send('Invalid role.');
}
const role = await rbacData.fetchRoleById(pool, roleId);
if (!role) {
return res.status(404).send('Role not found.');
}
const page = Math.max(1, Math.floor(Number(req.query.page) || 1));
const search = common.getSearchQuery(req);
const sort = common.getSortQuery(req);
const direction = common.getSortDirectionQuery(req);
const currentUserId = req.currentUser ? Number(req.currentUser.id) : null;
const permissionKeys = await rbacData.fetchRolePermissionKeys(pool, roleId);
const viewModel = await buildRoleCreateViewModel({
name: buildDuplicateRoleName(role.name),
description: buildDuplicateRole(role).description
}, permissionKeys, [], page, search, sort, direction, currentUserId);
res.send(pages.renderRbacAddPage('Review the copied values and save when ready.', req.currentUser, viewModel.formValues, viewModel.permissionGroups, viewModel.users, viewModel.pagination, 'primary'));
} catch (error) {
next(error);
}
});
app.post('/settings/roles', requirePermission('rbac.create'), async function (req, res, next) {
try {
const name = common.validateMaxLength(req.body.name || '', ROLE_NAME_MAX_LENGTH, 'Role name');
const description = common.validateMaxLength(req.body.description || '', ROLE_DESCRIPTION_MAX_LENGTH, 'Role description');
const selectedUserIds = Object.prototype.hasOwnProperty.call(req.body || {}, 'users_present')
? readArrayField(req.body, ['user_ids[]', 'user_ids'])
: [];
const normalizedUserIds = normalizeSelectedIds(selectedUserIds);
const selectedPermissionKeys = normalizePermissionKeys(Array.isArray(req.body['permission_keys[]'])
? req.body['permission_keys[]']
: req.body.permission_keys
? [].concat(req.body.permission_keys)
: []);
const validPermissionKeys = new Set(permissions.map(function (permission) {
return String(permission.key || '').trim();
}));
const page = Math.max(1, Math.floor(Number(req.query.page) || 1));
const search = common.getSearchQuery(req);
const sort = common.getSortQuery(req);
const direction = common.getSortDirectionQuery(req);
const currentUserId = req.currentUser ? Number(req.currentUser.id) : null;
const createViewModel = await buildRoleCreateViewModel({ name: name, description: description }, selectedPermissionKeys, selectedUserIds, page, search, sort, direction, currentUserId);
if (!name) {
return res.status(400).send(pages.renderRbacAddPage('Role name is required.', req.currentUser, createViewModel.formValues, createViewModel.permissionGroups, createViewModel.users, createViewModel.pagination, 'warning'));
}
if (await common.fetchDuplicateName(pool, 'a_roles', name)) {
return res.status(400).send(pages.renderRbacAddPage('A role with that name already exists.', req.currentUser, createViewModel.formValues, createViewModel.permissionGroups, createViewModel.users, createViewModel.pagination, 'warning'));
}
if (selectedPermissionKeys.some(function (permissionKey) {
return !validPermissionKeys.has(permissionKey);
})) {
return res.status(400).send(pages.renderRbacAddPage('One or more selected permissions are invalid.', req.currentUser, createViewModel.formValues, createViewModel.permissionGroups, createViewModel.users, createViewModel.pagination, 'warning'));
}
const availableUsers = await rbacData.fetchUsersWithRoles(pool);
const availableUserIds = new Set(availableUsers.map(function (user) {
return Number(user.id);
}));
if (normalizedUserIds.some(function (userId) {
return !availableUserIds.has(userId);
})) {
return res.status(400).send(pages.renderRbacAddPage('One or more selected users are invalid.', req.currentUser, createViewModel.formValues, createViewModel.permissionGroups, createViewModel.users, createViewModel.pagination, 'warning'));
}
const roleKey = await createUniqueRoleKey(name);
const actorId = getAuditUserId(req);
const connection = await pool.getConnection();
let insertedRoleId = null;
try {
await connection.beginTransaction();
const [result] = await connection.query(
'INSERT INTO a_roles (role_key, name, description, created_by, modified_by) VALUES (?, ?, ?, ?, ?)',
[roleKey, name, description || null, actorId, actorId]
);
insertedRoleId = Number(result.insertId);
if (selectedPermissionKeys.length) {
await rbacData.syncRolePermissions(connection, insertedRoleId, selectedPermissionKeys);
}
await rbacData.syncRoleUsers(connection, insertedRoleId, normalizedUserIds);
await connection.commit();
} catch (error) {
await connection.rollback();
throw error;
} finally {
connection.release();
}
if (typeof recordRequestAuditEvent === 'function') {
await recordRequestAuditEvent(pool, req, {
category: 'roles',
eventType: 'role.created',
actorUserId: actorId,
targetType: 'role',
targetId: insertedRoleId,
targetLabel: name,
details: { permissionKeys: selectedPermissionKeys, userIds: normalizedUserIds }
});
}
res.redirect('/settings/roles/' + insertedRoleId + '/edit?message=' + encodeURIComponent('Role created.'));
} catch (error) {
next(error);
}
});
app.get('/settings/roles/:id/edit', requirePermission('rbac.update'), async function (req, res, next) {
try {
const roleId = Number(req.params.id);
if (!Number.isInteger(roleId) || roleId <= 0) {
return res.status(400).send('Invalid role.');
}
const viewModel = await loadRoleViewModel(roleId);
if (!viewModel) {
return res.status(404).send('Role not found.');
}
const page = Math.max(1, Math.floor(Number(req.query.page) || 1));
const search = common.getSearchQuery(req);
const sort = common.getSortQuery(req);
const direction = common.getSortDirectionQuery(req);
const currentUserId = req.currentUser ? Number(req.currentUser.id) : null;
const data = await rbacData.fetchUsersWithRolesPage(pool, page, LIST_PAGE_SIZE, search, sort, direction, {
excludeUserId: currentUserId
});
const users = mapUsersForView(data.users, viewModel.selectedUserIds);
viewModel.role.inUse = String(viewModel.role.role_key || '') === DEFAULT_ROLE.key || String(viewModel.role.role_key || '') === 'administrators' || Number(viewModel.role.user_count) > 0;
res.send(pages.renderRbacEditPage(viewModel.role, req.query.message ? String(req.query.message) : '', req.currentUser, viewModel.permissionGroups, users, buildPagination(data.totalItems, data.currentPage, 'page', { search: search, sort: sort, direction: direction }, LIST_PAGE_SIZE, 'users', 'User pages')));
} catch (error) {
next(error);
}
});
app.post('/settings/roles/:id', requirePermission('rbac.update'), async function (req, res, next) {
try {
const roleId = Number(req.params.id);
if (!Number.isInteger(roleId) || roleId <= 0) {
return res.status(400).send('Invalid role.');
}
const role = await rbacData.fetchRoleById(pool, roleId);
if (!role) {
return res.status(404).send('Role not found.');
}
const name = common.validateMaxLength(req.body.name || '', ROLE_NAME_MAX_LENGTH, 'Role name');
const description = common.validateMaxLength(req.body.description || '', ROLE_DESCRIPTION_MAX_LENGTH, 'Role description');
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 visibleUserIds = shouldSyncUsers
? normalizeSelectedIds(readArrayField(req.body, ['user_ids_present[]', 'user_ids_present']))
: [];
const normalizedPermissionKeys = shouldSyncPermissions ? normalizePermissionKeys(selectedPermissionKeys) : [];
let normalizedUserIds = shouldSyncUsers ? normalizeSelectedIds(selectedUserIds) : [];
if (!name) {
return res.redirect('/settings/roles/' + roleId + '/edit?message=' + encodeURIComponent('Role name is required.'));
}
if (await common.fetchDuplicateName(pool, 'a_roles', name, roleId)) {
return res.redirect('/settings/roles/' + roleId + '/edit?message=' + encodeURIComponent('A role with that name already exists.'));
}
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('/settings/roles/' + roleId + '/edit?message=' + encodeURIComponent('One or more selected permissions are invalid.'));
}
const existingRolePermissionKeys = shouldSyncPermissions
? await rbacData.fetchRolePermissionKeys(pool, roleId)
: [];
let existingRoleUserIds = [];
let availableUsers = [];
if (shouldSyncUsers) {
availableUsers = await rbacData.fetchUsersWithRoles(pool);
existingRoleUserIds = await rbacData.fetchRoleUserIds(pool, roleId);
const visibleUserIdSet = new Set(visibleUserIds);
normalizedUserIds = existingRoleUserIds.filter(function (userId) {
return !visibleUserIdSet.has(userId);
}).concat(normalizedUserIds);
}
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('/settings/roles/' + roleId + '/edit?message=' + encodeURIComponent('One or more selected users are invalid.'));
}
const connection = await pool.getConnection();
try {
await connection.beginTransaction();
await connection.query(
'UPDATE a_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();
}
if (typeof recordRequestAuditEvent === 'function') {
const changes = buildAuditChanges({
name: role.name,
description: role.description,
permissionKeys: existingRolePermissionKeys,
userIds: existingRoleUserIds
}, {
name: name,
description: description || null,
permissionKeys: normalizedPermissionKeys,
userIds: normalizedUserIds
});
await recordRequestAuditEvent(pool, req, {
category: 'roles',
eventType: 'role.updated',
actorUserId: req.currentUser.id,
targetType: 'role',
targetId: roleId,
targetLabel: name,
details: { changes: changes }
});
}
res.redirect('/settings/roles/' + roleId + '/edit?message=' + encodeURIComponent('Role updated.'));
} catch (error) {
next(error);
}
});
app.post('/settings/roles/:id/permissions', requirePermission('rbac.update'), async function (req, res, next) {
try {
const roleId = Number(req.params.id);
if (!Number.isInteger(roleId) || roleId <= 0) {
return res.status(400).send('Invalid role.');
}
const role = await rbacData.fetchRoleById(pool, roleId);
if (!role) {
return res.status(404).send('Role not found.');
}
const selectedPermissionKeys = Array.isArray(req.body['permission_keys[]'])
? req.body['permission_keys[]']
: req.body.permission_keys
? [].concat(req.body.permission_keys)
: [];
const validPermissionKeys = new Set(permissions.map(function (permission) {
return String(permission.key || '').trim();
}));
const normalizedPermissionKeys = normalizePermissionKeys(selectedPermissionKeys);
if (normalizedPermissionKeys.some(function (permissionKey) {
return !validPermissionKeys.has(permissionKey);
})) {
return res.redirect('/settings/roles/' + roleId + '/edit?message=' + encodeURIComponent('One or more selected permissions are invalid.'));
}
const existingPermissionKeys = await rbacData.fetchRolePermissionKeys(pool, roleId);
const connection = await pool.getConnection();
try {
await connection.beginTransaction();
await rbacData.syncRolePermissions(connection, roleId, normalizedPermissionKeys);
await connection.commit();
} catch (error) {
await connection.rollback();
throw error;
} finally {
connection.release();
}
if (typeof recordRequestAuditEvent === 'function') {
await recordRequestAuditEvent(pool, req, {
category: 'roles',
eventType: 'role.permissions_updated',
actorUserId: req.currentUser.id,
targetType: 'role',
targetId: roleId,
targetLabel: role.name,
details: { changes: buildAuditChanges({ permissionKeys: existingPermissionKeys }, { permissionKeys: normalizedPermissionKeys }) }
});
}
res.redirect('/settings/roles/' + roleId + '/edit?message=' + encodeURIComponent('Permissions updated.'));
} catch (error) {
next(error);
}
});
app.post('/settings/roles/:id/users', requirePermission('rbac.update'), async function (req, res, next) {
try {
const roleId = Number(req.params.id);
if (!Number.isInteger(roleId) || roleId <= 0) {
return res.status(400).send('Invalid role.');
}
const role = await rbacData.fetchRoleById(pool, roleId);
if (!role) {
return res.status(404).send('Role not found.');
}
const selectedUserIds = Array.isArray(req.body['user_ids[]'])
? req.body['user_ids[]']
: req.body.user_ids
? [].concat(req.body.user_ids)
: [];
const normalizedUserIds = normalizeSelectedIds(selectedUserIds);
const existingUserIds = await rbacData.fetchRoleUserIds(pool, roleId);
const availableUsers = await rbacData.fetchUsersWithRoles(pool);
const validUserIds = new Set(availableUsers.map(function (user) {
return Number(user.id);
}));
if (normalizedUserIds.some(function (userId) {
return !validUserIds.has(userId);
})) {
return res.redirect('/settings/roles/' + roleId + '/edit?message=' + encodeURIComponent('One or more selected users are invalid.'));
}
await rbacData.syncRoleUsers(pool, roleId, normalizedUserIds);
if (typeof recordRequestAuditEvent === 'function') {
await recordRequestAuditEvent(pool, req, {
category: 'roles',
eventType: 'role.users_updated',
actorUserId: req.currentUser.id,
targetType: 'role',
targetId: roleId,
targetLabel: role.name,
details: { changes: buildAuditChanges({ userIds: existingUserIds }, { userIds: normalizedUserIds }) }
});
}
res.redirect('/settings/roles/' + roleId + '/edit?message=' + encodeURIComponent('Users updated.'));
} catch (error) {
next(error);
}
});
app.post('/settings/roles/:id/delete', requirePermission('rbac.delete'), async function (req, res, next) {
try {
const roleId = Number(req.params.id);
if (!Number.isInteger(roleId) || roleId <= 0) {
return res.status(400).send('Invalid role.');
}
const role = await rbacData.fetchRoleById(pool, roleId);
if (!role) {
return res.status(404).send('Role not found.');
}
if (String(role.role_key || '') === DEFAULT_ROLE.key || String(role.role_key || '') === 'administrators') {
return res.redirect('/settings/roles?message=' + encodeURIComponent('The built-in Super Admin role cannot be deleted.'));
}
if (Number(role.user_count) > 0) {
return res.redirect('/settings/roles?message=' + encodeURIComponent('Remove all users from this role before deleting it.'));
}
await pool.query('DELETE FROM a_roles WHERE id = ?', [roleId]);
if (typeof recordRequestAuditEvent === 'function') {
await recordRequestAuditEvent(pool, req, {
category: 'roles',
eventType: 'role.deleted',
actorUserId: req.currentUser.id,
targetType: 'role',
targetId: roleId,
targetLabel: role.name
});
}
res.redirect('/settings/roles?message=' + encodeURIComponent('Role deleted.'));
} catch (error) {
next(error);
}
});
};