feat(access): доступ к учебникам и экзаменам по классам/ученикам из админ-панели

Модель allowlist (закрыто по умолчанию), правило ученика важнее класса.
Управляют админ (все) и учителя (свои классы/ученики).

- миграция 040: таблица content_access + непрерывный переход
  (всем существующим классам открыт текущий контент)
- сервис contentAccess: резолвинг доступа, главы наследуют хаб
- API /api/access (catalog/targets/rules) для admin+teacher
- гейты: каталог учебников, router.param slug/examKey, фильтр tracks
- клиентские редиректы на /403 (textbook-tracker, exam-prep boot)
- раздел админки «Доступ к учебникам»: классы + ученики (tri-state)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Maxim Dolgolyov
2026-05-30 12:33:05 +03:00
parent 98f955a85e
commit 471171b77c
12 changed files with 564 additions and 4 deletions
+13 -2
View File
@@ -2,9 +2,19 @@
const router = require('express').Router();
const db = require('../db/db');
const { authMiddleware } = require('../middleware/auth');
const access = require('../services/contentAccess');
router.use(authMiddleware);
/* Гейт доступа: любой маршрут с :examKey проверяется по allowlist.
Админ/учитель проходят всегда; ученик — только при наличии правила. */
router.param('examKey', (req, res, next, examKey) => {
if (!access.canAccessExam(req.user, examKey)) {
return res.status(403).json({ error: 'Нет доступа к этому экзамен-модулю' });
}
next();
});
/* ── Statements (prepared once) ────────────────────────────────── */
const SQL = {
listTracks: db.prepare(`
@@ -399,8 +409,9 @@ const SQL = {
/* ── GET /api/exam-prep/tracks ──
Public list of enabled exam tracks (for a future landing page). */
router.get('/tracks', (_req, res) => {
const tracks = SQL.listTracks.all();
router.get('/tracks', (req, res) => {
const tracks = SQL.listTracks.all()
.filter(t => access.canAccessExam(req.user, t.exam_key));
res.json({ tracks });
});