From 107ca2220c9f5d15987b7e47ee459c7fea3655c9 Mon Sep 17 00:00:00 2001 From: Maxim Dolgolyov Date: Fri, 12 Jun 2026 22:15:54 +0300 Subject: [PATCH] =?UTF-8?q?feat(imggen):=20feature-gate=20=C2=ABimggen?= =?UTF-8?q?=C2=BB=20=D1=81=20=D0=BA=D0=BE=D0=BD=D1=82=D1=80=D0=BE=D0=BB?= =?UTF-8?q?=D0=B5=D0=BC=20=D0=BF=D0=BE=20=D0=BA=D0=BB=D0=B0=D1=81=D1=81?= =?UTF-8?q?=D0=B0=D0=BC/=D1=83=D1=87=D0=B5=D0=BD=D0=B8=D0=BA=D0=B0=D0=BC?= =?UTF-8?q?=20(=D0=A1=D0=BF=D1=80=D0=B8=D0=BD=D1=822)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - server: requireFeature('imggen') на /api/imggen (глобальный гейт). - imggenController: enforcement через isFeatureEnabledForUser в status()/generate() — учитывает глобальный флаг + оверлей класса + free_student (403 если выкл.). - admin «games/features» + free-student: тумблер «Генерация картинок (ИИ)». - classes.html: переключатель модуля imggen в настройках класса (per-class). Дефолт — ON (opt-in disable), как у остальных фич. Проверено на features-движке. Co-Authored-By: Claude Opus 4.8 --- backend/src/controllers/imggenController.js | 7 ++++++- backend/src/server.js | 2 +- frontend/classes.html | 8 ++++++-- frontend/js/admin/sections/games.js | 2 ++ 4 files changed, 15 insertions(+), 4 deletions(-) diff --git a/backend/src/controllers/imggenController.js b/backend/src/controllers/imggenController.js index 7bd4d84..4968161 100644 --- a/backend/src/controllers/imggenController.js +++ b/backend/src/controllers/imggenController.js @@ -6,6 +6,7 @@ const fs = require('fs'); const path = require('path'); const db = require('../db/db'); +const { isFeatureEnabledForUser } = require('../middleware/features'); const GEN_DIR = path.join(__dirname, '../../uploads/generated'); const _cooldown = new Map(); // userId → last ts (антиспам) @@ -29,8 +30,11 @@ function _limits() { }; } +// Фича включена для ЭТОГО пользователя (глобально + оверлей класса + free_student). +function _featOn(req) { return isFeatureEnabledForUser(req.user.id, req.user.role, 'imggen'); } + /* GET /api/imggen/status — для UI (показывать кнопки или нет) */ -function status(req, res) { res.json({ enabled: _enabled() }); } +function status(req, res) { res.json({ enabled: _enabled() && _featOn(req) }); } /* FLUX лучше понимает английский. Если в промпте есть кириллица — переводим * через тот же LLM-провайдер, что и ассистент (с failover). При сбое — исходный текст. */ @@ -119,6 +123,7 @@ function stats() { /* POST /api/imggen { prompt } → { url } */ async function generate(req, res) { + if (!_featOn(req)) return res.status(403).json({ error: 'Генерация картинок отключена для вашего класса' }); if (!_enabled()) return res.status(503).json({ error: _configured() ? 'Генерация картинок временно выключена' : 'Генерация изображений не настроена' }); const prompt = String((req.body && req.body.prompt) || '').trim().slice(0, 500); if (prompt.length < 3) return res.status(400).json({ error: 'Опиши, что нарисовать (хотя бы пару слов)' }); diff --git a/backend/src/server.js b/backend/src/server.js index ba3322a..c66adcd 100644 --- a/backend/src/server.js +++ b/backend/src/server.js @@ -160,7 +160,7 @@ app.use('/api/questions', questionRoutes); app.use('/api/classes', classRoutes); app.use('/api/assignments', assignmentRoutes); app.use('/api/files', fileRoutes); -app.use('/api/imggen', require('./routes/imggen')); +app.use('/api/imggen', requireFeature('imggen'), require('./routes/imggen')); app.use('/api/tests', testRoutes); app.use('/api/notifications', notificationRoutes); app.use('/api/permissions', permissionRoutes); diff --git a/frontend/classes.html b/frontend/classes.html index 62c4bc4..3673f5c 100644 --- a/frontend/classes.html +++ b/frontend/classes.html @@ -735,6 +735,10 @@ Питомец + @@ -2281,7 +2285,7 @@ document.getElementById('set-invite-code').textContent = currentClass.invite_code || '—'; // Populate module toggles (all enabled by default if no features set) const f = currentClass.features || {}; - const FEATS = ['gamification', 'collection', 'hangman', 'crossword', 'red_book', 'pet']; + const FEATS = ['gamification', 'collection', 'hangman', 'crossword', 'red_book', 'pet', 'imggen']; for (const key of FEATS) { const el = document.getElementById('feat-' + key); if (el) el.checked = f[key] !== false; // default enabled @@ -2290,7 +2294,7 @@ async function saveModules() { if (!currentClass) return; - const FEATS = ['gamification', 'collection', 'hangman', 'crossword', 'red_book', 'pet']; + const FEATS = ['gamification', 'collection', 'hangman', 'crossword', 'red_book', 'pet', 'imggen']; const features = {}; for (const key of FEATS) { const el = document.getElementById('feat-' + key); diff --git a/frontend/js/admin/sections/games.js b/frontend/js/admin/sections/games.js index 69cd05f..96c2a10 100644 --- a/frontend/js/admin/sections/games.js +++ b/frontend/js/admin/sections/games.js @@ -11,6 +11,7 @@ { key: 'red_book', label: 'Красная книга', desc: 'Интерактивная Красная книга РБ: виды, биомы, пищевые сети, квесты', icon: 'leaf' }, { key: 'collection', label: 'Коллекция', desc: 'Коллекция карточек и достижений — игровой прогресс ученика', icon: 'layers' }, { key: 'flashcards', label: 'Флеш-карточки', desc: 'Карточки для запоминания терминов и понятий методом интервальных повторений', icon: 'square-stack' }, + { key: 'imggen', label: 'Генерация картинок (ИИ)', desc: 'ИИ-генерация изображений в ассистенте, флэшкартах, уроках, питомце, аватаре, доске', icon: 'image' }, { key: 'knowledge_map', label: 'Карта знаний', desc: 'Визуальная карта тем и связей между биологическими понятиями', icon: 'share-2' }, { key: 'board', label: 'Доска', desc: 'Классная доска с объявлениями, постами и обсуждениями', icon: 'layout-dashboard'}, { key: 'biochem', label: 'Биохимия', desc: 'Молекулярный редактор, задачи на построение молекул и реакции', icon: 'flask-conical' }, @@ -28,6 +29,7 @@ { key: 'lab', label: 'Лаборатория', desc: 'Виртуальные симуляции и интерактивные опыты', icon: 'flask-conical' }, { key: 'knowledge_map',label: 'Карта знаний', desc: 'Визуальная карта тем и связей между понятиями', icon: 'map' }, { key: 'flashcards', label: 'Флеш-карточки', desc: 'Карточки для повторения терминов и понятий', icon: 'square-stack' }, + { key: 'imggen', label: 'Генерация картинок (ИИ)', desc: 'ИИ-генерация изображений в ассистенте, флэшкартах, уроках, питомце', icon: 'image' }, { key: 'board', label: 'Доска', desc: 'Классная доска с объявлениями и постами', icon: 'layout-dashboard' }, { key: 'biochem', label: 'Биохимия', desc: 'Молекулярный редактор, задачи на построение молекул и реакции', icon: 'flask-conical' }, { key: 'live_quiz', label: 'Живая викторина', desc: 'Синхронная викторина в реальном времени для всего класса', icon: 'radio' },