feat(access): вид «по классу», массовые действия, бейджи состояния + чистка orphan-правил
По итогам ревью системы прав: - админка: переключатель режимов «По контенту» / «По классу» - кнопки «Открыть всем классам» / «Закрыть у всех» (и зеркально по классу) - бейджи N/M (сколько классов открыто) в списке контента - эндпоинты /api/access/summary и /api/access/class/:id - вкладка «Доступ к учебникам» перенесена к «Права доступа» (группа Пользователи) - чистка content_access при удалении класса/ученика (нет FK) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -480,6 +480,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);
|
||||
db.prepare('DELETE FROM users WHERE id = ?').run(uid);
|
||||
});
|
||||
|
||||
|
||||
@@ -324,6 +324,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);
|
||||
res.json({ ok: true });
|
||||
}
|
||||
|
||||
|
||||
@@ -76,6 +76,36 @@ router.get('/targets', (req, res) => {
|
||||
res.json({ classes: classList, looseStudents });
|
||||
});
|
||||
|
||||
/* ── Сводка: сколько классов открыто по каждому контенту ───────────────── */
|
||||
/* GET /api/access/summary
|
||||
→ { totalClasses, textbooks:{[slug]:openCount}, exams:{[key]:openCount} } */
|
||||
router.get('/summary', (req, res) => {
|
||||
const admin = isAdmin(req);
|
||||
const totalClasses = admin
|
||||
? db.prepare('SELECT COUNT(*) n FROM classes').get().n
|
||||
: db.prepare('SELECT COUNT(*) n FROM classes WHERE teacher_id = ?').get(req.user.id).n;
|
||||
|
||||
const rows = admin
|
||||
? db.prepare(`
|
||||
SELECT content_type, content_ref, COUNT(DISTINCT target_id) n
|
||||
FROM content_access
|
||||
WHERE scope = 'class' AND allow = 1
|
||||
GROUP BY content_type, content_ref`).all()
|
||||
: db.prepare(`
|
||||
SELECT content_type, content_ref, COUNT(DISTINCT target_id) n
|
||||
FROM content_access
|
||||
WHERE scope = 'class' AND allow = 1
|
||||
AND target_id IN (SELECT id FROM classes WHERE teacher_id = ?)
|
||||
GROUP BY content_type, content_ref`).all(req.user.id);
|
||||
|
||||
const textbooks = {}, exams = {};
|
||||
for (const r of rows) {
|
||||
if (r.content_type === 'textbook') textbooks[r.content_ref] = r.n;
|
||||
else exams[r.content_ref] = r.n;
|
||||
}
|
||||
res.json({ totalClasses, textbooks, exams });
|
||||
});
|
||||
|
||||
/* ── Текущие правила для одного контента ───────────────────────────────── */
|
||||
/* GET /api/access/rules?content_type=&content_ref=
|
||||
→ { classRules:{[class_id]:allow}, studentRules:{[user_id]:allow} } */
|
||||
@@ -101,6 +131,23 @@ router.get('/rules', (req, res) => {
|
||||
function teacherOwnsClass(teacherId, classId) {
|
||||
return !!db.prepare('SELECT 1 FROM classes WHERE id = ? AND teacher_id = ?').get(classId, teacherId);
|
||||
}
|
||||
|
||||
/* ── Открытый классу контент (для вида «по классу») ────────────────────── */
|
||||
/* GET /api/access/class/:id → { textbooks:[slug], exams:[exam_key] } (allow=1) */
|
||||
router.get('/class/:id', requireRole('admin', 'teacher'), (req, res) => {
|
||||
const cid = Number(req.params.id);
|
||||
if (!Number.isInteger(cid) || cid <= 0) return res.status(400).json({ error: 'неверный id' });
|
||||
if (!isAdmin(req) && !teacherOwnsClass(req.user.id, cid)) {
|
||||
return res.status(403).json({ error: 'Нет прав на этот класс' });
|
||||
}
|
||||
const rows = db.prepare(`
|
||||
SELECT content_type, content_ref FROM content_access
|
||||
WHERE scope = 'class' AND target_id = ? AND allow = 1
|
||||
`).all(cid);
|
||||
const textbooks = [], exams = [];
|
||||
for (const r of rows) (r.content_type === 'textbook' ? textbooks : exams).push(r.content_ref);
|
||||
res.json({ textbooks, exams });
|
||||
});
|
||||
function teacherCanManageStudent(teacherId, studentId) {
|
||||
const inClass = db.prepare(`
|
||||
SELECT 1 FROM class_members cm JOIN classes c ON c.id = cm.class_id
|
||||
|
||||
Reference in New Issue
Block a user