# Phase 3: БД + API (custom_sims) **Status:** ⬜ Not Started **Parent plan:** [PLAN.md](./PLAN.md) **Domain:** backend ## Objective Сохранение custom-симуляций: таблица БД, CRUD API под авторизацией с проверкой владения, серверная валидация спеки. После фазы спека сохраняется/грузится/удаляется через API. ## Tasks - [ ] Миграция `backend/src/db/migrations/0NN_custom_sims.sql` (следующий свободный номер): таблица `custom_sims` (id, owner_id FK users ON DELETE CASCADE, title, description, subject, grade, cat, spec_json TEXT, status TEXT 'draft|published' DEFAULT 'draft', version INT, created_at, updated_at) + индекс по owner_id, status. - [ ] Контроллер `backend/src/controllers/customSimController.js`: list (own + published), get, create, update, remove. Владение проверяется на mutate (owner или admin). - [ ] Серверная валидация спеки `validateSpec(spec)`: размер JSON, `specVersion`, лимиты (число params/objects, глубина строк-выражений), типы объектов из whitelist, строки-подписи обрезаются/санитизируются. Отказ с 400 при нарушении. ⛔ Никакого исполнения спеки на сервере. - [ ] Роуты `backend/src/routes/customSims.js`: `GET /api/custom-sims` (свои+published), `GET /api/custom-sims/:id`, `POST /` (teacher/admin), `PUT /:id`, `DELETE /:id`. Смонтировать в server.js под authMiddleware + фича-гейт при необходимости. - [ ] Клиент `js/api.js`: `customSimsList/Get/Create/Update/Delete` + добавить в `window.LS`. - [ ] Тесты `backend/tests/custom-sims.test.js`: CRUD, ownership (403 чужой), валидация (400 кривая спека). Использовать `seedRow()` паттерн (устойчив к дрейфу схемы). ## Files to Modify/Create - `backend/src/db/migrations/0NN_custom_sims.sql` (new) - `backend/src/controllers/customSimController.js` (new) - `backend/src/routes/customSims.js` (new) - `backend/src/server.js` — монтирование роутера (modify) - `js/api.js` — клиентские методы (modify) - `backend/tests/custom-sims.test.js` (new) ## Acceptance Criteria - POST сохраняет спеку, GET возвращает свои+published, PUT/DELETE только владельцу/админу (403 иначе). - Кривая/огромная спека → 400. Тесты зелёные (в пределах baseline). - `npm run migrate` применяет таблицу; роут не отдаёт SPA-fallback после рестарта. ## Notes - node:sqlite (DatabaseSync), НЕ better-sqlite3. - Спека хранится как TEXT(JSON); парс/валидация — на входе. - Не делать blanket `router.use(requireRole('admin'))` — read-роуты auth-only, мутации — inline requireRole. ## Review Checklist - [ ] Все задачи выполнены - [ ] Ownership и валидация спеки покрыты тестами - [ ] Миграция идемпотентна (IF NOT EXISTS / INSERT OR IGNORE где надо) - [ ] `npm run lint:routes` чисто; тесты в пределах baseline ## Handoff to Next Phase