Files
MixedAssets/server/services/roles.js
2026-06-05 04:15:24 -05:00

103 lines
3.8 KiB
JavaScript

const { all, audit, json, now, one, parseJson, run } = require('../db');
const { CAPABILITY_KEYS, WILDCARD, capabilitySetIncludes, sanitizeCapabilities } = require('./accessControl');
function slugify(value) {
return String(value || '')
.trim()
.toLowerCase()
.replace(/[^a-z0-9]+/g, '_')
.replace(/^_+|_+$/g, '')
.slice(0, 40);
}
function fromRow(row) {
if (!row) return null;
return {
key: row.key,
name: row.name,
description: row.description || '',
capabilities: parseJson(row.capabilities_json, []),
is_system: Boolean(row.is_system),
locked: Boolean(row.locked),
user_count: one('SELECT COUNT(*) AS c FROM users WHERE role = ?', [row.key]).c
};
}
function listRoles() {
return all('SELECT * FROM roles ORDER BY sort_order, name').map(fromRow);
}
function getRole(key) {
return fromRow(one('SELECT * FROM roles WHERE key = ?', [key]));
}
function roleExists(key) {
return Boolean(one('SELECT key FROM roles WHERE key = ?', [key]));
}
// Capabilities granted to a role key (empty when the role is unknown).
function capabilitiesForRole(key) {
const row = one('SELECT capabilities_json FROM roles WHERE key = ?', [key]);
return row ? parseJson(row.capabilities_json, []) : [];
}
function roleHasCapability(key, capability) {
return capabilitySetIncludes(capabilitiesForRole(key), capability);
}
function createRole(body, userId) {
const name = String(body.name || '').trim();
if (!name) throw Object.assign(new Error('A role name is required'), { status: 400 });
const key = slugify(body.key || name);
if (!key) throw Object.assign(new Error('A valid role key is required'), { status: 400 });
if (roleExists(key)) throw Object.assign(new Error('A role with that key already exists'), { status: 400 });
const capabilities = sanitizeCapabilities(body.capabilities);
const ts = now();
const sort = (one('SELECT MAX(sort_order) AS m FROM roles').m || 0) + 1;
run(
'INSERT INTO roles (key, name, description, capabilities_json, is_system, locked, sort_order, created_at, updated_at) VALUES (?, ?, ?, ?, 0, 0, ?, ?, ?)',
[key, name, body.description || null, json(capabilities), sort, ts, ts]
);
audit(userId, 'create', 'role', key, null, { name, capabilities });
return getRole(key);
}
function updateRole(key, body, userId) {
const before = one('SELECT * FROM roles WHERE key = ?', [key]);
if (!before) return null;
const name = body.name !== undefined ? String(body.name).trim() || before.name : before.name;
// Locked roles (admin) always keep the wildcard capability set.
const capabilities = before.locked
? parseJson(before.capabilities_json, [WILDCARD])
: (body.capabilities !== undefined ? sanitizeCapabilities(body.capabilities) : parseJson(before.capabilities_json, []));
run(
'UPDATE roles SET name = ?, description = ?, capabilities_json = ?, updated_at = ? WHERE key = ?',
[name, body.description ?? before.description, json(capabilities), now(), key]
);
audit(userId, 'update', 'role', key, fromRow(before), { name, capabilities });
return getRole(key);
}
function deleteRole(key, userId) {
const before = one('SELECT * FROM roles WHERE key = ?', [key]);
if (!before) return { ok: false, status: 404, error: 'Role not found' };
if (before.is_system) return { ok: false, status: 400, error: 'Built-in roles cannot be deleted' };
const inUse = one('SELECT COUNT(*) AS c FROM users WHERE role = ?', [key]).c;
if (inUse) return { ok: false, status: 400, error: `Reassign the ${inUse} user(s) with this role before deleting it` };
run('DELETE FROM roles WHERE key = ?', [key]);
audit(userId, 'delete', 'role', key, fromRow(before), null);
return { ok: true };
}
module.exports = {
CAPABILITY_KEYS,
capabilitiesForRole,
createRole,
deleteRole,
getRole,
listRoles,
roleExists,
roleHasCapability,
updateRole
};