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 };