feat(access): Фаза 1c — видимость курсов по классам (Фаза 1 завершена)

Миграция 052: мост «открыть все опубликованные курсы всем существующим классам»
(тип 'course' уже в CHECK из 051). courseController.list/search фильтруют курсы
для НЕпривилегированных по allowedRefs(uid,'course') (content_ref = courses.id как
TEXT); admin/teacher — все. /api/access/catalog отдаёт курсы; CONTENT_TYPES в
админ-UI = textbook,exam,sim,course → курсы управляются во всех режимах «Доступ».
Тест course-access 4/4 (allowlist+класс+privileged+каталог). Полный набор: 213 pass.

ВАЖНО: новый опубликованный курс по умолчанию закрыт (allowlist) — открыть классам
в админке. Мост сохранил видимость текущих опубликованных курсов у существующих
классов. class_courses остаётся для назначений с дедлайном (сверх видимости).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Maxim Dolgolyov
2026-06-03 13:31:10 +03:00
parent 2c7200fbad
commit 9b7585ac7b
6 changed files with 94 additions and 5 deletions
+13 -3
View File
@@ -1,6 +1,15 @@
const db = require('../db/db');
const access = require('../services/contentAccess');
/* ── helpers ──────────────────────────────────────────────────────────── */
/* Видимость курсов по классам (добавочная модель): ученик видит только
* разрешённые его классу/лично курсы; admin/teacher — все. Возвращает
* предикат для фильтрации строк курсов (по c.id). */
function courseVisible(user) {
if (access.PRIVILEGED.has(user.role)) return () => true;
const allowed = access.allowedRefs(user.id, 'course');
return (row) => allowed.has(String(row.id));
}
// Reused SQL fragment: user's completed-lesson count for a course (param: user_id)
const DONE_COUNT_SUBQ = `(SELECT COUNT(*) FROM lesson_progress lp
@@ -45,7 +54,7 @@ function list(req, res) {
ORDER BY c.subject_slug, c.order_index, c.id
`).all(uid, ...args);
res.json(rows.map(courseRow));
res.json(rows.filter(courseVisible(req.user)).map(courseRow));
}
/* ── GET /api/courses/search?q=… ─────────────────────────────────────── */
@@ -59,13 +68,14 @@ function search(req, res) {
const pubC = role === 'student' ? 'AND c.is_published = 1' : '';
const pubL = role === 'student' ? 'AND l.is_published = 1 AND c.is_published = 1' : '';
const vis = courseVisible(req.user);
const courses = db.prepare(`
SELECT c.*,
(SELECT COUNT(*) FROM lessons l WHERE l.course_id = c.id ${progressSubquery(role)}) AS lesson_count,
${DONE_COUNT_SUBQ}
FROM courses c WHERE (c.title LIKE ? OR c.description LIKE ?) ${pubC}
ORDER BY c.subject_slug, c.order_index LIMIT 20
`).all(uid, like, like).map(courseRow);
`).all(uid, like, like).filter(vis).map(courseRow);
const lessons = db.prepare(`
SELECT l.id, l.title, l.course_id, c.title AS course_title, c.subject_slug,
@@ -75,7 +85,7 @@ function search(req, res) {
LEFT JOIN lesson_progress lp ON lp.lesson_id = l.id AND lp.user_id = ?
WHERE l.title LIKE ? ${pubL}
ORDER BY c.subject_slug, l.order_index LIMIT 30
`).all(uid, like);
`).all(uid, like).filter(r => vis({ id: r.course_id }));
res.json({ courses, lessons });
}