security(routes): закрыт долг по незащищённым :id-маршрутам (baseline 66→0)

check-route-auth теперь распознаёт router-level guards (router.use(<guard>)) —
ушли ложные срабатывания (admin/permissions/flashcards/lessons/… защищены на
уровне роутера, что линтер уже принимает как authMiddleware). Из 66 осталось
8 действительно безавторизационных :id-маршрутов — все публичные по дизайну
(гостевая доска по секретному токену, справочные данные Red Book, список тем
предмета): помечены @public-by-design после проверки (мутации требуют auth).
Baseline опущен до 0 — новые незащищённые маршруты теперь сразу падают в хуке.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Maxim Dolgolyov
2026-06-11 23:00:19 +03:00
parent 9cfb7d1c3b
commit 900fdb893d
4 changed files with 28 additions and 5 deletions
+5
View File
@@ -40,6 +40,7 @@ function notifySession(sessionId, data) {
}
/* ── GET /api/classroom/guest/:token ─── session info (pre-join screen) */
// @public-by-design: гостевой доступ по секретному токену (read-only доска)
router.get('/:token', (req, res) => {
const session = sessionByToken(req.params.token);
if (!session) return res.status(404).json({ error: 'Ссылка недействительна' });
@@ -58,6 +59,7 @@ router.get('/:token', (req, res) => {
});
/* ── POST /api/classroom/guest/:token/join ─── choose name, get guestId */
// @public-by-design: гостевой доступ по секретному токену
router.post('/:token/join', (req, res) => {
const session = sessionByToken(req.params.token);
if (!session) return res.status(404).json({ error: 'Ссылка недействительна' });
@@ -92,6 +94,7 @@ router.post('/:token/join', (req, res) => {
});
/* ── GET /api/classroom/guest/:token/strokes ─── whiteboard strokes */
// @public-by-design: гостевой доступ по секретному токену
router.get('/:token/strokes', (req, res) => {
const session = sessionByToken(req.params.token);
if (!session) return res.status(404).json({ error: 'Ссылка недействительна' });
@@ -118,6 +121,7 @@ router.get('/:token/strokes', (req, res) => {
});
/* ── GET /api/classroom/guest/:token/stream ─── SSE */
// @public-by-design: гостевой доступ по секретному токену (SSE)
router.get('/:token/stream', (req, res) => {
const session = sessionByToken(req.params.token);
if (!session) return res.status(404).end();
@@ -150,6 +154,7 @@ router.get('/:token/stream', (req, res) => {
});
/* ── POST /api/classroom/guest/:token/leave ─── explicit goodbye */
// @public-by-design: гостевой доступ по секретному токену
router.post('/:token/leave', (req, res) => {
const guestId = req.body?.guestId;
const guest = guestId ? guests.get(guestId) : null;
+2
View File
@@ -11,7 +11,9 @@ router.get('/map-data', ctrl.getMapData);
router.get('/food-web', ctrl.getFoodWeb);
router.get('/daily', optionalAuth, ctrl.getDaily);
router.get('/species', optionalAuth, ctrl.getSpecies);
// @public-by-design: публичные справочные данные о виде (read-only)
router.get('/species/:id', optionalAuth, ctrl.getSpeciesById);
// @public-by-design: публичные справочные данные о биоме (read-only)
router.get('/biome/:habitatId', ctrl.getBiomeSpecies);
// Auth required
+1
View File
@@ -30,6 +30,7 @@ router.patch('/:slug', authMiddleware, requireRole('admin'), (req, res) => {
res.json(db.prepare('SELECT * FROM subjects WHERE slug = ?').get(req.params.slug));
});
// @public-by-design: публичный список тем предмета (read-only каталог)
router.get('/:slug/topics', (req, res) => {
const subject = db.prepare('SELECT id FROM subjects WHERE slug = ?').get(req.params.slug);
if (!subject) return res.status(404).json({ error: 'Subject not found' });