mirror of
https://gitverse.ru/kpa39l/md2vk.git
synced 2026-09-29 09:55:04 +00:00
Baseline md2vk: docs, audit log, docker 8420, openspec, deploy
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
# publishing/markdown Specification
|
||||
|
||||
## Purpose
|
||||
Перечень требований к сервису md2vk — публикация Markdown на стену VK через официальный VK API,
|
||||
с защитой доступа, аудитом и инфраструктурой деплоя.
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: Конвертация Markdown в формат VK
|
||||
|
||||
Сервис MUST конвертировать Markdown-текст в VK format_data (bold, italic, link, inline_code) и
|
||||
разбивать длинные посты на чанки (лимит ~4096 символов, учёт `@` = 2 символа).
|
||||
|
||||
#### Scenario: конвертация без публикации
|
||||
|
||||
- **WHEN** клиент вызывает `POST /api/v1/convert` с валидным `message_md` и API-ключом
|
||||
- **THEN** сервис возвращает `{text, format_data:{version:1,items:[...]}}` без обращения к VK
|
||||
|
||||
### Requirement: Публикация поста на стене VK
|
||||
|
||||
Сервис MUST публиковать посты через официальный VK API `wall.post` (access_token, owner_id,
|
||||
from_group, format_data), сохраняя запись в архив с статусом published/error.
|
||||
|
||||
#### Scenario: успешная публикация
|
||||
|
||||
- **WHEN** клиент вызывает `POST /api/v1/publish` с валидным API-ключом, vk_account_id и message_md
|
||||
- **THEN** сервис вызывает VK API wall.post и возвращает `{success:true, vk_post_id, vk_owner_id, url}`,
|
||||
а публикация получает статус `published`
|
||||
|
||||
#### Scenario: ошибка VK API
|
||||
|
||||
- **WHEN** VK API возвращает ошибку (невалидный/отозванный токен, лимиты)
|
||||
- **THEN** сервис возвращает `{success:false, error}` и сохраняет публикацию со статусом `error`
|
||||
и текстом ошибки в `error_message`
|
||||
|
||||
### Requirement: Управление VK-аккаунтами
|
||||
|
||||
Сервис MUST поддерживать несколько VK-аккаунтов на пользователя: добавление (с проверкой токена
|
||||
через users.get), список, soft-delete; токены хранить только зашифрованными.
|
||||
|
||||
#### Scenario: добавление аккаунта с валидным токеном
|
||||
|
||||
- **WHEN** клиент вызывает `POST /api/v1/accounts` с api_key и корректным access_token (scope wall)
|
||||
- **THEN** сервис проверяет токен через VK users.get, шифрует его Fernet и сохраняет аккаунт
|
||||
|
||||
#### Scenario: дубликат аккаунта
|
||||
|
||||
- **WHEN** клиент добавляет VK-аккаунт, который уже есть у пользователя (тот же vk_user_id)
|
||||
- **THEN** сервис возвращает 409 и не создаёт дубликат
|
||||
|
||||
### Requirement: Аутентификация по API-ключу
|
||||
|
||||
Все эндпоинты `/api/v1/*` (кроме `/health`) MUST требовать API-ключ `md2vk_...` (Bearer-заголовок
|
||||
или поле `api_key` в теле); в БД хранится только SHA-256 хэш; сравнение constant-time.
|
||||
API-ключ SHOULD NOT позволять прочитать VK-токен.
|
||||
|
||||
#### Scenario: запрос без ключа
|
||||
|
||||
- **WHEN** клиент вызывает `/api/v1/accounts` без Authorization-заголовка
|
||||
- **THEN** сервис возвращает 401 с сообщением об использовании Bearer
|
||||
|
||||
#### Scenario: неверный ключ
|
||||
|
||||
- **WHEN** клиент передаёт ключ, хэш которого не найден в БД
|
||||
- **THEN** сервис возвращает 401
|
||||
|
||||
### Requirement: Шифрование VK-токенов
|
||||
|
||||
VK OAuth-токены MUST храниться в БД только в зашифрованном виде (Fernet: AES-128-CBC + HMAC-SHA256);
|
||||
ключ шифрования — из Docker secret `/run/secrets/token_encryption_key`; расшифровка только в памяти.
|
||||
|
||||
#### Scenario: утечка файла БД
|
||||
|
||||
- **WHEN** файл `data/md2vk.db` попадает в чужие руки без ключа шифрования
|
||||
- **THEN** VK-токены не могут быть расшифрованы
|
||||
|
||||
### Requirement: Внешний контроль доступа (basic auth + fail2ban)
|
||||
|
||||
Прод-домен https://md2vk.nixg.ru MUST быть закрыт HTTP Basic Auth (пользователь estorozhenko,
|
||||
bcrypt-хэш в Caddyfile). Сервис MUST блокировать IP после 5 неудачных попыток подряд (fail2ban,
|
||||
jail `md2vk`), с ручной разблокировкой (fail2ban-client unban).
|
||||
|
||||
#### Scenario: неверный пароль 5 раз подряд
|
||||
|
||||
- **WHEN** клиент 5 раз подряд отправляет запросы с неверным паролем basic auth
|
||||
- **THEN** fail2ban банит IP (bantime=-1), последующие запросы получают отказ соединения
|
||||
|
||||
#### Scenario: ручная разблокировка
|
||||
|
||||
- **WHEN** администратор выполняет `fail2ban-client unban <ip>` на vps02
|
||||
- **THEN** IP снова может обращаться к сервису
|
||||
|
||||
### Requirement: Аудит-лог запросов API
|
||||
|
||||
Сервис MUST логировать каждый запрос `/api/v1/*` в JSONL (ts, ip, method, path, api_key_prefix,
|
||||
user_id, status, success, latency_ms, error) с ротацией по дням.
|
||||
|
||||
#### Scenario: запрос к API
|
||||
|
||||
- **WHEN** любой запрос к `/api/v1/*` завершается
|
||||
- **THEN** в каталоге логов (AUDIT_LOG_DIR, по умолчанию ./logs) появляется строка JSON
|
||||
с полями ts, status, latency_ms и др.
|
||||
|
||||
### Requirement: Деплой через docker и Caddy
|
||||
|
||||
Приложение MUST запускаться docker-контейнером на bigbox, слушать 127.0.0.1:8420, монтировать
|
||||
./data → /data (SQLite), ./logs → /logs, секрет token_encryption_key; Caddy на vps02 MUST
|
||||
проксировать md2vk.nixg.ru → 10.8.0.2:8420 с basic auth и TLS (Let's Encrypt).
|
||||
|
||||
#### Scenario: проверка здоровья через Caddy
|
||||
|
||||
- **WHEN** выполняется `curl https://md2vk.nixg.ru/api/v1/health` с верными учётными данными
|
||||
- **THEN** возвращается `{"status":"ok"}`
|
||||
|
||||
### Requirement: Git-зеркала
|
||||
|
||||
Репозиторий MUST иметь истину на gitverse.ru (kpa39l/md2vk) и pull mirror на gitea.nixg.ru
|
||||
(estorozhenko/md2vk); секреты и данные не коммитятся (.gitignore).
|
||||
|
||||
#### Scenario: синхронизация зеркала
|
||||
|
||||
- **WHEN** в gitverse появляется новый коммит в main
|
||||
- **THEN** gitea.nixg.ru подтягивает его по расписанию (mirror interval)
|
||||
|
||||
### Requirement: Отложенные посты
|
||||
|
||||
Сервис MUST сохранять посты с `publish_date` в статусе `scheduled`. Планировщик (worker,
|
||||
выполняющий wall.post для scheduled-постов) — открытая задача (SHOULD в будущем).
|
||||
|
||||
#### Scenario: отложенная публикация
|
||||
|
||||
- **WHEN** клиент вызывает `/api/v1/publish` с publish_date в будущем
|
||||
- **THEN** публикация сохраняется со статусом `scheduled` и не отправляется в VK сразу
|
||||
Reference in New Issue
Block a user