From 81bf5d75eb57f9f8d25ef05149eb1f326ebf8094 Mon Sep 17 00:00:00 2001 From: Maxim Dolgolyov Date: Wed, 24 Jun 2026 21:41:25 +0300 Subject: [PATCH] =?UTF-8?q?fix(features):=20=D0=B0=D0=B4=D0=BC=D0=B8=D0=BD?= =?UTF-8?q?-=D0=BE=D0=B2=D0=B5=D1=80=D1=80=D0=B0=D0=B9=D0=B4=20=D0=B2=20re?= =?UTF-8?q?quireFeature=20=E2=80=94=20API=20=D0=BE=D1=82=D0=BA=D0=BB=D1=8E?= =?UTF-8?q?=D1=87=D1=91=D0=BD=D0=BD=D0=BE=D0=B3=D0=BE=20=D0=BC=D0=BE=D0=B4?= =?UTF-8?q?=D1=83=D0=BB=D1=8F=20404-=D0=B8=D0=BB=20=D0=B0=D0=B4=D0=BC?= =?UTF-8?q?=D0=B8=D0=BD=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Симптом: collection выключен, админ открывал страницу (фронтовый админ-оверрайд), но GET /api/collection отдавал 404 — requireFeature 404-ил всех. requireFeature идёт ДО authMiddleware (req.user нет), поэтому сам декодирую Bearer-токен: если роль admin — пропускаем к API даже выключенного модуля. Для student/teacher всё по-прежнему 404 (модуль скрыт). Зеркалит фронтовый _isAdminUser. Чинит ВСЕ отключённые модули для админа, не только коллекцию. Проверено: admin→bypass, student/teacher/нет токена/мусор/чужой секрет→404 (6/6). Co-Authored-By: Claude Opus 4.8 (1M context) --- backend/src/middleware/features.js | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/backend/src/middleware/features.js b/backend/src/middleware/features.js index 24828dd..070d506 100644 --- a/backend/src/middleware/features.js +++ b/backend/src/middleware/features.js @@ -29,6 +29,19 @@ * if (feats.exam9 === false) { ... } */ const db = require('../db/db'); +const jwt = require('jsonwebtoken'); + +// Админ-оверрайд: requireFeature идёт ДО authMiddleware (req.user ещё нет), +// поэтому декодируем Bearer-токен сами — админ открывает и отключённые модули +// (зеркалит фронтовый _isAdminUser, см. project_gamification_killswitch). +function _isAdminReq(req) { + try { + const h = req.headers.authorization || ''; + if (!h.startsWith('Bearer ')) return false; + const p = jwt.verify(h.slice(7), process.env.JWT_SECRET, { algorithms: ['HS256'] }); + return !!(p && p.role === 'admin'); + } catch (e) { return false; } +} const _stmtSingle = db.prepare("SELECT value FROM app_settings WHERE key = ?"); const _stmtGlobalFeats = db.prepare("SELECT key, value FROM app_settings WHERE key LIKE 'feature_%'"); @@ -41,7 +54,7 @@ function requireFeature(name) { const settingKey = `feature_${name}_enabled`; return (req, res, next) => { const row = _stmtSingle.get(settingKey); - if (row && row.value === '0') { + if (row && row.value === '0' && !_isAdminReq(req)) { // админ проходит к API даже выключенного модуля return res.status(404).json({ error: 'Feature disabled' }); } next();