'use strict'; /** * Feature-flag middleware + helpers. * * requireFeature(name) * Express middleware that 404s when the GLOBAL feature flag is off. * Doesn't see per-class overrides or free_student restrictions — * those are layered on in computeFeaturesForUser below. * * computeFeaturesForUser(userId, role) * Synchronous merge of three sources, mirroring /api/features in * server.js (kept in one place so other endpoints — gamification * achievements, shop, etc. — can apply the same view of "is X * enabled for THIS user right now"): * 1) global app_settings.feature__enabled (default = ON) * 2) class overlay (classes.features JSON, only `false` wins) * 3) free_student role overlay (app_settings.free_student_features) * Returns { featureName: boolean } where boolean = is-enabled. * * isFeatureEnabledForUser(userId, role, featureName) * Thin wrapper around computeFeaturesForUser for single-feature * checks. Returns true if the feature is on (or unknown — opt-in * disable model). * * Usage: * app.use('/api/pet', requireFeature('pet'), petRoutes); * const feats = computeFeaturesForUser(userId, role); * 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_%'"); const _stmtClassFeats = db.prepare( 'SELECT c.features FROM classes c JOIN class_members cm ON cm.class_id = c.id WHERE cm.user_id = ?' ); const _stmtFreeStudentFeats = db.prepare("SELECT value FROM app_settings WHERE key = 'free_student_features'"); function requireFeature(name) { const settingKey = `feature_${name}_enabled`; return (req, res, next) => { const row = _stmtSingle.get(settingKey); if (row && row.value === '0' && !_isAdminReq(req)) { // админ проходит к API даже выключенного модуля return res.status(404).json({ error: 'Feature disabled' }); } next(); }; } function computeFeaturesForUser(userId, role) { // 1) Globals — missing = ON. const features = {}; for (const r of _stmtGlobalFeats.all()) { const name = r.key.replace('feature_', '').replace('_enabled', ''); features[name] = r.value === '1'; } // 2) Class overlay — only `false` wins (more restrictive). if (userId && (role === 'student' || role === 'free_student')) { for (const row of _stmtClassFeats.all(userId)) { if (!row.features) continue; try { const f = JSON.parse(row.features); for (const [key, val] of Object.entries(f)) { if (val === false) features[key] = false; } } catch { /* malformed JSON — ignore */ } } } // 3) free_student overlay. if (role === 'free_student') { const fsRow = _stmtFreeStudentFeats.get(); if (fsRow?.value) { try { const fsFeats = JSON.parse(fsRow.value); for (const [key, val] of Object.entries(fsFeats)) { if (val === false) features[key] = false; } } catch { /* malformed — ignore */ } } } return features; } function isFeatureEnabledForUser(userId, role, featureName) { if (!featureName) return true; const feats = computeFeaturesForUser(userId, role); // Missing key (no row in app_settings) → treat as ON (opt-in disable). return feats[featureName] !== false; } module.exports = { requireFeature, computeFeaturesForUser, isFeatureEnabledForUser };