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:
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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' });
|
||||
|
||||
Reference in New Issue
Block a user