feat(access): Фаза 0 — целостность правил доступа + подтверждение массового закрытия
- contentAccess.purgeAccessFor(scope,id) — единая точка очистки content_access (нет FK). deleteClass и _deleteUserTx переведены на неё (убрано дублирование). - Админ-UI: confirm() перед «Закрыть у всех / Закрыть весь» (необратимая массовая операция больше не срабатывает мгновенно). - Новый тест content-access.test.js (9/9): allowlist, ученик>класс, наследование главой хаба, admin/teacher bypass, allowedRefs/filterTextbooks, purgeAccessFor, чистка правил при DELETE класса. Полный backend-набор: 203/206 (3 — baseline Auth). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
const db = require('../db/db');
|
||||
const { stripTags } = require('../utils/sanitize');
|
||||
const { audit } = require('../utils/audit');
|
||||
const { purgeAccessFor } = require('../services/contentAccess');
|
||||
|
||||
/* ── Prepared statements ──────────────────────────────────────────────── */
|
||||
const stmts = {
|
||||
@@ -480,8 +481,8 @@ const _deleteUserTx = db.transaction((uid) => {
|
||||
// The rest cascades via ON DELETE CASCADE, but explicitly clean large tables:
|
||||
db.prepare('DELETE FROM notifications WHERE user_id = ?').run(uid);
|
||||
db.prepare('DELETE FROM test_sessions WHERE user_id = ?').run(uid);
|
||||
// Персональные правила доступа к контенту (нет FK — чистим вручную):
|
||||
db.prepare("DELETE FROM content_access WHERE scope = 'student' AND target_id = ?").run(uid);
|
||||
// Персональные правила доступа к контенту (нет FK — единая чистка):
|
||||
purgeAccessFor('student', uid);
|
||||
db.prepare('DELETE FROM users WHERE id = ?').run(uid);
|
||||
});
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ const crypto = require('crypto');
|
||||
const { onClassJoined } = require('./gamificationController');
|
||||
const { pushNotif } = require('../utils/notifications');
|
||||
const { stripTags } = require('../utils/sanitize');
|
||||
const { purgeAccessFor } = require('../services/contentAccess');
|
||||
|
||||
function genCode() {
|
||||
return crypto.randomBytes(4).toString('hex').toUpperCase();
|
||||
@@ -324,8 +325,8 @@ function deleteClass(req, res) {
|
||||
if (req.user.role !== 'admin' && cls.teacher_id !== req.user.id)
|
||||
return res.status(403).json({ error: 'Forbidden' });
|
||||
stmts.deleteClass.run(req.params.id);
|
||||
// Правила доступа к контенту для этого класса (нет FK — чистим вручную):
|
||||
db.prepare("DELETE FROM content_access WHERE scope = 'class' AND target_id = ?").run(req.params.id);
|
||||
// Правила доступа к контенту для этого класса (нет FK — единая чистка):
|
||||
purgeAccessFor('class', req.params.id);
|
||||
res.json({ ok: true });
|
||||
}
|
||||
|
||||
|
||||
@@ -84,6 +84,19 @@ function filterTextbooks(user, rows) {
|
||||
return rows.filter(r => allow.has(r.slug));
|
||||
}
|
||||
|
||||
/* ── Очистка правил (единая точка; у content_access нет FK) ───────────────
|
||||
* Вызывать из ВСЕХ путей удаления цели, чтобы не оставлять осиротевших правил:
|
||||
* purgeAccessFor('class', classId) — при удалении класса
|
||||
* purgeAccessFor('student', userId) — при удалении пользователя
|
||||
* Возвращает число удалённых строк. */
|
||||
const _purgeClass = db.prepare("DELETE FROM content_access WHERE scope = 'class' AND target_id = ?");
|
||||
const _purgeStudent = db.prepare("DELETE FROM content_access WHERE scope = 'student' AND target_id = ?");
|
||||
function purgeAccessFor(scope, id) {
|
||||
if (scope === 'class') return _purgeClass.run(id).changes;
|
||||
if (scope === 'student') return _purgeStudent.run(id).changes;
|
||||
return 0;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
PRIVILEGED,
|
||||
textbookAccessKey,
|
||||
@@ -92,4 +105,5 @@ module.exports = {
|
||||
canAccessExam,
|
||||
allowedRefs,
|
||||
filterTextbooks,
|
||||
purgeAccessFor,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user