450 lines
17 KiB
JavaScript
450 lines
17 KiB
JavaScript
module.exports = function registerAdminRbacRoutes(app, deps) {
|
|
const pool = deps.pool;
|
|
const common = deps.common;
|
|
const pages = deps.pages;
|
|
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 { buildPagination } = require('../../lib/pagination');
|
|
const requirePermission = deps.requirePermission;
|
|
|
|
const LIST_PAGE_SIZE = 10;
|
|
|
|
function slugifyRoleKey(name) {
|
|
const value = String(name || '').trim().toLowerCase();
|
|
const slug = value.replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
|
|
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 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.sectionKey : String(permission.section_name || '').trim().toLowerCase().replace(/[^a-z0-9]+/g, '-'),
|
|
resourceName: definition ? definition.name : permission.section_name,
|
|
categoryName: definition ? definition.sectionName : permission.section_name,
|
|
sectionOrder: definition ? definition.sectionOrder : 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(),
|
|
sectionOrder: Number(permission.sectionOrder) || 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 actionOrder = { create: 1, read: 2, update: 3, edit: 3, delete: 4, allow: 5 };
|
|
const leftOrder = actionOrder[String(left.actionKey || '').trim()] || 99;
|
|
const rightOrder = actionOrder[String(right.actionKey || '').trim()] || 99;
|
|
if (leftOrder !== rightOrder) {
|
|
return leftOrder - rightOrder;
|
|
}
|
|
return String(left.name || '').localeCompare(String(right.name || ''));
|
|
});
|
|
});
|
|
|
|
groups.sort(function (left, right) {
|
|
const leftOrder = Number(left.sectionOrder) || 999;
|
|
const rightOrder = Number(right.sectionOrder) || 999;
|
|
if (leftOrder !== rightOrder) {
|
|
return leftOrder - rightOrder;
|
|
}
|
|
return String(left.title || '').localeCompare(String(right.title || ''));
|
|
});
|
|
|
|
return groups;
|
|
}
|
|
|
|
async function buildRoleCreateViewModel(formValues, selectedPermissionKeys) {
|
|
const permissionRows = await rbacData.fetchPermissions(pool);
|
|
return {
|
|
formValues: {
|
|
name: String(formValues && formValues.name || '').trim(),
|
|
description: String(formValues && formValues.description || '').trim()
|
|
},
|
|
permissionGroups: buildPermissionGroups(mapPermissionsForView(permissionRows, selectedPermissionKeys))
|
|
};
|
|
}
|
|
|
|
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);
|
|
const users = await rbacData.fetchUsersWithRoles(pool);
|
|
return {
|
|
role: Object.assign({}, role, {
|
|
permissionKeys: selectedPermissionKeys,
|
|
permissionCount: Number(role.permission_count) || 0,
|
|
userCount: Number(role.user_count) || 0
|
|
}),
|
|
permissionGroups: buildPermissionGroups(mapPermissionsForView(permissionRows, selectedPermissionKeys)),
|
|
users: mapUsersForView(users, selectedUserIds)
|
|
};
|
|
}
|
|
|
|
app.get('/rbac', requirePermission('rbac.read'), async function (req, res, next) {
|
|
try {
|
|
const page = Math.max(1, Math.floor(Number(req.query.page) || 1));
|
|
const data = await rbacData.fetchRolesPage(pool, page, LIST_PAGE_SIZE);
|
|
res.send(pages.renderRbacPage({
|
|
roles: data.roles || [],
|
|
pagination: buildPagination(data.totalItems, data.currentPage, 'page', {}, LIST_PAGE_SIZE, 'roles', 'Role pages')
|
|
}, req.query.message ? String(req.query.message) : '', req.currentUser));
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
});
|
|
|
|
app.get('/rbac/new', requirePermission('rbac.create'), function (req, res, next) {
|
|
buildRoleCreateViewModel({
|
|
name: String(req.query.name || '').trim(),
|
|
description: String(req.query.description || '').trim()
|
|
}, []).then(function (viewModel) {
|
|
res.send(pages.renderRbacAddPage(req.query.message ? String(req.query.message) : '', req.currentUser, viewModel.formValues, viewModel.permissionGroups, 'primary'));
|
|
}).catch(function (error) {
|
|
next(error);
|
|
});
|
|
});
|
|
|
|
app.post('/rbac', requirePermission('rbac.create'), async function (req, res, next) {
|
|
try {
|
|
const name = String(req.body.name || '').trim();
|
|
const description = String(req.body.description || '').trim();
|
|
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 createViewModel = await buildRoleCreateViewModel({ name: name, description: description }, selectedPermissionKeys);
|
|
if (!name) {
|
|
return res.status(400).send(pages.renderRbacAddPage('Role name is required.', req.currentUser, createViewModel.formValues, createViewModel.permissionGroups, 'warning'));
|
|
}
|
|
if (await common.fetchDuplicateName(pool, 'roles', name)) {
|
|
return res.status(400).send(pages.renderRbacAddPage('A role with that name already exists.', req.currentUser, createViewModel.formValues, createViewModel.permissionGroups, '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, '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 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 connection.commit();
|
|
} catch (error) {
|
|
await connection.rollback();
|
|
throw error;
|
|
} finally {
|
|
connection.release();
|
|
}
|
|
res.redirect('/rbac/' + insertedRoleId + '/edit?message=' + encodeURIComponent('Role created.'));
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
});
|
|
|
|
app.get('/rbac/: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 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('/rbac/: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 = 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('/rbac/' + roleId + '/edit?message=' + encodeURIComponent('Role name is required.'));
|
|
}
|
|
|
|
if (await common.fetchDuplicateName(pool, 'roles', name, roleId)) {
|
|
return res.redirect('/rbac/' + 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('/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('/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('/rbac/' + roleId + '/edit?message=' + encodeURIComponent('Role updated.'));
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
});
|
|
|
|
app.post('/rbac/: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('/rbac/' + roleId + '/edit?message=' + encodeURIComponent('One or more selected permissions are invalid.'));
|
|
}
|
|
|
|
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();
|
|
}
|
|
res.redirect('/rbac/' + roleId + '/edit?message=' + encodeURIComponent('Permissions updated.'));
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
});
|
|
|
|
app.post('/rbac/: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 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('/rbac/' + roleId + '/edit?message=' + encodeURIComponent('One or more selected users are invalid.'));
|
|
}
|
|
|
|
await rbacData.syncRoleUsers(pool, roleId, normalizedUserIds);
|
|
res.redirect('/rbac/' + roleId + '/edit?message=' + encodeURIComponent('Users updated.'));
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
});
|
|
|
|
app.post('/rbac/: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 || '') === 'administrators') {
|
|
return res.redirect('/rbac?message=' + encodeURIComponent('The built-in Administrators role cannot be deleted.'));
|
|
}
|
|
if (Number(role.user_count) > 0) {
|
|
return res.redirect('/rbac?message=' + encodeURIComponent('Remove all users from this role before deleting it.'));
|
|
}
|
|
|
|
await pool.query('DELETE FROM roles WHERE id = ?', [roleId]);
|
|
res.redirect('/rbac?message=' + encodeURIComponent('Role deleted.'));
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
});
|
|
};
|