feat(permissions): C-3 — пер-ролевые права кастомных ролей (резолвер + конфиг)

Миграция 056: снят CHECK с role_permissions.role (пересборка) → можно хранить
набор прав произвольной кастомной роли. isEnabled(uid,permRole,baseRole,key):
user override → role_permissions[customRole] → фолбэк role_permissions[base] →
дефолт реестра(base). requirePermission передаёт permRole=customRole||role.
getMyPermissions/getUserPermissions: roleMap = база + наложение кастомной роли.
Тест C-3: права кастомной роли перекрывают базу, фолбэк на базу. custom-roles 8/8,
permissions 17/17, backend без регрессий.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Maxim Dolgolyov
2026-06-03 15:11:56 +03:00
parent 7cdb2e2af2
commit 32c2c44b76
4 changed files with 67 additions and 19 deletions
@@ -60,11 +60,14 @@ function getMyPermissions(req, res) {
if (role === 'admin') return res.json({ role, permissions: [] }); // admins bypass all
seedDefaults();
const roleRows = db.prepare(
'SELECT permission, enabled FROM role_permissions WHERE role = ?'
).all(role);
// База роли + наложение кастомной роли (если назначена): role_permissions[base]
// перекрываются role_permissions[customRole].
const customRole = req.user.customRole || null;
const roleMap = {};
for (const r of roleRows) roleMap[r.permission] = r.enabled === 1;
for (const r of db.prepare('SELECT permission, enabled FROM role_permissions WHERE role = ?').all(role)) roleMap[r.permission] = r.enabled === 1;
if (customRole && customRole !== role) {
for (const r of db.prepare('SELECT permission, enabled FROM role_permissions WHERE role = ?').all(customRole)) roleMap[r.permission] = r.enabled === 1;
}
const userRows = db.prepare(
"SELECT permission, enabled FROM user_permissions WHERE user_id = ? AND (expires_at IS NULL OR expires_at > datetime('now'))"
@@ -85,16 +88,16 @@ function getMyPermissions(req, res) {
/* ── GET /api/permissions/users/:id ──────────────────────────────────── */
function getUserPermissions(req, res) {
const uid = Number(req.params.id);
const target = db.prepare('SELECT id, role FROM users WHERE id = ?').get(uid);
const target = db.prepare('SELECT id, role, custom_role FROM users WHERE id = ?').get(uid);
if (!target) return res.status(404).json({ error: 'User not found' });
seedDefaults();
// role-level values
const roleRows = db.prepare(
'SELECT permission, enabled FROM role_permissions WHERE role = ?'
).all(target.role);
// role-level values: база роли + наложение кастомной роли (если назначена)
const roleMap = {};
for (const r of roleRows) roleMap[r.permission] = r.enabled === 1;
for (const r of db.prepare('SELECT permission, enabled FROM role_permissions WHERE role = ?').all(target.role)) roleMap[r.permission] = r.enabled === 1;
if (target.custom_role && target.custom_role !== target.role) {
for (const r of db.prepare('SELECT permission, enabled FROM role_permissions WHERE role = ?').all(target.custom_role)) roleMap[r.permission] = r.enabled === 1;
}
// user-level overrides (просроченные временные не учитываем)
const userRows = db.prepare(