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:
@@ -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/
|
||||
|
||||
Reference in New Issue
Block a user