From 225e252e3c69bf268cd5e68ec35b6440c3bd7d3f Mon Sep 17 00:00:00 2001 From: Maxim Dolgolyov Date: Sat, 13 Jun 2026 15:22:59 +0300 Subject: [PATCH] =?UTF-8?q?feat(sim-builder):=20=D1=82=D1=83=D0=BC=D0=B1?= =?UTF-8?q?=D0=BB=D0=B5=D1=80=20=C2=AB=D0=9A=D0=BE=D0=BD=D1=81=D1=82=D1=80?= =?UTF-8?q?=D1=83=D0=BA=D1=82=D0=BE=D1=80=20=D1=81=D0=B8=D0=BC=D1=83=D0=BB?= =?UTF-8?q?=D1=8F=D1=86=D0=B8=D0=B9=C2=BB=20=D0=B2=20=D0=B0=D0=B4=D0=BC?= =?UTF-8?q?=D0=B8=D0=BD=D0=BA=D0=B5=20(feature=5Fsim=5Fbuilder=5Fenabled)?= =?UTF-8?q?=20=E2=80=94=20=D0=B3=D0=B5=D0=B9=D1=82=20=D0=B0=D0=B2=D1=82?= =?UTF-8?q?=D0=BE=D1=80=D0=B8=D0=BD=D0=B3=D0=B0=20+=20=D1=81=D0=BA=D1=80?= =?UTF-8?q?=D1=8B=D1=82=D0=B8=D0=B5/=D1=80=D0=B5=D0=B4=D0=B8=D1=80=D0=B5?= =?UTF-8?q?=D0=BA=D1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/controllers/adminController.js | 2 +- backend/src/routes/customSims.js | 20 +++++++++++++------- frontend/js/admin/sections/games.js | 1 + frontend/sim-builder.html | 7 +++++++ js/api.js | 1 + 5 files changed, 23 insertions(+), 8 deletions(-) diff --git a/backend/src/controllers/adminController.js b/backend/src/controllers/adminController.js index 598b69c..c4db1f0 100644 --- a/backend/src/controllers/adminController.js +++ b/backend/src/controllers/adminController.js @@ -525,7 +525,7 @@ function getFeatures(_req, res) { function updateFeatures(req, res) { const allowed = ['crossword', 'hangman', 'pet', 'red_book', 'collection', 'flashcards', 'knowledge_map', 'board', 'biochem', 'live_quiz', 'classroom', - 'gamification', 'assistant']; + 'gamification', 'assistant', 'sim_builder']; const updates = req.body; const stmt = db.prepare("INSERT OR REPLACE INTO app_settings (key, value) VALUES (?, ?)"); const getOld = db.prepare("SELECT value FROM app_settings WHERE key = ?"); diff --git a/backend/src/routes/customSims.js b/backend/src/routes/customSims.js index db1d9e8..13e3a42 100644 --- a/backend/src/routes/customSims.js +++ b/backend/src/routes/customSims.js @@ -6,29 +6,35 @@ const express = require('express'); const router = express.Router(); const { authMiddleware, requireRole } = require('../middleware/auth'); +const { requireFeature } = require('../middleware/features'); const c = require('../controllers/customSimController'); router.use(authMiddleware); +// «Конструктор симуляций» можно отключить в админке (feature_sim_builder_enabled). +// Чтение/проигрывание уже сохранённых симуляций остаётся доступным; гейтим только +// авторинг — создание/правку/удаление/раздачу/клон/связи. +const gate = requireFeature('sim_builder'); + router.get('/', c.list); // @public-by-design: router-level authMiddleware (above) + ownership/published check in handler router.get('/:id', c.get); // @public-by-design: router-level authMiddleware (above) + ownership/published check in handler router.get('/:id/related', c.related); -router.post('/', requireRole('teacher', 'admin'), c.create); +router.post('/', gate, requireRole('teacher', 'admin'), c.create); // @public-by-design: router-level authMiddleware (above) + per-row ownership check in handler -router.put('/:id', requireRole('teacher', 'admin'), c.update); +router.put('/:id', gate, requireRole('teacher', 'admin'), c.update); // @public-by-design: router-level authMiddleware (above) + per-row ownership check in handler -router.delete('/:id', requireRole('teacher', 'admin'), c.remove); +router.delete('/:id', gate, requireRole('teacher', 'admin'), c.remove); // Фаза 6 — раздача классу / клон / курикулумные связи. Мутации — inline // requireRole(teacher,admin) + per-row ownership в хендлере. -router.post('/:id/share', requireRole('teacher', 'admin'), c.share); -router.post('/:id/clone', requireRole('teacher', 'admin'), c.clone); +router.post('/:id/share', gate, requireRole('teacher', 'admin'), c.share); +router.post('/:id/clone', gate, requireRole('teacher', 'admin'), c.clone); // @public-by-design: router-level authMiddleware (above) + per-row ownership check in handler -router.post('/:id/links', requireRole('teacher', 'admin'), c.addLink); +router.post('/:id/links', gate, requireRole('teacher', 'admin'), c.addLink); // @public-by-design: router-level authMiddleware (above) + per-row ownership check in handler -router.delete('/:id/links/:linkId', requireRole('teacher', 'admin'), c.removeLink); +router.delete('/:id/links/:linkId', gate, requireRole('teacher', 'admin'), c.removeLink); module.exports = router; diff --git a/frontend/js/admin/sections/games.js b/frontend/js/admin/sections/games.js index 96c2a10..1e25a16 100644 --- a/frontend/js/admin/sections/games.js +++ b/frontend/js/admin/sections/games.js @@ -17,6 +17,7 @@ { key: 'biochem', label: 'Биохимия', desc: 'Молекулярный редактор, задачи на построение молекул и реакции', icon: 'flask-conical' }, { key: 'live_quiz', label: 'Живая викторина', desc: 'Синхронная викторина в реальном времени для всего класса', icon: 'radio' }, { key: 'classroom', label: 'Онлайн-уроки (classroom)', desc: 'Синхронные онлайн-уроки с доской и видео', icon: 'video' }, + { key: 'sim_builder', label: 'Конструктор симуляций', desc: 'Создание учителем своих интерактивных симуляций (рабочее поле, формулы, физика, графики)', icon: 'pencil-ruler' }, ]; const FS_FEATURES = [ diff --git a/frontend/sim-builder.html b/frontend/sim-builder.html index 2811423..5f62851 100644 --- a/frontend/sim-builder.html +++ b/frontend/sim-builder.html @@ -189,6 +189,13 @@ var ip = LS.initPage() || {}; if (!(ip.isTeacher || ip.isAdmin)) { location.href = '/dashboard'; return; } + // Фича-гейт: «Конструктор симуляций» можно отключить в админке (feature_sim_builder_enabled). + if (LS.loadFeatures) { + LS.loadFeatures().then(function (feats) { + if (feats && feats.sim_builder === false) { LS.toast && LS.toast('Конструктор симуляций отключён', 'warn'); location.href = '/dashboard'; } + }).catch(function () {}); + } + if (!window.SimEngine || !window.SimExpr || !window.SimBuilder) { document.getElementById('sbu-preview').innerHTML = '
Движок симуляций не загрузился. Обновите страницу.
'; diff --git a/js/api.js b/js/api.js index 4f19df5..f71eaa2 100644 --- a/js/api.js +++ b/js/api.js @@ -860,6 +860,7 @@ async function hideDisabledFeatures() { biochem: ['/biochem', '/biochem-library', '/biochem-reactions'], live_quiz: ['/live-quiz'], classroom: ['/classroom'], + sim_builder: ['/sim-builder', '/sim-builder.html'], exam9: ['/exam9', '/exam9.html'], textbooks: ['/textbooks', '/textbooks.html', '/textbook'], };