Commit Graph

15 Commits

Author SHA1 Message Date
Maxim Dolgolyov d5fbd0168e feat(permissions): +10 прав ролей с энфорсом (Доступ · роли)
Реестр (registry.js) пополнен правами, которыми раньше нельзя было управлять:
• Учитель: classroom.host (онлайн-уроки), livequiz.host (живые викторины),
  simbuilder.use (конструктор симуляций), flashcards.manage (общие колоды).
• Ученик: homework.submit (сдача ДЗ), materials.save («Мои материалы»),
  assistant.use (ИИ-ассистент), games.play (учебные игры),
  flashcards.access / exam.access (доступ к разделам).
Все default=1 → текущее поведение сохранено; админ может выключить по роли/классу/юзеру.

Энфорс на роутах: учительские — requirePermission (роуты уже teacher-only);
ученические на ОБЩИХ роутах (assistant/materials/games/flashcards/exam-prep) —
новый requirePermissionForStudents(key) (учитель/админ проходят всегда, проверка
только ученику — иначе isEnabled=false сломал бы учителя). PERM_DEFAULTS строится
из реестра → фолбэк до сидирования = enabled, никто не блокируется. Группы UI —
существующие (новых ярлыков нет). seedDefaults авто-сидит новые ключи на чтении.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 17:31:00 +03:00
Maxim Dolgolyov 09c6c2b21d fix(reliability): multer-ошибки, process-хендлеры, анти-гонка питомца, flashcards (Спринт2)
- errorHandler: MulterError → 413 «слишком большой» / 400 (а не 500).
- server: process.on(unhandledRejection/uncaughtException) — глобальная страховка
  с логированием, процесс не падает от единичной асинхронной ошибки.
- pet: атомарный CAS на кулдаунах petAction/starCatch/feedPet
  (UPDATE ... WHERE last IS ?, начисление только при changes=1) — нет двойного
  начисления при параллельных запросах. Проверено на семантике node:sqlite.
- assistant.flashcardsFromText: await callLLMFailover в try/catch → 502 вместо
  необработанного отклонения промиса.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 22:08:02 +03:00
Maxim Dolgolyov 32c2c44b76 feat(permissions): C-3 — пер-ролевые права кастомных ролей (резолвер + конфиг)
Миграция 056: снят CHECK с role_permissions.role (пересборка) → можно хранить
набор прав произвольной кастомной роли. isEnabled(uid,permRole,baseRole,key):
user override → role_permissions[customRole] → фолбэк role_permissions[base] →
дефолт реестра(base). requirePermission передаёт permRole=customRole||role.
getMyPermissions/getUserPermissions: roleMap = база + наложение кастомной роли.
Тест C-3: права кастомной роли перекрывают базу, фолбэк на базу. custom-roles 8/8,
permissions 17/17, backend без регрессий.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 15:11:56 +03:00
Maxim Dolgolyov 7cdb2e2af2 feat(permissions): C-2 — присвоение кастомной роли пользователю (users.custom_role)
Миграция 055: ADD COLUMN users.custom_role (безопасно, без пересборки users).
Модель: users.role = функциональная база (встроенная, CHECK ок, драйвит ветки
контроллеров и резолв прав), users.custom_role = имя кастомной роли. updateRole
(PATCH /api/admin/users/:id/role) принимает кастомные роли → ставит base_roles[0]
как базу + custom_role=имя; встроенная → custom_role=NULL; неизвестная → 400.
authMiddleware/optionalAuth читают custom_role → req.user.customRole; requireRole
расширяет до effectiveRoles(customRole||role). Тесты custom-roles 7/7; backend без регрессий.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 15:03:41 +03:00
Maxim Dolgolyov 5aa2dd1a4b feat(permissions): C-1 — фундамент кастомных ролей (roles table + наследование гейтов)
Phase C, Stage C-1 (ветка feature/custom-roles): таблица roles (name, label,
base_roles JSON, is_builtin) + засев встроенных. auth.effectiveRoles(role) —
кастомная роль наследует base_roles (какие встроенные гейты проходит); встроенные
— быстрый путь без БД. requireRole() теперь проверяет пересечение allowed с
effectiveRoles → 111 существующих гейтов не задеты (встроенные ведут себя как
прежде). Дизайн: PHASE_C_DESIGN.md. Тест effectiveRoles 5/5; полный backend pass.

ВАЖНО (обнаружено): users.role в канон-схеме имеет CHECK (admin/teacher/student/
free_student), безопасно пересобрать users (FK от многих таблиц, миграции в txn)
нельзя → присвоение кастомной роли пользователю пойдёт через users.custom_role (C-2).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 14:57:10 +03:00
Maxim Dolgolyov a250d15f9a feat(permissions): B8 — временные права (expires_at) с авто-снятием
Миграция 053: user_permissions.expires_at (NULL = бессрочно). Резолвер isEnabled
+ /me + /users/:id игнорируют просроченные оверрайды (наследуют роль); seedDefaults
чистит просроченные строки. setUserPermission принимает days → выдаёт право на
срок (datetime('now','+N days')). API отдаёт expiresAt. Клиент: setUserPermission(...,days).
В модалке прав пользователя — бейдж «до ДАТА» + кнопка «врем.» (выдать на N дней).
Тест: срок хранится/отдаётся, просроченное игнорируется и вычищается. Backend pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 14:43:06 +03:00
Maxim Dolgolyov 9ac2a612e0 feat(permissions): A1 — зависимости между правами (requires) + план переработки
registry: поле requires (questions.delete→manage, templates.public→manage,
courses.interactive→manage, simulations.quiz→access), проброшено в byRole.
auth.requirePermission: вынесен isEnabled(); право = own AND все requires
(дочернее не работает без родителя). /me и /users/🆔 effective с учётом
requires + requires в ответе. UI permissions.js: каскад — дочернее с
невыполненной зависимостью неактивно (тумблер заблокирован + «Требует: …»).
Тест зависимости. План: plans/permissions-rework/PLAN.md. Backend 216 pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 14:10:20 +03:00
Maxim Dolgolyov fe122b7681 feat(admin): журнал событий безопасности (Tier 1-2) + аудит чувствительных действий (Tier 3)
- security_events (миграция 047) + utils/securityLog.js (defensive, lazy stmt)
- Tier 1: login.success/fail, register, password.change в authController
- Tier 2: 403 (роль/разрешение) в middleware/auth, rate_limited в rateLimit
- Tier 3: audit() на выдачу доступа (access), начисление/сброс XP (gam), модерацию аватаров
- API GET/DELETE /api/admin/security-log (фильтр по категории + поиск, прунинг по дням)
- Frontend: вкладка «Безопасность» в admin.html + loadSecurityLog, расширены ACTION_LABELS

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 15:28:21 +03:00
Maxim Dolgolyov 29301ff87d feat(flashcards): фаза 1 полировки — хоткеи, поиск, drag-reorder, честные интервалы
- study: хоткеи Space/стрелки=флип, 1-4/←→=оценка
- превью интервалов = точная копия серверного SM-2 (было враньё «<1 мин»)
- поиск/фильтр карточек внутри колоды
- drag-reorder карточек + endpoint PUT /decks/:id/reorder (requireOwnership)
- flashcard_decks добавлен в ALLOWED_TABLES requireOwnership
- эмодзи в empty-state → inline SVG .ic
- deleteCard: нативный confirm() → LS.confirm

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 09:53:03 +03:00
Maxim Dolgolyov 41ca41d69c feat(gamification): hide locked achievements of disabled modules
When a teacher / admin turns off a module (per-class, per-role, or
globally), the matching achievements no longer clutter the user's
'Достижения' tab — but only the ones the user hasn't earned yet.
Already-unlocked achievements stay visible forever. We never take a
reward away after the fact.

Backend:
  • migration 034 adds achievements.required_feature + backfills 42
    rows (9 exam9, 8 red_book, 6 lab, 5 classroom, 4 textbooks, 3 each
    of biochem/flashcards, 2 live_quiz, 2 pet). 32 core rows stay
    NULL = always visible.
  • middleware/features.js gains computeFeaturesForUser(userId, role)
    + isFeatureEnabledForUser — extracted from server.js#/api/features
    so multiple consumers (gam achievements, future shop filter, etc.)
    apply the same global+class+free_student merge.
  • service.seedAchievements derives required_feature from track/group
    when ACHIEVEMENT_DEFS doesn't spell one out, and UPDATE-syncs it on
    every boot — keeps catalogue consistent across upgrades.
  • _shared.getAllAchs SELECT now returns required_feature.
  • gamification/api.getAchievements filters: drop locked rows whose
    required_feature is === false for this user. Missing flag = ON
    (opt-in disable model).

Verified: with exam9 + pet disabled, 12 locked achievements vanish from
the response while unlocked ones in those tracks remain.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-29 20:40:16 +03:00
Maxim Dolgolyov 19c16bdfe8 feat(perm): block API endpoints for globally-disabled features (B-lite)
Adds backend/src/middleware/features.js with requireFeature(name)

that returns 404 when app_settings.feature_<name>_enabled='0'.

Wired on 8 routes:

- /api/pet            (pet)

- /api/collection     (collection)

- /api/red-book       (red_book)

- /api/flashcards     (flashcards)

- /api/knowledge-map  (knowledge_map)

- /api/biochem        (biochem)

- /api/games/hangman/*   (hangman, per-route inside games router)

- /api/games/crossword/* (crossword, per-route)

Scope: GLOBAL only. Per-class disable (classes.features JSON) and the

free_student role overlay remain UI-gated. Add user-aware merge later

if needed (extract logic from /api/features endpoint into shared helper).

Not gated (intentional, core teacher tools): board, classroom, live_quiz.

Smoke: pet disabled → 404; enabled → 401 (auth-required passthrough).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 14:35:29 +03:00
Maxim Dolgolyov 76883b569c feat(perm): central permission registry + key validation in linter
- backend/src/permissions/registry.js: single source of truth (PERMISSIONS map)
  with all 24 keys (16 teacher + 8 student, student keys also cover free_student).
  Exports isKnown(), listKeys(), byRole(), buildDefaultsMap().
- auth.js: PERM_DEFAULTS now sourced from registry.buildDefaultsMap();
  new perm() helper validates key at registration time (crashes early on typos).
  requirePermission() unchanged — backward compat preserved.
- permissionsController.js: ALL_PERMISSIONS now built from registry.byRole();
  inline 24-entry array removed. API response shape unchanged.
- check-route-auth.js: validates every requirePermission/perm call key against
  registry; lists unknown keys as errors before exit.
  perm() added to GUARDS list so it counts as route protection.

Discrepancy noted: auth.js had free_student with same 8 keys as student;
permissionsController never seeded free_student rows. Registry documents
this via roles:[] array; buildDefaultsMap() correctly covers free_student.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-17 14:22:18 +03:00
Maxim Dolgolyov 952a54f97c security+perf: полное ревью — 17 фиксов P0/P1 (XSS, IDOR, race conditions, rate limits, TURN, WAL)
## P0
- admin.html:2608, red-book-ecosystem.html:489-495 — XSS: u.name/node.name_ru/description обернуты в LS.esc()
- classController.js getAnnouncements — добавлена проверка teacher_id (B14: учитель A не может читать объявления класса B)

## P1 — auth & validation
- authController.js — минимум пароля 6→8 символов (register + change password + login.html)
- gamificationController adminAward — валидация max XP/coins (1M), Number coercion
- shopController adminAwardCoins — валидация max + проверка changes>0

## P1 — race conditions
- petController.buyBg — atomic UPDATE WHERE coins>=? (race-safe)
- shopController.purchaseItem — atomic conditional UPDATE
- liveController — добавлен question_id в live_answers (миграция с пересозданием таблицы), история ответов сохраняется при смене вопроса учителем
- ws-server: invalidateDrawCache экспортирован, classroomController grant/revoke вызывают его → permission revoke применяется мгновенно (раньше до 10s stale)

## P1 — rate limits & retry
- rateLimit middleware: новый параметр byUser=true (использует req.user.id вместо IP — не блокирует пользователей за NAT)
- routes/classroom.js: reactionLimiter (15/5s) на /chat/:msgId/react, handLimiter (5/5s) на raise/lower hand
- api.js sendAnswer — retry 3x с exp backoff (300/1200/2700ms), не повторяет на 4xx (F5)

## P1 — performance
- classroomController.getStrokes — LIMIT 5000 + флаг hasMore (защита от OOM на 10K+ strokes)
- whiteboard.js _liveStrokes — TTL 1.5s на каждый live preview (auto-cleanup при крашe ремоут юзера)

## Infrastructure
- config.js: TURN_URL/USER/PASS env vars
- server.js: GET /api/ice-servers возвращает STUN + опциональный TURN из env
- classroom-rtc.js: фетчит /api/ice-servers вместо хардкода (поддержка TURN для NAT/CGNAT школьных сетей)
- .env.example: документация TURN
- db.js: PRAGMA synchronous=NORMAL (5x быстрее с WAL), cache_size 16MB, temp_store=MEMORY
- ws-server.js closeAll() + server.js shutdown — graceful WS shutdown при SIGTERM

## False positives (не баги, агенты ошиблись)
- assignmentController FK на tests — на самом деле users (migrate.js:317-318)
- .env в git — gitignore корректно исключает
- admin.html без requireAuth — есть LS.initPage() который вызывает requireAuth
- submissionsController IDOR — обе ручки уже проверяют teacher_id
- screenSender = null inside try/catch — на самом деле снаружи
- SSE без backoff — есть exponential 2s→30s
- sessionController NOT IN на пустом массиве — есть guard usedIds.length>0
- getChat без LIMIT — есть LIMIT 100/200
- trust proxy — установлен на server.js:105

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-23 12:16:08 +03:00
Maxim Dolgolyov 3a4623a60a fix: полное ревью системы — 15 исправлений безопасности и надёжности
Безопасность:
- tests/🆔 скрыть is_correct и explanation для студентов (P0)
- SQL injection: limit/offset через placeholder вместо template literal
- Stored XSS: stripTags для lesson comments, flashcards, redBook sightings
- profile.html: escape e.message в showMsg (XSS через server error)
- attachment_url: валидация только /uploads/* путей
- requestId: генерировать UUID сервером, не доверять клиенту
- register: скрыть token_version из ответа

Надёжность:
- register: обработка UNIQUE constraint race condition
- pet buyBg: re-check баланса внутри транзакции
- DB errors: скрыть e.message в testController/questionController/courseController
- preferences: лимит 50KB на размер JSON

UX:
- board.html: debounce 250ms на search input

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 10:59:19 +03:00
Maxim Dolgolyov be4d43105e LearnSpace: full-stack educational whiteboard platform
Node.js/Express backend + vanilla JS frontend.
Features: real-time collaborative whiteboard (SSE), multi-page support,
LaTeX formulas, shapes/connectors, coordinate systems, number lines,
compass, zoom/pan, Catmull-Rom pencil smoothing, ruler/protractor with
rotation & resize controls, minimap navigation overlay, auto-measurements,
multi-page thumbnails sidebar, PNG export, page templates.
Student/teacher workflows: classes, assignments, library, dashboard.
Mobile responsive. SQLite (better-sqlite3).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-12 10:10:37 +03:00