feat(api): внешний API и API-ключи для интеграций

Отдельный префикс /api/v1 со своей авторификацией по API-ключам,
чтобы внешние системы могли забирать и менять данные, не получая
доступа к админке.

Что добавлено:
- таблица api_keys (db/init.sql, db/migration.sql, ensureApiKeysTable)
- CRUD ключей: GET/POST /api/api-keys, PUT/DELETE /:id, POST /:id/rotate
- requireApiKey: X-Api-Key или Authorization: Bearer, только для /api/v1/*
- 21 эндпоинт /api/v1: branches, groups, students, modules, entries,
  lesson-reports, stats, me; списки в формате {items,total,limit,offset}
- страница управления ключами public/apikeys.html + пункт в меню

Безопасность:
- в БД только sha256(ключ) и префикс, секрет отдаётся один раз
- скоупы read/write: без write мутации дают 403
- branch_ids ключа сужают права и понижают роль до tutor
- per-key rate limit на cache.rateLimitStore, подбор ключей -> бан IP
- аудит мутаций с меткой via_api_key
- ключи не входят в бэкап и удаляются при restore

Проверено: api-keys.selftest.js (45 проверок), api.smoketest.js без
регрессий, работа без Redis через in-memory fallback.
This commit is contained in:
dev
2026-10-04 23:12:35 +03:00
parent d77df46092
commit 678cb97bb9
9 changed files with 1386 additions and 3 deletions
+49 -1
View File
@@ -504,6 +504,7 @@ docker compose exec redis redis-cli -a "$REDIS_PASSWORD" --no-auth-warning TTL '
```bash
node redis.selftest.js # юнит-тесты redis.js
node api.smoketest.js # сквозная проверка API (нужен запущенный стек)
node api-keys.selftest.js # внешний API и API-ключи (нужен запущенный стек)
```
## Уведомления
@@ -550,7 +551,7 @@ node api.smoketest.js # сквозная проверка API (нужен
## Безопасность
- **Пароль администратора** обязателен (`ADMIN_PASSWORD`) — он создаёт первого админа в пустой БД; фолбэка на `admin` нет. Самостоятельной роли в API не даёт: доступ только по сессиям.
- **Пароль администратора** обязателен (`ADMIN_PASSWORD`) — он создаёт первого админа в пустой БД; фолбэка на `admin` нет. В API сам по себе он не авторизует: доступ дают сессия (`X-Auth-Token`) или API-ключ (`X-Api-Key`, только для `/api/v1/*`).
- **CORS отключён** — кросс-доменные запросы к API запрещены.
- **Rate limiting** по IP на публичные роуты: `POST /api/entries` — 10 запросов / 15 мин, загрузка файлов и share-ссылки — 300 / 15 мин.
- **Загрузки** ограничены: суммарно на запись и на файл — лимиты из `UPLOAD_TOTAL_LIMIT_MB` / `UPLOAD_FILE_LIMIT_MB` (по умолчанию 200 МБ и 50 МБ); заблокированы опасные расширения (`.html`, `.js`, `.svg`, `.xml`, `.exe` и др.); SVG не отдаётся inline.
@@ -607,6 +608,52 @@ node api.smoketest.js # сквозная проверка API (нужен
Сессия хранится в таблице `sessions` (срок 30 дней) и кэшируется в Redis на 30 секунд.
### Внешний API и API-ключи
Для интеграций с внешними системами есть отдельный префикс `/api/v1` и собственная авторизация — **API-ключи**. Ключи создаются в админке: **API-ключи** в боковом меню (`public/apikeys.html`), либо через `GET/POST/PUT/DELETE /api/api-keys` (администратор).
Ключ передаётся в заголовке `X-Api-Key` или `Authorization: Bearer <ключ>`:
```bash
curl -H "X-Api-Key: wsk_ВАШ_КЛЮЧ" https://ВАШ_ДОМЕН/api/v1/groups
curl -H "Authorization: Bearer wsk_ВАШ_КЛЮЧ" https://ВАШ_ДОМЕН/api/v1/stats
```
Секрет показывается **один раз** — при создании и при перевыпуске (`⟳` в таблице). В базе хранится только SHA-256 хеш, поэтому восстановить ключ нельзя: если он потерян или утёк, выпустите новый, а старый удалите.
| Метод | Путь | Право |
| --- | --- | --- |
| `GET` | `/api/v1/me` | чтение |
| `GET` | `/api/v1/branches` | чтение |
| `GET` | `/api/v1/groups`, `/groups/:id` | чтение |
| `GET` | `/api/v1/students`, `/students/:id` | чтение |
| `POST`, `PUT` | `/api/v1/students[/:id]` | запись |
| `GET` | `/api/v1/modules` | чтение |
| `GET` | `/api/v1/entries`, `/entries/:id`, `/entries/:id/files` | чтение |
| `POST`, `PUT`, `DELETE` | `/api/v1/entries[/:id]` | запись |
| `GET` | `/api/v1/lesson-reports`, `/lesson-reports/:id` | чтение |
| `POST`, `PUT`, `DELETE` | `/api/v1/lesson-reports[/:id]` | запись |
| `GET` | `/api/v1/stats` | чтение |
Списки возвращают единый формат `{ items, total, limit, offset }`; поддерживаются `limit`/`offset` (до 500) и фильтры (`group_id`, `module_id`, `student_name`, `search`, `date_from`, `date_to`).
Меры безопасности:
- **Права**: у ключа есть скоупы `read` и `write`; без `write` все изменения возвращают `403`.
- **Филиалы**: ключ можно ограничить конкретными филиалами — он увидит **не больше**, чем доступно выдавшему его пользователю (админский ключ с ограничением теряет доступ ко всем остальным филиалам).
- **Срок и лимиты**: у ключа задаются дата окончания и лимит запросов в минуту (по умолчанию 120); при превышении — `429`.
- **Отзыв**: удаление ключа действует немедленно; старый ключ перестаёт работать и после ротации.
- **Подбор ключей** считается, при частых неудачах IP получает бан.
- **Аудит**: все изменения, сделанные через API, попадают в аудит с пометкой `via_api_key`.
- **Изоляция**: ключ работает только в `/api/v1/*` и не открывает доступ к админ-панели.
- **Бэкап**: ключи не входят в архив — после восстановления их нужно выпустить заново.
Проверка:
```bash
node api-keys.selftest.js
```
## Структура проекта
```
@@ -621,6 +668,7 @@ node api.smoketest.js # сквозная проверка API (нужен
├── diff.js # пословный diff текста и сборка изменений записи для аудита
├── diff.selftest.js # тесты diff.js (вставки, удаления, большие тексты, обрезка)
├── api.smoketest.js # сквозная проверка API по поднятому стеку
├── api-keys.selftest.js # тесты внешнего API: ключи, права, филиалы, rate limit
├── worker.js # фоновый worker AI-проверки и ИИ-улучшения фото
├── certs/ # cert.pem приложения (монтируется в tailscale, в git не хранится)
├── db/