Files
Learn_System/backend/src/controllers/permissionsController.js
T
Maxim Dolgolyov 7d474b40c0 feat(permissions): A3 — история изменений прав (endpoint + UI)
GET /api/permissions/log (admin-only) — последние изменения ролевых прав (или
?user_id= для личных оверрайдов) из admin_audit_log; читаемый текст («включил
«X» для роли «учитель»») с резолвом меток через registry. Клиент LS.permissionsLog.
Вкладка «Доступ · роли»: блок «История изменений прав ролей» с кнопкой «Показать».
Тест: admin видит записи, не-админу 403. permissions 13/13.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 14:14:56 +03:00

201 lines
8.8 KiB
JavaScript

const db = require('../db/db');
const { audit } = require('../utils/audit');
const registry = require('../permissions/registry');
/* ── All known permissions — sourced from central registry ────────────── */
// Only teacher and student entries are exposed to the admin UI.
// free_student shares the same keys as student (handled in auth.js fallback).
const ALL_PERMISSIONS = [
...registry.byRole('teacher'),
...registry.byRole('student'),
];
/* ── Seed defaults once per startup ───────────────────────────────────── */
function seedDefaults() {
const upsert = db.prepare(
'INSERT OR IGNORE INTO role_permissions (role, permission, enabled) VALUES (?, ?, ?)'
);
const run = db.transaction(() => {
for (const p of ALL_PERMISSIONS) upsert.run(p.role, p.key, p.default);
});
run();
}
/* ── GET /api/permissions ─────────────────────────────────────────────── */
function getPermissions(_req, res) {
seedDefaults();
const rows = db.prepare('SELECT role, permission, enabled FROM role_permissions').all();
const map = { teacher: {}, student: {} };
for (const r of rows) {
if (map[r.role]) map[r.role][r.permission] = r.enabled === 1;
}
res.json({ permissions: map, definitions: ALL_PERMISSIONS });
}
/* ── POST /api/permissions { role, permission, enabled } ─────────────── */
function setPermission(req, res) {
const { role, permission, enabled } = req.body;
if (!['teacher', 'student'].includes(role))
return res.status(400).json({ error: 'Invalid role' });
if (!ALL_PERMISSIONS.find(p => p.key === permission && p.role === role))
return res.status(400).json({ error: 'Unknown permission' });
db.transaction(() => {
db.prepare(
'INSERT OR REPLACE INTO role_permissions (role, permission, enabled) VALUES (?, ?, ?)'
).run(role, permission, enabled ? 1 : 0);
// Invalidate JWTs for all users of that role so the change takes effect immediately
db.prepare(
'UPDATE users SET token_version = token_version + 1 WHERE role = ?'
).run(role);
})();
audit(req, 'permission.set', `role:${role}/${permission}`, `enabled=${enabled ? 1 : 0}`);
res.json({ ok: true });
}
/* ── GET /api/permissions/me (any authenticated user) ───────────────── */
function getMyPermissions(req, res) {
const uid = req.user.id;
const role = req.user.role;
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);
const roleMap = {};
for (const r of roleRows) roleMap[r.permission] = r.enabled === 1;
const userRows = db.prepare(
'SELECT permission, enabled FROM user_permissions WHERE user_id = ?'
).all(uid);
const userMap = {};
for (const r of userRows) userMap[r.permission] = r.enabled === 1;
const defs = ALL_PERMISSIONS.filter(p => p.role === role);
const base = {};
defs.forEach(d => { base[d.key] = userMap[d.key] !== undefined ? userMap[d.key] : (roleMap[d.key] ?? !!d.default); });
const result = defs.map(d => ({
key: d.key,
effective: base[d.key] && (d.requires || []).every(r => !!base[r]),
}));
res.json({ role, permissions: result });
}
/* ── 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);
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);
const roleMap = {};
for (const r of roleRows) roleMap[r.permission] = r.enabled === 1;
// user-level overrides
const userRows = db.prepare(
'SELECT permission, enabled FROM user_permissions WHERE user_id = ?'
).all(uid);
const userMap = {};
for (const r of userRows) userMap[r.permission] = r.enabled === 1;
const defs = ALL_PERMISSIONS.filter(p => p.role === target.role);
const base = {};
defs.forEach(d => { base[d.key] = userMap[d.key] !== undefined ? userMap[d.key] : (roleMap[d.key] ?? !!d.default); });
const result = defs.map(d => ({
key: d.key,
label: d.label,
desc: d.desc,
requires: d.requires || [],
roleVal: roleMap[d.key] ?? d.default, // effective role-level value
userVal: userMap[d.key], // undefined = no override
effective: base[d.key] && (d.requires || []).every(r => !!base[r]),
}));
res.json({ role: target.role, permissions: result });
}
/* ── POST /api/permissions/users/:id { permission, enabled } ─────────── */
function setUserPermission(req, res) {
const uid = Number(req.params.id);
const { permission, enabled } = req.body;
const target = db.prepare('SELECT role FROM users WHERE id = ?').get(uid);
if (!target) return res.status(404).json({ error: 'User not found' });
if (!ALL_PERMISSIONS.find(p => p.key === permission && p.role === target.role))
return res.status(400).json({ error: 'Unknown permission for this role' });
db.transaction(() => {
db.prepare(
'INSERT OR REPLACE INTO user_permissions (user_id, permission, enabled) VALUES (?, ?, ?)'
).run(uid, permission, enabled ? 1 : 0);
// Invalidate existing JWT for this user immediately
db.prepare(
'UPDATE users SET token_version = token_version + 1 WHERE id = ?'
).run(uid);
})();
audit(req, 'permission.user_set', `user:${uid}/${permission}`, `enabled=${enabled ? 1 : 0}`);
res.json({ ok: true });
}
/* ── DELETE /api/permissions/users/:id/reset (single or all) ─────────── */
function resetUserPermissions(req, res) {
const uid = Number(req.params.id);
const { permission } = req.body; // optional: reset one key
db.transaction(() => {
if (permission) {
db.prepare(
'DELETE FROM user_permissions WHERE user_id = ? AND permission = ?'
).run(uid, permission);
} else {
db.prepare('DELETE FROM user_permissions WHERE user_id = ?').run(uid);
}
// Bump token_version so the user's JWT picks up the new effective permissions
// immediately (could be a downgrade if override was =1 and role default is =0).
db.prepare('UPDATE users SET token_version = token_version + 1 WHERE id = ?').run(uid);
})();
audit(req, 'permission.user_reset', `user:${uid}`, permission || null);
res.json({ ok: true });
}
/* ── GET /api/permissions/log?user_id= — история изменений прав (admin) ── */
function getPermissionLog(req, res) {
const uid = req.query.user_id ? Number(req.query.user_id) : null;
const rows = uid
? db.prepare(`
SELECT a.action, a.target, a.detail, a.created_at, u.name AS actor
FROM admin_audit_log a LEFT JOIN users u ON u.id = a.admin_id
WHERE a.action LIKE 'permission.user%' AND (a.target = ? OR a.target LIKE ?)
ORDER BY a.id DESC LIMIT 50`).all('user:' + uid, 'user:' + uid + '/%')
: db.prepare(`
SELECT a.action, a.target, a.detail, a.created_at, u.name AS actor
FROM admin_audit_log a LEFT JOIN users u ON u.id = a.admin_id
WHERE a.action = 'permission.set'
ORDER BY a.id DESC LIMIT 50`).all();
const labelOf = {};
for (const k of registry.listKeys()) labelOf[k] = registry.PERMISSIONS[k].label;
const roleName = (r) => (r === 'teacher' ? 'учитель' : r === 'student' ? 'ученик' : r);
const onoff = (d) => (/enabled=1/.test(d || '') ? 'включил' : /enabled=0/.test(d || '') ? 'выключил' : 'изменил');
const out = rows.map(r => {
let text;
if (r.action === 'permission.set') {
const m = /^role:([^/]+)\/(.+)$/.exec(r.target || '');
const key = m ? m[2] : '';
text = `${onoff(r.detail)} «${labelOf[key] || key}» для роли «${roleName(m ? m[1] : '')}»`;
} else if (r.action === 'permission.user_set') {
const m = /^user:\d+\/(.+)$/.exec(r.target || '');
const key = m ? m[1] : '';
text = `${onoff(r.detail)} личное «${labelOf[key] || key}»`;
} else { // permission.user_reset
text = r.detail ? `сбросил личное «${labelOf[r.detail] || r.detail}»` : 'сбросил все личные правила';
}
return { actor: r.actor || '—', text, at: r.created_at };
});
res.json(out);
}
module.exports = { getPermissions, setPermission, seedDefaults, ALL_PERMISSIONS, getMyPermissions, getUserPermissions, setUserPermission, resetUserPermissions, getPermissionLog };