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
+20
View File
@@ -176,6 +176,26 @@ CREATE TABLE IF NOT EXISTS sessions (
CREATE INDEX IF NOT EXISTS idx_sessions_token ON sessions(token);
CREATE INDEX IF NOT EXISTS idx_sessions_expires_at ON sessions(expires_at);
CREATE TABLE IF NOT EXISTS api_keys (
id SERIAL PRIMARY KEY,
user_id INT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
name VARCHAR(150) NOT NULL,
prefix VARCHAR(16) NOT NULL,
key_hash VARCHAR(64) NOT NULL UNIQUE,
scopes TEXT[] NOT NULL DEFAULT ARRAY['read']::TEXT[],
branch_ids INT[] NOT NULL DEFAULT ARRAY[]::INT[],
rate_limit_per_min INT,
created_at TIMESTAMPTZ DEFAULT now(),
last_used_at TIMESTAMPTZ,
last_used_ip VARCHAR(45),
expires_at TIMESTAMPTZ,
revoked_at TIMESTAMPTZ
);
CREATE INDEX IF NOT EXISTS idx_api_keys_key_hash ON api_keys(key_hash);
CREATE INDEX IF NOT EXISTS idx_api_keys_user_id ON api_keys(user_id);
CREATE INDEX IF NOT EXISTS idx_api_keys_revoked_at ON api_keys(revoked_at);
ALTER TABLE audit_log ADD COLUMN IF NOT EXISTS user_id INT REFERENCES users(id) ON DELETE SET NULL;
CREATE TABLE IF NOT EXISTS group_photos (