feat(permissions): B7 — пресеты-профили прав (применение к классу одним кликом)
PRESETS (student): «Полный доступ», «Режим фокуса» (без магазина/испытаний), «Ограниченный» (+ без лаборатории), «Сбросить к стандарту роли». GET /api/permissions/presets + POST /api/permissions/class/:id/preset (admin). Рефактор: общий applyPermsToClass() (карта key→1/0/inherit) — его используют и bulk, и preset. В блоке «Массово по классу» — кнопки пресетов (с подтверждением). Тест: список + применение focus/reset + валидация. Backend pass (3 baseline-Auth). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -157,34 +157,69 @@ function resetUserPermissions(req, res) {
|
||||
res.json({ ok: true });
|
||||
}
|
||||
|
||||
/* ── POST /api/permissions/class/:id/bulk { permission, enabled } ──────────
|
||||
Выставить личное правило (user_permissions) сразу всем ученикам класса.
|
||||
enabled: 1 (вкл) | 0 (выкл) | null/'inherit' (снять оверрайд → наследование роли). */
|
||||
function setClassPermission(req, res) {
|
||||
const cid = Number(req.params.id);
|
||||
const { permission } = req.body || {};
|
||||
let { enabled } = req.body || {};
|
||||
if (!Number.isInteger(cid) || cid <= 0) return res.status(400).json({ error: 'неверный id класса' });
|
||||
if (!ALL_PERMISSIONS.find(p => p.key === permission && p.role === 'student'))
|
||||
return res.status(400).json({ error: 'Unknown student permission' });
|
||||
/* ── Пресеты-профили (студенческие) — применяются к классу одним кликом ──── */
|
||||
const PRESETS = {
|
||||
student: [
|
||||
{ id: 'full', label: 'Полный доступ', desc: 'Все возможности ученика включены',
|
||||
perms: { 'tests.free': 1, 'board.post': 1, 'profile.edit': 1, 'shop.purchase': 1, 'gamification.challenges': 1, 'theory.access': 1, 'simulations.access': 1, 'simulations.quiz': 1 } },
|
||||
{ id: 'focus', label: 'Режим фокуса', desc: 'Без магазина и испытаний — меньше отвлечений',
|
||||
perms: { 'shop.purchase': 0, 'gamification.challenges': 0 } },
|
||||
{ id: 'restricted', label: 'Ограниченный', desc: 'Без магазина, испытаний и лаборатории',
|
||||
perms: { 'shop.purchase': 0, 'gamification.challenges': 0, 'simulations.access': 0 } },
|
||||
{ id: 'reset', label: 'Сбросить к стандарту роли', desc: 'Снять все личные правила (наследование роли)',
|
||||
perms: { 'tests.free': 'inherit', 'board.post': 'inherit', 'profile.edit': 'inherit', 'shop.purchase': 'inherit', 'gamification.challenges': 'inherit', 'theory.access': 'inherit', 'simulations.access': 'inherit', 'simulations.quiz': 'inherit' } },
|
||||
],
|
||||
};
|
||||
|
||||
/* Применить карту прав { key: 1|0|'inherit' } всем ученикам класса. → число затронутых. */
|
||||
function applyPermsToClass(cid, permsMap) {
|
||||
const members = db.prepare(`
|
||||
SELECT u.id FROM class_members cm JOIN users u ON u.id = cm.user_id
|
||||
WHERE cm.class_id = ? AND u.role IN ('student','free_student')`).all(cid);
|
||||
const inherit = (enabled === null || enabled === undefined || enabled === 'inherit');
|
||||
const val = (enabled === 1 || enabled === true || enabled === '1') ? 1 : 0;
|
||||
|
||||
const del = db.prepare('DELETE FROM user_permissions WHERE user_id = ? AND permission = ?');
|
||||
const up = db.prepare('INSERT OR REPLACE INTO user_permissions (user_id, permission, enabled) VALUES (?, ?, ?)');
|
||||
const bump = db.prepare('UPDATE users SET token_version = token_version + 1 WHERE id = ?');
|
||||
db.transaction(() => {
|
||||
for (const m of members) {
|
||||
if (inherit) del.run(m.id, permission); else up.run(m.id, permission, val);
|
||||
for (const [key, v] of Object.entries(permsMap)) {
|
||||
if (v === 'inherit' || v === null) del.run(m.id, key);
|
||||
else up.run(m.id, key, (v === 1 || v === true || v === '1') ? 1 : 0);
|
||||
}
|
||||
bump.run(m.id); // user-level: точечно обновляем сессию каждого затронутого ученика
|
||||
}
|
||||
})();
|
||||
audit(req, 'permission.class_bulk', `class:${cid}/${permission}`, inherit ? 'inherit' : `enabled=${val}`);
|
||||
res.json({ ok: true, affected: members.length });
|
||||
return members.length;
|
||||
}
|
||||
|
||||
/* ── POST /api/permissions/class/:id/bulk { permission, enabled } ──────────
|
||||
Выставить ОДНО личное правило сразу всем ученикам класса. */
|
||||
function setClassPermission(req, res) {
|
||||
const cid = Number(req.params.id);
|
||||
const { permission } = req.body || {};
|
||||
const { enabled } = req.body || {};
|
||||
if (!Number.isInteger(cid) || cid <= 0) return res.status(400).json({ error: 'неверный id класса' });
|
||||
if (!ALL_PERMISSIONS.find(p => p.key === permission && p.role === 'student'))
|
||||
return res.status(400).json({ error: 'Unknown student permission' });
|
||||
const inherit = (enabled === null || enabled === undefined || enabled === 'inherit');
|
||||
const affected = applyPermsToClass(cid, { [permission]: inherit ? 'inherit' : ((enabled === 1 || enabled === true || enabled === '1') ? 1 : 0) });
|
||||
audit(req, 'permission.class_bulk', `class:${cid}/${permission}`, inherit ? 'inherit' : `enabled=${enabled ? 1 : 0}`);
|
||||
res.json({ ok: true, affected });
|
||||
}
|
||||
|
||||
/* ── GET /api/permissions/presets → { student:[{id,label,desc,perms}] } ──── */
|
||||
function getPresets(_req, res) { res.json(PRESETS); }
|
||||
|
||||
/* ── POST /api/permissions/class/:id/preset { preset } ─────────────────────
|
||||
Применить пресет-профиль ко всем ученикам класса. */
|
||||
function applyClassPreset(req, res) {
|
||||
const cid = Number(req.params.id);
|
||||
const { preset } = req.body || {};
|
||||
if (!Number.isInteger(cid) || cid <= 0) return res.status(400).json({ error: 'неверный id класса' });
|
||||
const p = PRESETS.student.find(x => x.id === preset);
|
||||
if (!p) return res.status(400).json({ error: 'Unknown preset' });
|
||||
const affected = applyPermsToClass(cid, p.perms);
|
||||
audit(req, 'permission.class_preset', `class:${cid}`, p.id);
|
||||
res.json({ ok: true, affected, preset: p.id });
|
||||
}
|
||||
|
||||
/* ── GET /api/permissions/log?user_id= — история изменений прав (admin) ── */
|
||||
@@ -225,4 +260,4 @@ function getPermissionLog(req, res) {
|
||||
res.json(out);
|
||||
}
|
||||
|
||||
module.exports = { getPermissions, setPermission, seedDefaults, ALL_PERMISSIONS, getMyPermissions, getUserPermissions, setUserPermission, resetUserPermissions, getPermissionLog, setClassPermission };
|
||||
module.exports = { getPermissions, setPermission, seedDefaults, ALL_PERMISSIONS, getMyPermissions, getUserPermissions, setUserPermission, resetUserPermissions, getPermissionLog, setClassPermission, getPresets, applyClassPreset };
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const router = require('express').Router();
|
||||
const { authMiddleware, requireRole } = require('../middleware/auth');
|
||||
const { getPermissions, setPermission, getMyPermissions, getUserPermissions, setUserPermission, resetUserPermissions, getPermissionLog, setClassPermission } = require('../controllers/permissionsController');
|
||||
const { getPermissions, setPermission, getMyPermissions, getUserPermissions, setUserPermission, resetUserPermissions, getPermissionLog, setClassPermission, getPresets, applyClassPreset } = require('../controllers/permissionsController');
|
||||
|
||||
router.use(authMiddleware);
|
||||
|
||||
@@ -13,8 +13,10 @@ router.get('/', getPermissions);
|
||||
router.get('/log', getPermissionLog);
|
||||
router.post('/', setPermission);
|
||||
|
||||
/* ── Массово по классу (личные оверрайды всем ученикам класса) ── */
|
||||
/* ── Пресеты-профили + массово по классу (личные оверрайды ученикам класса) ── */
|
||||
router.get('/presets', requireRole('admin'), getPresets);
|
||||
router.post('/class/:id/bulk', requireRole('admin'), setClassPermission);
|
||||
router.post('/class/:id/preset', requireRole('admin'), applyClassPreset);
|
||||
|
||||
/* ── Per-user overrides ── */
|
||||
router.get('/users/:id', getUserPermissions);
|
||||
|
||||
Reference in New Issue
Block a user