Implement RBAC roles system
This commit is contained in:
@@ -16,6 +16,7 @@ module.exports = function registerAdminContentRoutes(app, deps) {
|
||||
const getSlideDeleteBlockMessage = deps.getSlideDeleteBlockMessage;
|
||||
const getTemplateDeleteBlockMessage = deps.getTemplateDeleteBlockMessage;
|
||||
const getCanvasSizeDeleteBlockMessage = deps.getCanvasSizeDeleteBlockMessage;
|
||||
const requirePermission = deps.requirePermission;
|
||||
|
||||
if (!pool || !common || !pages || !upload || typeof fetchScreensBySlideId !== 'function' || typeof fetchScreensByTemplateId !== 'function' || typeof collectUploadReferencesFromSlide !== 'function' || typeof collectUploadReferencesFromTemplate !== 'function' || typeof collectUploadReferencesFromPayload !== 'function' || typeof syncPlaylistUploadsOnChange !== 'function' || typeof getAuditUserId !== 'function' || typeof redirectAfterSave !== 'function' || typeof notifyPlayerScreens !== 'function' || typeof broadcastDashboardState !== 'function' || typeof getSlideDeleteBlockMessage !== 'function' || typeof getTemplateDeleteBlockMessage !== 'function' || typeof getCanvasSizeDeleteBlockMessage !== 'function') {
|
||||
throw new Error('registerAdminContentRoutes requires the content route dependencies.');
|
||||
@@ -55,7 +56,7 @@ module.exports = function registerAdminContentRoutes(app, deps) {
|
||||
}, {});
|
||||
}
|
||||
|
||||
app.get('/admin/slides', async function (req, res, next) {
|
||||
app.get('/admin/slides', requirePermission('slides.read'), async function (req, res, next) {
|
||||
try {
|
||||
const data = await common.fetchAdminData(pool);
|
||||
res.send(pages.renderSlidesPage(data, req.query.message ? String(req.query.message) : '', req.currentUser));
|
||||
@@ -64,7 +65,7 @@ module.exports = function registerAdminContentRoutes(app, deps) {
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/admin/slides/new', async function (req, res, next) {
|
||||
app.get('/admin/slides/new', requirePermission('slides.create'), async function (req, res, next) {
|
||||
try {
|
||||
const data = await common.fetchTemplatesData(pool);
|
||||
res.send(pages.renderSlideFormPage(data, 'create', null, req.query.message ? String(req.query.message) : '', req.currentUser));
|
||||
@@ -73,7 +74,7 @@ module.exports = function registerAdminContentRoutes(app, deps) {
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/admin/slides/:id/edit', async function (req, res, next) {
|
||||
app.get('/admin/slides/:id/edit', requirePermission('slides.edit'), async function (req, res, next) {
|
||||
try {
|
||||
const slide = await common.fetchSlideById(pool, Number(req.params.id));
|
||||
if (!slide) {
|
||||
@@ -86,7 +87,7 @@ module.exports = function registerAdminContentRoutes(app, deps) {
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/admin/slides', upload.any(), async function (req, res, next) {
|
||||
app.post('/admin/slides', requirePermission('slides.create'), upload.any(), async function (req, res, next) {
|
||||
try {
|
||||
const payload = await common.buildSlidePayload(pool, req, null);
|
||||
const actorId = getAuditUserId(req);
|
||||
@@ -110,7 +111,7 @@ module.exports = function registerAdminContentRoutes(app, deps) {
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/admin/slides/:id', upload.any(), async function (req, res, next) {
|
||||
app.post('/admin/slides/:id', requirePermission('slides.edit'), upload.any(), async function (req, res, next) {
|
||||
try {
|
||||
const slide = await common.fetchSlideById(pool, Number(req.params.id));
|
||||
if (!slide) {
|
||||
@@ -147,7 +148,7 @@ module.exports = function registerAdminContentRoutes(app, deps) {
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/admin/slides/:id/delete', async function (req, res, next) {
|
||||
app.post('/admin/slides/:id/delete', requirePermission('slides.delete'), async function (req, res, next) {
|
||||
try {
|
||||
const slide = await common.fetchSlideById(pool, Number(req.params.id));
|
||||
if (!slide) {
|
||||
@@ -177,7 +178,7 @@ module.exports = function registerAdminContentRoutes(app, deps) {
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/admin/templates', async function (req, res, next) {
|
||||
app.get('/admin/templates', requirePermission('templates.read'), async function (req, res, next) {
|
||||
try {
|
||||
const data = await common.fetchTemplatesData(pool);
|
||||
res.send(pages.renderTemplatesPage(data, req.query.message ? String(req.query.message) : '', req.currentUser));
|
||||
@@ -186,7 +187,7 @@ module.exports = function registerAdminContentRoutes(app, deps) {
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/admin/templates/new', async function (req, res, next) {
|
||||
app.get('/admin/templates/new', requirePermission('templates.create'), async function (req, res, next) {
|
||||
try {
|
||||
const data = await common.fetchCanvasSizesData(pool);
|
||||
res.send(pages.renderTemplateFormPage(null, 'create', req.query.message ? String(req.query.message) : '', data.canvasSizes, req.currentUser));
|
||||
@@ -195,7 +196,7 @@ module.exports = function registerAdminContentRoutes(app, deps) {
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/admin/templates', upload.any(), async function (req, res, next) {
|
||||
app.post('/admin/templates', requirePermission('templates.create'), upload.any(), async function (req, res, next) {
|
||||
try {
|
||||
const payload = await common.buildTemplatePayload(pool, req, null);
|
||||
const actorId = getAuditUserId(req);
|
||||
@@ -226,7 +227,7 @@ module.exports = function registerAdminContentRoutes(app, deps) {
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/admin/templates/:id/edit', async function (req, res, next) {
|
||||
app.get('/admin/templates/:id/edit', requirePermission('templates.edit'), async function (req, res, next) {
|
||||
try {
|
||||
const template = await common.fetchTemplateById(pool, Number(req.params.id));
|
||||
if (!template) {
|
||||
@@ -239,7 +240,7 @@ module.exports = function registerAdminContentRoutes(app, deps) {
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/admin/templates/:id', upload.any(), async function (req, res, next) {
|
||||
app.post('/admin/templates/:id', requirePermission('templates.edit'), upload.any(), async function (req, res, next) {
|
||||
try {
|
||||
const template = await common.fetchTemplateById(pool, Number(req.params.id));
|
||||
if (!template) {
|
||||
@@ -280,7 +281,7 @@ module.exports = function registerAdminContentRoutes(app, deps) {
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/admin/templates/:id/delete', async function (req, res, next) {
|
||||
app.post('/admin/templates/:id/delete', requirePermission('templates.delete'), async function (req, res, next) {
|
||||
try {
|
||||
const template = await common.fetchTemplateById(pool, Number(req.params.id));
|
||||
if (!template) {
|
||||
@@ -308,7 +309,7 @@ module.exports = function registerAdminContentRoutes(app, deps) {
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/admin/canvas-sizes', async function (req, res, next) {
|
||||
app.get('/admin/canvas-sizes', requirePermission('canvas-sizes.read'), async function (req, res, next) {
|
||||
try {
|
||||
const data = await common.fetchAdminData(pool);
|
||||
res.send(pages.renderCanvasSizesPage(data, req.query.message ? String(req.query.message) : '', req.currentUser));
|
||||
@@ -317,11 +318,11 @@ module.exports = function registerAdminContentRoutes(app, deps) {
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/admin/canvas-sizes/new', function (req, res) {
|
||||
app.get('/admin/canvas-sizes/new', requirePermission('canvas-sizes.create'), function (req, res) {
|
||||
res.send(pages.renderCanvasSizeFormPage(null, 'create', req.query.message ? String(req.query.message) : '', req.currentUser));
|
||||
});
|
||||
|
||||
app.post('/admin/canvas-sizes', async function (req, res, next) {
|
||||
app.post('/admin/canvas-sizes', requirePermission('canvas-sizes.create'), async function (req, res, next) {
|
||||
try {
|
||||
const payload = common.buildCanvasSizePayload(req, null);
|
||||
if (await canvasSizeExists(payload.width, payload.height)) {
|
||||
@@ -339,7 +340,7 @@ module.exports = function registerAdminContentRoutes(app, deps) {
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/admin/canvas-sizes/:id/edit', async function (req, res, next) {
|
||||
app.get('/admin/canvas-sizes/:id/edit', requirePermission('canvas-sizes.edit'), async function (req, res, next) {
|
||||
try {
|
||||
const canvasSize = await common.fetchCanvasSizeById(pool, Number(req.params.id));
|
||||
if (!canvasSize) {
|
||||
@@ -351,7 +352,7 @@ module.exports = function registerAdminContentRoutes(app, deps) {
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/admin/canvas-sizes/:id', async function (req, res, next) {
|
||||
app.post('/admin/canvas-sizes/:id', requirePermission('canvas-sizes.edit'), async function (req, res, next) {
|
||||
try {
|
||||
const canvasSize = await common.fetchCanvasSizeById(pool, Number(req.params.id));
|
||||
if (!canvasSize) {
|
||||
@@ -372,7 +373,7 @@ module.exports = function registerAdminContentRoutes(app, deps) {
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/admin/canvas-sizes/:id/delete', async function (req, res, next) {
|
||||
app.post('/admin/canvas-sizes/:id/delete', requirePermission('canvas-sizes.delete'), async function (req, res, next) {
|
||||
try {
|
||||
const canvasSize = await common.fetchCanvasSizeById(pool, Number(req.params.id));
|
||||
if (!canvasSize) {
|
||||
|
||||
@@ -19,6 +19,7 @@ module.exports = function registerAdminManageRoutes(app, deps) {
|
||||
const getPlaylistDeleteBlockMessage = deps.getPlaylistDeleteBlockMessage;
|
||||
const forwardPlayerCommand = deps.forwardPlayerCommand;
|
||||
const PLAYER_PUBLIC_BASE_URL = deps.playerPublicBaseUrl;
|
||||
const requirePermission = deps.requirePermission;
|
||||
|
||||
async function fetchAllScreenSlugs() {
|
||||
const [rows] = await pool.query('SELECT slug FROM screens ORDER BY slug ASC');
|
||||
@@ -29,7 +30,7 @@ module.exports = function registerAdminManageRoutes(app, deps) {
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
app.post('/admin/commands', async function (req, res, next) {
|
||||
app.post('/admin/commands', requirePermission('dashboard.allow'), async function (req, res, next) {
|
||||
try {
|
||||
const command = String((req.body && req.body.command) || req.query.command || '').trim().toLowerCase();
|
||||
const blackoutValue = req.body && Object.prototype.hasOwnProperty.call(req.body, 'blackout')
|
||||
@@ -71,7 +72,7 @@ module.exports = function registerAdminManageRoutes(app, deps) {
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/admin/playlists', async function (req, res, next) {
|
||||
app.post('/admin/playlists', requirePermission('playlists.create'), async function (req, res, next) {
|
||||
try {
|
||||
const name = String(req.body.name || '').trim();
|
||||
const fadeBetweenSlides = req.body.fade_between_slides ? 1 : 0;
|
||||
@@ -90,7 +91,7 @@ module.exports = function registerAdminManageRoutes(app, deps) {
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/admin/playlists/:id', async function (req, res, next) {
|
||||
app.post('/admin/playlists/:id', requirePermission('playlists.edit'), async function (req, res, next) {
|
||||
const connection = await pool.getConnection();
|
||||
try {
|
||||
const name = String(req.body.name || '').trim();
|
||||
@@ -255,7 +256,7 @@ module.exports = function registerAdminManageRoutes(app, deps) {
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/admin/playlists/:id/delete', async function (req, res, next) {
|
||||
app.post('/admin/playlists/:id/delete', requirePermission('playlists.delete'), async function (req, res, next) {
|
||||
try {
|
||||
const playlist = await common.fetchPlaylistById(pool, Number(req.params.id));
|
||||
if (!playlist) {
|
||||
@@ -272,7 +273,7 @@ module.exports = function registerAdminManageRoutes(app, deps) {
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/admin/playlists/:id/slides', async function (req, res, next) {
|
||||
app.post('/admin/playlists/:id/slides', requirePermission('playlists.edit'), async function (req, res, next) {
|
||||
try {
|
||||
const playlist = await common.fetchPlaylistById(pool, Number(req.params.id));
|
||||
if (!playlist) {
|
||||
@@ -307,7 +308,7 @@ module.exports = function registerAdminManageRoutes(app, deps) {
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/admin/playlists/:id/slides/:playlistSlideId', async function (req, res, next) {
|
||||
app.post('/admin/playlists/:id/slides/:playlistSlideId', requirePermission('playlists.edit'), async function (req, res, next) {
|
||||
try {
|
||||
const playlist = await common.fetchPlaylistById(pool, Number(req.params.id));
|
||||
if (!playlist) {
|
||||
@@ -331,7 +332,7 @@ module.exports = function registerAdminManageRoutes(app, deps) {
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/admin/playlists/:id/slides/:playlistSlideId/move', async function (req, res, next) {
|
||||
app.post('/admin/playlists/:id/slides/:playlistSlideId/move', requirePermission('playlists.edit'), async function (req, res, next) {
|
||||
const connection = await pool.getConnection();
|
||||
try {
|
||||
const playlist = await common.fetchPlaylistById(connection, Number(req.params.id));
|
||||
@@ -378,7 +379,7 @@ module.exports = function registerAdminManageRoutes(app, deps) {
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/admin/playlists/:id/slides/:playlistSlideId/config', async function (req, res, next) {
|
||||
app.get('/admin/playlists/:id/slides/:playlistSlideId/config', requirePermission('playlists.edit'), async function (req, res, next) {
|
||||
try {
|
||||
const playlist = await common.fetchPlaylistById(pool, Number(req.params.id));
|
||||
if (!playlist) {
|
||||
@@ -415,7 +416,7 @@ module.exports = function registerAdminManageRoutes(app, deps) {
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/admin/playlists/:id/slides/:playlistSlideId/config', async function (req, res, next) {
|
||||
app.post('/admin/playlists/:id/slides/:playlistSlideId/config', requirePermission('playlists.edit'), async function (req, res, next) {
|
||||
try {
|
||||
const playlist = await common.fetchPlaylistById(pool, Number(req.params.id));
|
||||
if (!playlist) {
|
||||
@@ -495,7 +496,7 @@ module.exports = function registerAdminManageRoutes(app, deps) {
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/admin/playlists/:id/slides/:playlistSlideId/delete', async function (req, res, next) {
|
||||
app.post('/admin/playlists/:id/slides/:playlistSlideId/delete', requirePermission('playlists.edit'), async function (req, res, next) {
|
||||
try {
|
||||
const playlist = await common.fetchPlaylistById(pool, Number(req.params.id));
|
||||
if (!playlist) {
|
||||
@@ -510,7 +511,7 @@ module.exports = function registerAdminManageRoutes(app, deps) {
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/admin/screens/new', async function (req, res, next) {
|
||||
app.get('/admin/screens/new', requirePermission('screens.create'), async function (req, res, next) {
|
||||
try {
|
||||
const data = await common.fetchScreenEditData(pool);
|
||||
res.send(pages.renderScreenFormPage(data, req.query.message ? String(req.query.message) : '', req.currentUser));
|
||||
@@ -519,7 +520,7 @@ module.exports = function registerAdminManageRoutes(app, deps) {
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/admin/screens', async function (req, res, next) {
|
||||
app.post('/admin/screens', requirePermission('screens.create'), async function (req, res, next) {
|
||||
try {
|
||||
const name = String(req.body.name || '').trim();
|
||||
if (!name) {
|
||||
@@ -540,7 +541,7 @@ module.exports = function registerAdminManageRoutes(app, deps) {
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/admin/screens/:id', async function (req, res, next) {
|
||||
app.post('/admin/screens/:id', requirePermission('screens.edit'), async function (req, res, next) {
|
||||
try {
|
||||
const name = String(req.body.name || '').trim();
|
||||
if (!name) {
|
||||
@@ -575,7 +576,7 @@ module.exports = function registerAdminManageRoutes(app, deps) {
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/admin/screens/:id/delete', async function (req, res, next) {
|
||||
app.post('/admin/screens/:id/delete', requirePermission('screens.delete'), async function (req, res, next) {
|
||||
try {
|
||||
const screen = await common.fetchScreenById(pool, Number(req.params.id));
|
||||
if (!screen) {
|
||||
|
||||
@@ -3,8 +3,16 @@ module.exports = function registerAdminPagesRoutes(app, deps) {
|
||||
const common = deps.common;
|
||||
const pages = deps.pages;
|
||||
const buildDashboardState = deps.buildDashboardState;
|
||||
const requirePermission = deps.requirePermission;
|
||||
|
||||
app.get('/admin', async function (req, res, next) {
|
||||
function requireQueryPermission(readPermissionKey, editPermissionKey) {
|
||||
return function (req, res, next) {
|
||||
const permissionKey = req.query && req.query.edit ? editPermissionKey : readPermissionKey;
|
||||
return requirePermission(permissionKey)(req, res, next);
|
||||
};
|
||||
}
|
||||
|
||||
app.get('/admin', requirePermission('dashboard.read'), async function (req, res, next) {
|
||||
try {
|
||||
const data = await buildDashboardState(pool);
|
||||
res.send(pages.renderDashboardPage(data, req.query.message ? String(req.query.message) : '', req.currentUser));
|
||||
@@ -13,7 +21,7 @@ module.exports = function registerAdminPagesRoutes(app, deps) {
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/admin/clients', async function (req, res, next) {
|
||||
app.get('/admin/clients', requirePermission('clients.read'), async function (req, res, next) {
|
||||
try {
|
||||
const data = await buildDashboardState(pool);
|
||||
res.send(pages.renderConnectedClientsPage(data, req.query.message ? String(req.query.message) : '', req.currentUser));
|
||||
@@ -22,7 +30,7 @@ module.exports = function registerAdminPagesRoutes(app, deps) {
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/admin/screens', async function (req, res, next) {
|
||||
app.get('/admin/screens', requireQueryPermission('screens.read', 'screens.edit'), async function (req, res, next) {
|
||||
try {
|
||||
const data = await buildDashboardState(pool);
|
||||
if (req.query.edit) {
|
||||
@@ -39,7 +47,7 @@ module.exports = function registerAdminPagesRoutes(app, deps) {
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/admin/screens/:id/edit', async function (req, res, next) {
|
||||
app.get('/admin/screens/:id/edit', requirePermission('screens.edit'), async function (req, res, next) {
|
||||
try {
|
||||
const screen = await common.fetchScreenById(pool, Number(req.params.id));
|
||||
if (!screen) {
|
||||
@@ -52,7 +60,7 @@ module.exports = function registerAdminPagesRoutes(app, deps) {
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/admin/playlists', async function (req, res, next) {
|
||||
app.get('/admin/playlists', requireQueryPermission('playlists.read', 'playlists.edit'), async function (req, res, next) {
|
||||
try {
|
||||
const data = await common.fetchAdminData(pool);
|
||||
if (req.query.edit) {
|
||||
@@ -68,11 +76,11 @@ module.exports = function registerAdminPagesRoutes(app, deps) {
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/admin/playlists/new', function (req, res) {
|
||||
app.get('/admin/playlists/new', requirePermission('playlists.create'), function (req, res) {
|
||||
res.send(pages.renderPlaylistFormPage(req.query.message ? String(req.query.message) : '', req.currentUser));
|
||||
});
|
||||
|
||||
app.get('/admin/playlists/:id/edit', async function (req, res, next) {
|
||||
app.get('/admin/playlists/:id/edit', requirePermission('playlists.edit'), async function (req, res, next) {
|
||||
try {
|
||||
const playlist = await common.fetchPlaylistById(pool, Number(req.params.id));
|
||||
if (!playlist) {
|
||||
|
||||
@@ -0,0 +1,365 @@
|
||||
module.exports = function registerAdminRbacRoutes(app, deps) {
|
||||
const pool = deps.pool;
|
||||
const pages = deps.pages;
|
||||
const getAuditUserId = deps.getAuditUserId;
|
||||
const rbacData = deps.rbacData;
|
||||
const permissions = Array.isArray(deps.permissions) ? deps.permissions : [];
|
||||
const normalizePermissionKeys = deps.normalizePermissionKeys;
|
||||
const requirePermission = deps.requirePermission;
|
||||
|
||||
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('/admin/rbac', requirePermission('rbac.read'), async function (req, res, next) {
|
||||
try {
|
||||
const roles = await rbacData.fetchRoles(pool);
|
||||
res.send(pages.renderRbacPage({ roles: roles }, req.query.message ? String(req.query.message) : '', req.currentUser));
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/admin/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('/admin/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 (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();
|
||||
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]
|
||||
);
|
||||
if (selectedPermissionKeys.length) {
|
||||
await rbacData.syncRolePermissions(connection, Number(result.insertId), selectedPermissionKeys);
|
||||
}
|
||||
await connection.commit();
|
||||
} catch (error) {
|
||||
await connection.rollback();
|
||||
throw error;
|
||||
} finally {
|
||||
connection.release();
|
||||
}
|
||||
res.redirect('/admin/rbac?message=' + encodeURIComponent('Role created.'));
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/admin/rbac/:id/edit', requirePermission('rbac.edit'), 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.');
|
||||
}
|
||||
|
||||
res.send(pages.renderRbacEditPage(viewModel.role, req.query.message ? String(req.query.message) : '', req.currentUser, viewModel.permissionGroups, viewModel.users));
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/admin/rbac/:id', requirePermission('rbac.edit'), 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();
|
||||
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]
|
||||
);
|
||||
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) {
|
||||
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('/admin/rbac/' + roleId + '/edit?message=' + encodeURIComponent('One or more selected permissions are invalid.'));
|
||||
}
|
||||
|
||||
await rbacData.syncRolePermissions(pool, roleId, normalizedPermissionKeys);
|
||||
res.redirect('/admin/rbac/' + roleId + '/edit?message=' + encodeURIComponent('Permissions updated.'));
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/admin/rbac/:id/users', requirePermission('rbac.edit'), 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('/admin/rbac/' + roleId + '/edit?message=' + encodeURIComponent('One or more selected users are invalid.'));
|
||||
}
|
||||
|
||||
await rbacData.syncRoleUsers(pool, roleId, normalizedUserIds);
|
||||
res.redirect('/admin/rbac/' + roleId + '/edit?message=' + encodeURIComponent('Users updated.'));
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/admin/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('/admin/rbac?message=' + encodeURIComponent('The built-in Administrators role cannot be deleted.'));
|
||||
}
|
||||
if (Number(role.user_count) > 0) {
|
||||
return res.redirect('/admin/rbac?message=' + encodeURIComponent('Remove all users from this role before deleting it.'));
|
||||
}
|
||||
|
||||
await pool.query('DELETE FROM roles WHERE id = ?', [roleId]);
|
||||
res.redirect('/admin/rbac?message=' + encodeURIComponent('Role deleted.'));
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -4,8 +4,9 @@ module.exports = function registerAdminScreenCommandRoutes(app, deps) {
|
||||
const getScreenConnections = deps.getScreenConnections;
|
||||
const isClientNameAvailable = deps.isClientNameAvailable;
|
||||
const withClientNameReservation = deps.withClientNameReservation;
|
||||
const requirePermission = deps.requirePermission;
|
||||
|
||||
app.post('/admin/screens/:slug/commands', async function (req, res, next) {
|
||||
app.post('/admin/screens/: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();
|
||||
|
||||
+127
-23
@@ -4,36 +4,85 @@ module.exports = function registerAdminUsersRoutes(app, deps) {
|
||||
const formatDashboardDate = deps.formatDashboardDate;
|
||||
const getAuditUserId = deps.getAuditUserId;
|
||||
const hashPassword = deps.hashPassword;
|
||||
const readArrayField = deps.readArrayField;
|
||||
const rbacData = deps.rbacData;
|
||||
const requirePermission = deps.requirePermission;
|
||||
|
||||
app.get('/admin/users', async function (req, res, next) {
|
||||
async function fetchRoleOptions() {
|
||||
return rbacData.fetchRoles(pool);
|
||||
}
|
||||
|
||||
function mapRolesForForm(roles, selectedRoleIds) {
|
||||
const selectedIds = new Set((Array.isArray(selectedRoleIds) ? selectedRoleIds : []).map(function (roleId) {
|
||||
return Number(roleId);
|
||||
}).filter(function (roleId) {
|
||||
return Number.isInteger(roleId) && roleId > 0;
|
||||
}));
|
||||
|
||||
return (Array.isArray(roles) ? roles : []).map(function (role) {
|
||||
return Object.assign({}, role, {
|
||||
isSelected: selectedIds.has(Number(role.id))
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function validateRoleIds(roleIds) {
|
||||
const availableRoles = await fetchRoleOptions();
|
||||
const validRoleIds = new Set(availableRoles.map(function (role) {
|
||||
return Number(role.id);
|
||||
}));
|
||||
const normalizedRoleIds = Array.from(new Set((Array.isArray(roleIds) ? roleIds : []).map(function (roleId) {
|
||||
return Number(roleId);
|
||||
}).filter(function (roleId) {
|
||||
return Number.isInteger(roleId) && roleId > 0;
|
||||
})));
|
||||
|
||||
if (!normalizedRoleIds.length) {
|
||||
return { ok: false, message: 'Select at least one role.' };
|
||||
}
|
||||
|
||||
if (normalizedRoleIds.some(function (roleId) {
|
||||
return !validRoleIds.has(roleId);
|
||||
})) {
|
||||
return { ok: false, message: 'One or more selected roles are invalid.' };
|
||||
}
|
||||
|
||||
return { ok: true, roleIds: normalizedRoleIds };
|
||||
}
|
||||
|
||||
app.get('/admin/users', requirePermission('users.read'), async function (req, res, next) {
|
||||
try {
|
||||
const [rows] = await pool.query('SELECT id, name, username, created_at, modified_at FROM users ORDER BY id ASC');
|
||||
const users = rows.map(function (user) {
|
||||
const users = await rbacData.fetchUsersWithRoles(pool);
|
||||
const mappedUsers = users.map(function (user) {
|
||||
return Object.assign({}, user, {
|
||||
isCurrentUser: Number(user.id) === Number(req.currentUser.id),
|
||||
createdAtLabel: formatDashboardDate(user.created_at),
|
||||
modifiedAtLabel: formatDashboardDate(user.modified_at)
|
||||
modifiedAtLabel: formatDashboardDate(user.modified_at),
|
||||
roleNames: String(user.roleNames || '').trim() || 'No roles assigned'
|
||||
});
|
||||
});
|
||||
res.send(pages.renderUsersPage({ users: users }, req.query.message ? String(req.query.message) : '', req.currentUser));
|
||||
res.send(pages.renderUsersPage({ users: mappedUsers }, req.query.message ? String(req.query.message) : '', req.currentUser));
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/admin/users/new', function (req, res) {
|
||||
res.send(pages.renderUsersAddPage(req.query.message ? String(req.query.message) : '', req.currentUser));
|
||||
app.get('/admin/users/new', requirePermission('users.create'), function (req, res) {
|
||||
fetchRoleOptions().then(function (roles) {
|
||||
res.send(pages.renderUsersAddPage(req.query.message ? String(req.query.message) : '', req.currentUser, mapRolesForForm(roles, []), {}, 'primary'));
|
||||
}).catch(function (error) {
|
||||
res.status(500).send(error.message || 'Unable to load roles.');
|
||||
});
|
||||
});
|
||||
|
||||
app.get('/admin/users/:id/edit', async function (req, res, next) {
|
||||
app.get('/admin/users/:id/edit', requirePermission('users.edit'), async function (req, res, next) {
|
||||
try {
|
||||
const userId = Number(req.params.id);
|
||||
if (!Number.isInteger(userId) || userId <= 0) {
|
||||
return res.status(400).send('Invalid user.');
|
||||
}
|
||||
|
||||
const [rows] = await pool.query('SELECT id, name, username, created_at, modified_at FROM users WHERE id = ? LIMIT 1', [userId]);
|
||||
const user = rows[0] || null;
|
||||
const user = await rbacData.fetchUserWithRoles(pool, userId);
|
||||
if (!user) {
|
||||
return res.status(404).send('User not found.');
|
||||
}
|
||||
@@ -41,54 +90,109 @@ module.exports = function registerAdminUsersRoutes(app, deps) {
|
||||
return res.redirect('/admin/account?message=' + encodeURIComponent('Use My Account to update your own login details.'));
|
||||
}
|
||||
|
||||
const roles = await fetchRoleOptions();
|
||||
res.send(pages.renderUsersEditPage(Object.assign({}, user, {
|
||||
isCurrentUser: false,
|
||||
createdAtLabel: formatDashboardDate(user.created_at),
|
||||
modifiedAtLabel: formatDashboardDate(user.modified_at)
|
||||
}), req.query.message ? String(req.query.message) : '', req.currentUser));
|
||||
modifiedAtLabel: formatDashboardDate(user.modified_at),
|
||||
roleNames: String(user.roleNames || '').trim() || 'No roles assigned'
|
||||
}), req.query.message ? String(req.query.message) : '', req.currentUser, mapRolesForForm(roles, user.roleIds)));
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/admin/users', async function (req, res, next) {
|
||||
app.post('/admin/users', requirePermission('users.create'), async function (req, res, next) {
|
||||
const connection = await pool.getConnection();
|
||||
try {
|
||||
const name = String(req.body.name || '').trim();
|
||||
const username = String(req.body.username || '').trim();
|
||||
const password = String(req.body.password || '');
|
||||
const confirmPassword = String(req.body.confirm_password || '');
|
||||
const selectedRoleIds = readArrayField(req.body, ['role_ids[]', 'role_ids']);
|
||||
const roleCheck = await validateRoleIds(selectedRoleIds);
|
||||
const formValues = {
|
||||
username: username,
|
||||
name: name
|
||||
};
|
||||
|
||||
async function renderValidationError(message) {
|
||||
const roles = await fetchRoleOptions();
|
||||
return res.status(400).send(pages.renderUsersAddPage(message, req.currentUser, mapRolesForForm(roles, selectedRoleIds), formValues, 'warning'));
|
||||
}
|
||||
|
||||
if (!name) {
|
||||
return res.redirect('/admin/users/new?message=' + encodeURIComponent('Name is required.'));
|
||||
return renderValidationError('Name is required.');
|
||||
}
|
||||
if (!username) {
|
||||
return res.redirect('/admin/users/new?message=' + encodeURIComponent('Username is required.'));
|
||||
return renderValidationError('Username is required.');
|
||||
}
|
||||
if (!password || password.length < 8) {
|
||||
return res.redirect('/admin/users/new?message=' + encodeURIComponent('Password must be at least 8 characters.'));
|
||||
return renderValidationError('Password must be at least 8 characters.');
|
||||
}
|
||||
if (password !== confirmPassword) {
|
||||
return res.redirect('/admin/users/new?message=' + encodeURIComponent('Passwords do not match.'));
|
||||
return renderValidationError('Passwords do not match.');
|
||||
}
|
||||
if (!roleCheck.ok) {
|
||||
return renderValidationError(roleCheck.message);
|
||||
}
|
||||
|
||||
const [existingRows] = await pool.query('SELECT id FROM users WHERE username = ? LIMIT 1', [username]);
|
||||
const [existingRows] = await connection.query('SELECT id FROM users WHERE username = ? LIMIT 1', [username]);
|
||||
if (existingRows.length) {
|
||||
return res.redirect('/admin/users/new?message=' + encodeURIComponent('That username already exists.'));
|
||||
return renderValidationError('That username already exists.');
|
||||
}
|
||||
|
||||
const passwordRecord = hashPassword(password);
|
||||
const actorId = getAuditUserId(req);
|
||||
await pool.query(
|
||||
await connection.beginTransaction();
|
||||
const [result] = await connection.query(
|
||||
'INSERT INTO users (name, username, password_hash, password_salt, password_iterations, created_by, modified_by) VALUES (?, ?, ?, ?, ?, ?, ?)',
|
||||
[name, username, passwordRecord.hash, passwordRecord.salt, passwordRecord.iterations, actorId, actorId]
|
||||
);
|
||||
await rbacData.syncUserRoles(connection, result.insertId, roleCheck.roleIds);
|
||||
await connection.commit();
|
||||
res.redirect('/admin/users?message=' + encodeURIComponent('User created.'));
|
||||
} catch (error) {
|
||||
try {
|
||||
await connection.rollback();
|
||||
} catch (_rollbackError) {
|
||||
// Ignore rollback failures and surface the original error.
|
||||
}
|
||||
next(error);
|
||||
} finally {
|
||||
connection.release();
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/admin/users/:id/roles', requirePermission('users.edit'), async function (req, res, next) {
|
||||
try {
|
||||
const userId = Number(req.params.id);
|
||||
if (!Number.isInteger(userId) || userId <= 0) {
|
||||
return res.status(400).send('Invalid user.');
|
||||
}
|
||||
if (Number(req.currentUser.id) === userId) {
|
||||
return res.redirect('/admin/account?message=' + encodeURIComponent('Use My Account to update your own login details.'));
|
||||
}
|
||||
|
||||
const [rows] = await pool.query('SELECT id FROM users WHERE id = ? LIMIT 1', [userId]);
|
||||
if (!rows.length) {
|
||||
return res.status(404).send('User not found.');
|
||||
}
|
||||
|
||||
const selectedRoleIds = readArrayField(req.body, ['role_ids[]', 'role_ids']);
|
||||
const roleCheck = await validateRoleIds(selectedRoleIds);
|
||||
if (!roleCheck.ok) {
|
||||
return res.redirect('/admin/users/' + userId + '/edit?message=' + encodeURIComponent(roleCheck.message));
|
||||
}
|
||||
|
||||
await rbacData.syncUserRoles(pool, userId, roleCheck.roleIds);
|
||||
res.redirect('/admin/users/' + userId + '/edit?message=' + encodeURIComponent('Roles updated.'));
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/admin/users/:id/username', async function (req, res, next) {
|
||||
app.post('/admin/users/:id/username', requirePermission('users.edit'), async function (req, res, next) {
|
||||
try {
|
||||
const userId = Number(req.params.id);
|
||||
const name = String(req.body.name || '').trim();
|
||||
@@ -122,7 +226,7 @@ module.exports = function registerAdminUsersRoutes(app, deps) {
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/admin/users/:id/password', async function (req, res, next) {
|
||||
app.post('/admin/users/:id/password', requirePermission('users.edit'), async function (req, res, next) {
|
||||
try {
|
||||
const userId = Number(req.params.id);
|
||||
const password = String(req.body.password || '');
|
||||
@@ -158,7 +262,7 @@ module.exports = function registerAdminUsersRoutes(app, deps) {
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/admin/users/:id/delete', async function (req, res, next) {
|
||||
app.post('/admin/users/:id/delete', requirePermission('users.delete'), async function (req, res, next) {
|
||||
try {
|
||||
const userId = Number(req.params.id);
|
||||
if (!Number.isInteger(userId) || userId <= 0) {
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
const { renderView } = require('../view');
|
||||
|
||||
function getErrorCopy(statusCode, message) {
|
||||
const normalizedStatusCode = Number(statusCode) || 500;
|
||||
|
||||
if (normalizedStatusCode === 403) {
|
||||
return {
|
||||
title: 'Access denied',
|
||||
errorTitle: 'Access denied',
|
||||
errorMessage: message || 'You do not have permission to access this area.',
|
||||
backLabel: 'Back to dashboard'
|
||||
};
|
||||
}
|
||||
|
||||
if (normalizedStatusCode === 404) {
|
||||
return {
|
||||
title: 'Not found',
|
||||
errorTitle: 'Oops! Page not found.',
|
||||
errorMessage: message || 'We could not find the page you were looking for. Meanwhile, you may return to the dashboard or try searching for what you need.',
|
||||
backLabel: 'Back to dashboard'
|
||||
};
|
||||
}
|
||||
|
||||
if (normalizedStatusCode >= 500) {
|
||||
return {
|
||||
title: 'Something went wrong',
|
||||
errorTitle: 'Something went wrong.',
|
||||
errorMessage: message || 'An unexpected error occurred. Please try again in a moment.',
|
||||
backLabel: 'Back to dashboard'
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
title: 'Error',
|
||||
errorTitle: 'Error',
|
||||
errorMessage: message || 'An unexpected error occurred.',
|
||||
backLabel: 'Back to dashboard'
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = function renderErrorPage(options, currentUser) {
|
||||
const errorOptions = options || {};
|
||||
const statusCode = Number(errorOptions.statusCode || 500);
|
||||
const copy = getErrorCopy(statusCode, String(errorOptions.message || '').trim());
|
||||
return renderView('error', {
|
||||
title: String(errorOptions.title || copy.title || 'Error').trim(),
|
||||
active: '',
|
||||
messageVariant: 'primary',
|
||||
currentUser: currentUser || null,
|
||||
statusCode: statusCode,
|
||||
errorTitle: String(errorOptions.errorTitle || copy.errorTitle || errorOptions.title || 'Error').trim(),
|
||||
errorMessage: String(errorOptions.message || copy.errorMessage || 'An unexpected error occurred.').trim(),
|
||||
detail: String(errorOptions.detail || '').trim(),
|
||||
backUrl: String(errorOptions.backUrl || '/admin').trim() || '/admin',
|
||||
backLabel: String(errorOptions.backLabel || copy.backLabel || 'Back to dashboard').trim() || 'Back to dashboard',
|
||||
searchUrl: String(errorOptions.searchUrl || '').trim(),
|
||||
bodyClass: 'error-page bg-dark text-white',
|
||||
errorShell: true,
|
||||
stylesheets: [],
|
||||
scripts: []
|
||||
});
|
||||
};
|
||||
@@ -20,5 +20,9 @@ module.exports = {
|
||||
renderTemplateEditPage: require('./templates/edit'),
|
||||
renderCanvasSizesPage: require('./canvas-sizes/list'),
|
||||
renderCanvasSizeFormPage: require('./canvas-sizes/add'),
|
||||
renderCanvasSizeEditPage: require('./canvas-sizes/edit')
|
||||
renderCanvasSizeEditPage: require('./canvas-sizes/edit'),
|
||||
renderErrorPage: require('./error'),
|
||||
renderRbacPage: require('./rbac/list'),
|
||||
renderRbacAddPage: require('./rbac/add'),
|
||||
renderRbacEditPage: require('./rbac/edit')
|
||||
};
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
const { renderView } = require('../../view');
|
||||
|
||||
module.exports = function renderRbacAddPage(message, currentUser, formValues, permissionGroups, messageVariant) {
|
||||
return renderView('rbac/add', {
|
||||
title: 'Create role',
|
||||
active: 'rbac',
|
||||
message: message,
|
||||
messageVariant: messageVariant || 'primary',
|
||||
currentUser: currentUser || null,
|
||||
formValues: formValues || {},
|
||||
permissionGroups: permissionGroups || [],
|
||||
scripts: ['js/rbac-permissions.js']
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,14 @@
|
||||
const { renderView } = require('../../view');
|
||||
|
||||
module.exports = function renderRbacEditPage(role, message, currentUser, permissionGroups, users) {
|
||||
return renderView('rbac/edit', {
|
||||
title: 'Edit role',
|
||||
active: 'rbac',
|
||||
message: message,
|
||||
currentUser: currentUser || null,
|
||||
role: role,
|
||||
permissionGroups: permissionGroups || [],
|
||||
users: users || [],
|
||||
scripts: ['js/rbac-permissions.js']
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,11 @@
|
||||
const { renderView } = require('../../view');
|
||||
|
||||
module.exports = function renderRbacPage(data, message, currentUser) {
|
||||
return renderView('rbac/list', {
|
||||
title: 'Roles and permissions',
|
||||
active: 'rbac',
|
||||
message: message,
|
||||
currentUser: currentUser || null,
|
||||
roles: data.roles || []
|
||||
});
|
||||
};
|
||||
@@ -1,10 +1,13 @@
|
||||
const { renderView } = require('../../view');
|
||||
|
||||
module.exports = function renderUsersAddPage(message, currentUser) {
|
||||
module.exports = function renderUsersAddPage(message, currentUser, roles, formValues, messageVariant) {
|
||||
return renderView('users/add', {
|
||||
title: 'Add user',
|
||||
active: 'users',
|
||||
message: message,
|
||||
currentUser: currentUser || null
|
||||
messageVariant: messageVariant || 'primary',
|
||||
currentUser: currentUser || null,
|
||||
roles: roles || [],
|
||||
formValues: formValues || {}
|
||||
});
|
||||
};
|
||||
@@ -1,11 +1,12 @@
|
||||
const { renderView } = require('../../view');
|
||||
|
||||
module.exports = function renderUsersEditPage(user, message, currentUser) {
|
||||
module.exports = function renderUsersEditPage(user, message, currentUser, roles) {
|
||||
return renderView('users/edit', {
|
||||
title: 'Edit user',
|
||||
active: 'users',
|
||||
message: message,
|
||||
currentUser: currentUser || null,
|
||||
user: user
|
||||
user: user,
|
||||
roles: roles || []
|
||||
});
|
||||
};
|
||||
Reference in New Issue
Block a user