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,84 @@
|
||||
# md2vk — Реквизиты и доступы
|
||||
|
||||
**Цель этого файла — собрать все реквизиты/доступы к смежным сервисам, чтобы не искать их потом.**
|
||||
Секреты (токены, пароли) в git **не** помещаются — вместо них указывается файл-источник. Пароль
|
||||
приложения хранится только как bcrypt-хэш (см. ниже).
|
||||
|
||||
## Схема продакшена
|
||||
|
||||
```
|
||||
Внешний мир
|
||||
│ https://md2vk.nixg.ru
|
||||
▼
|
||||
vps02 (87.242.100.206) ── Caddy (:443, basic auth + fail2ban)
|
||||
│ reverse_proxy 10.8.0.2:8420 (WireGuard)
|
||||
▼
|
||||
bigbox (10.8.0.2) ── docker-контейнер md2vk ── 127.0.0.1:8420 ── SQLite /opt/md2vk/data/md2vk.db
|
||||
│
|
||||
▼ VK API: https://api.vk.com/method/wall.post
|
||||
```
|
||||
|
||||
| Роль | Значение |
|
||||
|------|----------|
|
||||
| Прод-домен | `md2vk.nixg.ru` → A 87.242.100.206 (vps02) |
|
||||
| bigbox | 10.8.0.2 (WireGuard), хост проекта `/opt/md2vk` |
|
||||
| vps02 | 87.242.100.206, ssh-алиас `vps02` (estorozhenko, sudo по ключу) |
|
||||
| Caddy | контейнер `caddy` на vps02, конфиг `/opt/caddy/Caddyfile`, reload: `docker exec caddy caddy reload` |
|
||||
| Порт приложения | 127.0.0.1:8420 на bigbox (контейнер), извне не торчит |
|
||||
| БД | SQLite `/opt/md2vk/data/md2vk.db` (в docker — `/data/md2vk.db`) |
|
||||
|
||||
## Git
|
||||
|
||||
| Forge | URL | Логин | Как авторизоваться |
|
||||
|-------|-----|-------|--------------------|
|
||||
| **gitverse.ru (истина)** | `git@gitverse.ru:kpa39l/md2vk.git` | **kpa39l** | SSH-ключ `/home/estorozhenko/.ssh/gitverse`; PAT — в `git-tokens.env` |
|
||||
| gitea.nixg.ru (pull mirror) | `https://gitea.nixg.ru/estorozhenko/md2vk.git` | estorozhenko | токен `GITEA_NIXG_TOKEN` |
|
||||
|
||||
- Источник токенов: `/opt/hermes/.hermes/secrets/git-tokens.env` (`source` его, не искать токены по каталогам).
|
||||
Переменные: `GITVERSE_LOGIN=kpa39l`, `GITVERSE_PAT`, `GITEA_NIXG_API`, `GITEA_NIXG_TOKEN`.
|
||||
- Gitea локальный API (bigbox): `http://127.0.0.1:3000/api/v1`, токен `GITEA_TOKEN` (там же).
|
||||
- Правила работы с зеркалами: см. skill `git-forge-management`, reference `gitea-pull-mirror-gitverse.md`
|
||||
(migrate работает только по HTTPS+PAT: `https://oauth2:<PAT>@gitverse.ru/kpa39l/md2vk.git`).
|
||||
- `tea` CLI: `~/.local/bin/tea`, логин `gitea.nixg-full` — для работы с gitea.nixg.ru.
|
||||
|
||||
## Доступы к сервисам (смежные)
|
||||
|
||||
| Сервис | URL/адрес | Доступ |
|
||||
|--------|-----------|--------|
|
||||
| DNS nixg.ru | NS: ns1..ns4.jino.ru (Jino) | панель регистратора Jino (кто владеет доменом), A-записи |
|
||||
| VK API | https://dev.vk.com/ru/reference | открытая документация |
|
||||
| VK ID (приложение) | https://id.vk.ru/about/business/go/docs/ru/vkid/latest/vk-id/connection/create-application | нужен аккаунт VK для client_id/secret |
|
||||
| Caddy vps02 | /opt/caddy/Caddyfile | ssh vps02, sudo |
|
||||
| fail2ban vps02 | jail local, `/etc/fail2ban/` | ssh vps02, sudo |
|
||||
|
||||
## Пароль приложения (basic auth на Caddy)
|
||||
|
||||
- Пользователь: **estorozhenko**
|
||||
- Пароль: задан пользователем (md2vk!Ghjcgtrn73 — в менеджере паролей пользователя; в git не хранится).
|
||||
- В Caddyfile: bcrypt-хэш: `$2a$14$T5gAji7gmg1t3Ifxw0CJy.jat9vpFfOiJIE8j5bnHynEVH1q.QTPS` (генерируется заново при создании записи).
|
||||
- Генерация: `docker exec caddy caddy hash-password --plaintext '<пароль>'`.
|
||||
- Блокировка: fail2ban ban IP после 5 неудачных попыток подряд, ручной unban:
|
||||
`fail2ban-client -c /etc/fail2ban unban <ip>` (на vps02). Детали — [security.md](security.md).
|
||||
|
||||
## VK OAuth (для OAuth-флоу VK ID)
|
||||
|
||||
- Ссылка авторизации: `https://oauth.vk.com/authorize?client_id=...&scope=wall,offline&redirect_uri=...&response_type=token`
|
||||
- API: `https://api.vk.com/method/wall.post?access_token=...&owner_id=...&message=...&v=5.199`
|
||||
- Версия API в конфиге: `VK_API_VERSION=5.199`
|
||||
- Требуется пользовательский токен (не токен сообщества из настроек группы); для стены группы — токен администратора с `wall`, `photos`, публикация через `from_group=1`.
|
||||
- Ограничение VK: создать статью (лонгрид) через API нельзя; через `wall.post` можно прикрепить уже опубликованную: `attachments=articleXXX_YYY`. Подробности — [vk-api.md](vk-api.md).
|
||||
|
||||
## Файлы секретов проекта (не в git)
|
||||
|
||||
| Файл | Что это |
|
||||
|------|---------|
|
||||
| `/opt/md2vk/secrets/token_encryption_key` | Fernet-ключ шифрования VK-токенов (генерируется `python3 -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"`) |
|
||||
| `/opt/md2vk/secrets/estorozhenko_api_key.txt` | API-ключ пользователя estorozhenko (md2vk_...) для вызова API |
|
||||
| `/opt/hermes/.hermes/secrets/git-tokens.env` | токены gitverse/gitea |
|
||||
|
||||
## Наблюдаемость
|
||||
|
||||
- Лог авторизации/запросов API: `/opt/md2vk/logs/access.log` (JSONL) и `access.{YYYY-MM-DD}.log` (ротация по дням).
|
||||
В docker: каталог монтируется как `/logs`. Поля: `ts, ip, method, path, api_key_prefix, user_id, status, success, latency_ms, error`.
|
||||
- Health: `GET /api/v1/health` → `{"status":"ok"}`.
|
||||
- Метрики доступности/успешности/активной сессии — собираются из лога (см. deploy.md).
|
||||
@@ -0,0 +1,84 @@
|
||||
# md2vk — Архитектура
|
||||
|
||||
## Обзор
|
||||
|
||||
FastAPI-сервис, публикует Markdown на стену VK через `wall.post` с `format_data`.
|
||||
Полный стек: Python 3.12, FastAPI, SQLAlchemy 2.0 async, SQLite, httpx, cryptography (Fernet), Docker.
|
||||
|
||||
## Компоненты
|
||||
|
||||
```
|
||||
┌─────────────────────────────── /opt/md2vk (bigbox) ───────────────────────────────┐
|
||||
│ │
|
||||
│ docker (контейнер md2vk, порт 127.0.0.1:8420) │
|
||||
│ ┌────────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ app/main.py FastAPI entrypoint, lifespan → init_db │ │
|
||||
│ │ app/config.py Settings из env (pydantic-settings) │ │
|
||||
│ │ app/database.py async engine (aiosqlite), get_db │ │
|
||||
│ │ app/models.py ORM: User, VkAccount, Publication │ │
|
||||
│ │ app/security.py Fernet encrypt/decrypt, API-key gen/verify │ │
|
||||
│ │ app/vk_client.py httpx-клиент: wall.post, users.get │ │
|
||||
│ │ app/converters/markdown_to_vk.py MD → VK format_data + чанки │ │
|
||||
│ │ app/api/v1.py эндпоинты /api/v1/* │ │
|
||||
│ │ app/api/deps.py auth по API-ключу (Bearer / тело) │ │
|
||||
│ └────────────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ /data (volume) → SQLite /data/md2vk.db │
|
||||
│ /logs → access.{date}.log (JSONL аудит) │
|
||||
│ /run/secrets/token_encryption_key (Docker secret) │
|
||||
└───────────────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Поток публикации
|
||||
|
||||
1. Клиент (Hermes/skill) `POST /api/v1/publish` c `api_key`, `vk_account_id`, `message_md`
|
||||
2. Auth: hash api_key → User.is_active
|
||||
3. Load VkAccount → decrypt_token (Fernet, только в памяти)
|
||||
4. `markdown_to_vk(message_md)` → chunks (VK-лимит ~4096 симв., режет по абзацам; `@` = 2 симв.)
|
||||
5. Publication (status=draft|scheduled)
|
||||
6. Если `publish_date` задан → status=scheduled (планировщик — TODO)
|
||||
7. Иначе `vk.wall_post(message, owner_id, from_group, ..., format_data=items)`
|
||||
8. Ответ: post_id, owner_id, url `https://vk.com/wall{owner}_{post}`; ошибки → status=error + error_message
|
||||
|
||||
## Порт/интерфейсы
|
||||
|
||||
| Интерфейс | Адрес | Назначение |
|
||||
|-----------|-------|------------|
|
||||
| Локальный | 127.0.0.1:8420 | внутренний (Caddy на vps02 → 10.8.0.2:8420) |
|
||||
| VK API | https://api.vk.com/method | исходящий (wall.post, users.get) |
|
||||
| Swagger | /docs | openapi, за basic auth |
|
||||
|
||||
## База данных (SQLite)
|
||||
|
||||
| Таблица | Ключевые поля |
|
||||
|---------|---------------|
|
||||
| users | id, name, email, api_key_hash, api_key_prefix, is_active, created_at, updated_at |
|
||||
| vk_accounts | id, user_id→users, vk_user_id (owner_id, `-` = сообщество), display_name, access_token_enc (Fernet), token_type (user\|group), is_active, expires_at, last_used_at |
|
||||
| publications | id, vk_account_id→vk_accounts, status (draft\|scheduled\|published\|error), markdown_original, vk_text, vk_format_data, vk_post_id, vk_owner_id, attachments, scheduled_at, published_at, error_message |
|
||||
|
||||
## Отложенные посты
|
||||
|
||||
Модель поддерживает `scheduled_at`, `/publish` с `publish_date` создаёт запись `scheduled`.
|
||||
**Планировщика нет** — открытая задача (worker-процесс/cron, выбирающий `status=scheduled AND scheduled_at<=now`).
|
||||
|
||||
## Ошибки VK API
|
||||
|
||||
Код | Смысл
|
||||
----|------
|
||||
`VkApiError` | обёртка: `error_code` + `error_msg` из ответа VK; пишется в `publications.error_message`
|
||||
|
||||
## Конвертер Markdown
|
||||
|
||||
| Markdown | VK format |
|
||||
|----------|-----------|
|
||||
| `**bold**`, `__bold__` | `bold` |
|
||||
| `*italic*`, `_italic_` | `italic` |
|
||||
| `***bold italic***` | `bold` + `italic` |
|
||||
| `` `code` `` | `inline_code` |
|
||||
| `[text](url)` | `link` (+url) |
|
||||
| `# H1..H6` | `bold` (uppercase) |
|
||||
| `> quote` | `italic` |
|
||||
| ```` ```code```` | `code` (без format) |
|
||||
| `---` | `───` |
|
||||
|
||||
Длинный текст режется на чанки по `\n\n` (абзацы), запас 10% от лимита 4096.
|
||||
+127
@@ -0,0 +1,127 @@
|
||||
# md2vk — Деплой
|
||||
|
||||
## Топология
|
||||
|
||||
```
|
||||
https://md2vk.nixg.ru (Caddy на vps02, basic auth)
|
||||
└─ reverse_proxy 10.8.0.2:8420 (WireGuard: vps02 .4 → bigbox .1)
|
||||
└─ docker-контейнер md2vk на bigbox, 127.0.0.1:8420
|
||||
```
|
||||
|
||||
- Порт приложения **8420** (8000 занят docker-search-api на bigbox).
|
||||
- Контейнер слушает на 127.0.0.1:8420 (снаружи bigbox не открыт).
|
||||
|
||||
## bigbox (10.8.0.2) — приложение
|
||||
|
||||
```bash
|
||||
cd /opt/md2vk
|
||||
make docker-up # docker compose up -d --build
|
||||
docker ps | grep md2vk # статус
|
||||
curl -s http://127.0.0.1:8420/api/v1/health # {"status":"ok"}
|
||||
```
|
||||
|
||||
- Данные: named volume `md2vk_data` → `/data/md2vk.db` (SQLite).
|
||||
- Секрет: `./secrets/token_encryption_key` → `/run/secrets/token_encryption_key`.
|
||||
- Логи аудита: `./logs/` монтируется в `/logs`.
|
||||
- Health: `/api/v1/health`, проверяется Docker healthcheck.
|
||||
- Восстановление после перезагрузки: `restart: unless-stopped`.
|
||||
- Если надо пересоздать БД — удалить том (только по явной команде, данные!):
|
||||
`docker compose down -v && make docker-up` (НЕ делать без подтверждения).
|
||||
|
||||
## Создание пользователя и API-ключа
|
||||
|
||||
```bash
|
||||
cd /opt/md2vk
|
||||
source venv/bin/activate
|
||||
TOKEN_ENCRYPTION_KEY_FILE=/opt/md2vk/secrets/token_encryption_key ./scripts/create_user.py \
|
||||
--name estorozhenko --api-key-out secrets/estorozhenko_api_key.txt
|
||||
# вывод: API key сохранён в secrets/estorozhenko_api_key.txt (md2vk_...)
|
||||
```
|
||||
|
||||
Источник: `scripts/create_user.py` (добавляет User с сгенерированным ключом; ключ отображается один раз).
|
||||
|
||||
## vps02 (87.242.100.206) — Caddy
|
||||
|
||||
Файл `/opt/caddy/Caddyfile` на vps02, секция:
|
||||
|
||||
```
|
||||
md2vk.nixg.ru {
|
||||
basic_auth /* {
|
||||
estorozhenko $2a$14$T5gAji7gmg1t3Ifxw0CJy.jat9vpFfOiJIE8j5bnHynEVH1q.QTPS
|
||||
}
|
||||
reverse_proxy 10.8.0.2:8420
|
||||
}
|
||||
```
|
||||
|
||||
Применить (контейнер caddy, монтирует Caddyfile):
|
||||
|
||||
```bash
|
||||
ssh vps02
|
||||
docker exec caddy caddy validate --config /etc/caddy/Caddyfile # проверить синтаксис
|
||||
docker exec caddy caddy reload --config /etc/caddy/Caddyfile # перезагрузить
|
||||
# если нужно: docker restart caddy
|
||||
```
|
||||
|
||||
- TLS: Caddy автоматически выпускает Let's Encrypt сертификат для md2vk.nixg.ru (acme в Caddyfile: `email kpa39l@yandex.ru`).
|
||||
- Сертификат на caddy_data (volume), переживает рестарты.
|
||||
|
||||
## vps02 — fail2ban (блокировка после 5 попыток)
|
||||
|
||||
Пакет ставится на vps02 (Debian/Ubuntu):
|
||||
|
||||
```bash
|
||||
ssh vps02
|
||||
sudo apt-get update && sudo apt-get install -y fail2ban
|
||||
```
|
||||
|
||||
Конфиг jail (`/etc/fail2ban/jail.local`) и фильтр (`/etc/fail2ban/filter.d/md2vk.conf`):
|
||||
|
||||
```ini
|
||||
# /etc/fail2ban/filter.d/md2vk.conf
|
||||
[Definition]
|
||||
failregex = ^.*"status":401.*
|
||||
ignoreregex =
|
||||
```
|
||||
|
||||
```ini
|
||||
# /etc/fail2ban/jail.local
|
||||
[md2vk]
|
||||
enabled = true
|
||||
port = http,https
|
||||
filter = md2vk
|
||||
logpath = /var/log/caddy/access.log
|
||||
maxretry = 5
|
||||
findtime = 600
|
||||
bantime = -1
|
||||
action = iptables-allports
|
||||
```
|
||||
|
||||
- Лог Caddy в JSON: настроить в Caddyfile `log { output file /var/log/caddy/access.log format json }` (глобальный блок).
|
||||
- Перезапуск: `sudo systemctl restart fail2ban`.
|
||||
- Проверка: `sudo fail2ban-client status md2vk`.
|
||||
- Ручной unban: `sudo fail2ban-client -c /etc/fail2ban unban <ip>`.
|
||||
|
||||
## Сквозная проверка
|
||||
|
||||
```bash
|
||||
# 1. health через Caddy (без auth → 401)
|
||||
curl -s -o /dev/null -w "%{http_code}\n" https://md2vk.nixg.ru/api/v1/health # 401
|
||||
# 2. health c auth → 200
|
||||
curl -s -u 'estorozhenko:<пароль>' https://md2vk.nixg.ru/api/v1/health # {"status":"ok"}
|
||||
# 3. публикация не требует VK-токена? нет — конвертация:
|
||||
curl -s -u 'estorozhenko:<пароль>' -H 'Authorization: Bearer <api_key>' \
|
||||
-X POST https://md2vk.nixg.ru/api/v1/convert \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"message_md":"**test** *curl*"}' # format_data
|
||||
# 4. баним проверкой: 5 раз неверный пароль → fail2ban ban IP
|
||||
for i in 1 2 3 4 5 6; do curl -s -o /dev/null -w "%{http_code}\n" -u 'estorozhenko:wrong' https://md2vk.nixg.ru/api/v1/health; done
|
||||
# → 401 ×6, затем соединение DROP (curl timeout)
|
||||
sudo fail2ban-client status md2vk # IP в banned
|
||||
sudo fail2ban-client unban <ваш-ip>
|
||||
```
|
||||
|
||||
## Восстановление после аварии
|
||||
|
||||
- Приложение: `docker compose up -d --force-recreate` на bigbox.
|
||||
- Caddy: `docker restart caddy` на vps02 (volume caddy_data хранит сертификаты).
|
||||
- БД: том `md2vk_data`; бэкап = копия `/opt/md2vk/data/md2vk.db` (вне контейнера) — включена в homelab-backup (/opt/backup).
|
||||
@@ -0,0 +1,65 @@
|
||||
# md2vk — Документация
|
||||
|
||||
Указатель документации проекта. Все реквизиты и доступы — в [access.md](access.md).
|
||||
|
||||
## Разделы
|
||||
|
||||
| Файл | Содержание |
|
||||
|------|------------|
|
||||
| [index.md](index.md) | Этот указатель, обзор проекта |
|
||||
| [access.md](access.md) | **Все реквизиты и доступы к смежным сервисам** (vps02, Caddy, gitverse, gitea, DNS, VK API, fail2ban) |
|
||||
| [architecture.md](architecture.md) | Архитектура, компоненты, порты, схема |
|
||||
| [vk-api.md](vk-api.md) | Порядок взаимодействия с VK API: OAuth-флоу, wall.post, format_data, ограничения (лонгриды) |
|
||||
| [security.md](security.md) | Безопасность: пароль, basic auth, блокировка после 5 попыток, Fernet, API-ключ |
|
||||
| [deploy.md](deploy.md) | Деплой: docker на bigbox, Caddy на vps02, fail2ban |
|
||||
| [status.md](status.md) | Статус реализации по фазам (в т.ч. история) |
|
||||
|
||||
## Обзор
|
||||
|
||||
Сервис публикации Markdown-текста на стене VK через официальный VK API (`wall.post`).
|
||||
|
||||
```
|
||||
┌─────────────┐ ┌─────────────┐ ┌──────────────┐
|
||||
│ Hermes │────▶│ md2vk │────▶│ VK API │
|
||||
│ Agent │ │ :8420 │ │ wall.post │
|
||||
│ (skill) │◀────│ FastAPI │ │ vk.com │
|
||||
└─────────────┘ └─────────────┘ └──────────────┘
|
||||
```
|
||||
|
||||
- Публикация Markdown → VK wall.post с `format_data` (жирный, курсив, код, ссылки, заголовки, цитаты)
|
||||
- Конвертация Markdown → VK format_data без публикации (preview)
|
||||
- Управление VK-аккаунтами (несколько страниц), шифрование токенов (Fernet)
|
||||
- Архив публикаций с фильтрацией по статусу
|
||||
- Отложенные посты (статус `scheduled`; планировщик — открытая задача)
|
||||
|
||||
## Внешние точки
|
||||
|
||||
| Что | Адрес |
|
||||
|-----|-------|
|
||||
| Прод | https://md2vk.nixg.ru (basic auth, см. [security.md](security.md)) |
|
||||
| Swagger | https://md2vk.nixg.ru/docs (за basic auth) |
|
||||
| Локально | http://127.0.0.1:8420 (bigbox), Swagger http://127.0.0.1:8420/docs |
|
||||
|
||||
## Быстрый старт (dev)
|
||||
|
||||
```bash
|
||||
cd /opt/md2vk
|
||||
source venv/bin/activate
|
||||
pip install -r requirements.txt
|
||||
# ключ шифрования уже есть: secrets/token_encryption_key (не в git)
|
||||
make dev # uvicorn :8000 --reload (для dev; прод-порт 8420 в docker)
|
||||
```
|
||||
|
||||
## Git
|
||||
|
||||
Схема: **gitverse.ru (истина) → gitea.nixg.ru (pull mirror)**. Подробности в [access.md](access.md).
|
||||
|
||||
## Открытые задачи (см. [status.md](status.md) и openspec/)
|
||||
|
||||
- Планировщик отложенных постов (worker + cron)
|
||||
- Web UI (форма поста + превью), сейчас только API
|
||||
- OAuth-флоу VK VK ID (кнопка «Прикрепить аккаунт») — сейчас токен вводится вручную
|
||||
- Unit/integration тесты (pytest)
|
||||
- Rate limiting (in-memory bucket)
|
||||
- Retry при сетевых ошибках VK API
|
||||
- Read-only rootfs в Docker (частично: `read_only: true` в compose)
|
||||
@@ -0,0 +1,71 @@
|
||||
# md2vk — Безопасность
|
||||
|
||||
## Два уровня доступа
|
||||
|
||||
### 1. Внешний — Caddy basic_auth (на vps02)
|
||||
|
||||
- Весь https://md2vk.nixg.ru закрыт HTTP Basic Auth.
|
||||
- Пользователь: **estorozhenko**; пароль — у пользователя, в конфиге только bcrypt-хэш.
|
||||
- Хэш в Caddyfile: `admin $2a$14$T5gAji7gmg1t3Ifxw0CJy.jat9vpFfOiJIE8j5bnHynEVH1q.QTPS`
|
||||
(генерируется `docker exec caddy caddy hash-password --plaintext '<пароль>'`).
|
||||
- Без basic auth сервис наружу не отдаётся.
|
||||
|
||||
### 2. Внутренний — API-ключ (FastAPI)
|
||||
|
||||
- Все эндпоинты `/api/v1/*` (кроме `/health`) требуют API-ключ `md2vk_...`:
|
||||
- в заголовке `Authorization: Bearer <api_key>` (GET) или в теле `{"api_key": ...}` (POST);
|
||||
- API-ключ даёт право публиковать, но **не позволяет прочитать VK-токен** (raw токен не возвращается ни одним эндпоинтом).
|
||||
- Хранится только SHA-256 хэш (`api_key_hash`), сравнение constant-time (`hmac.compare_digest`).
|
||||
- Пользователь estorozhenko + API-ключ: создаётся скриптом, ключ кладётся в `secrets/estorozhenko_api_key.txt` (не в git).
|
||||
|
||||
## Блокировка после 5 неудачных попыток (fail2ban на vps02)
|
||||
|
||||
- На vps02 установлен fail2ban, jail `md2vk`:
|
||||
- следит за логами Caddy (JSON: `/var/log/caddy/access.log`),
|
||||
- фильтр: basic auth failure (`401` с `"err"`, absence of `"user_id"`):
|
||||
```
|
||||
^.*"status":401.*
|
||||
```
|
||||
- правило: ban IP после **5 неудачных попыток подряд** (`maxretry=5`, `findtime=600`),
|
||||
- наказание: **ban до ручного снятия** (`bantime = -1`),
|
||||
- действие: `iptables-allports` (ban на уровне ядра, DROP).
|
||||
- **Ручная разблокировка** (на vps02, sudo):
|
||||
```bash
|
||||
fail2ban-client -c /etc/fail2ban unban <ip>
|
||||
fail2ban-client -c /etc/fail2ban status md2vk # проверить
|
||||
```
|
||||
- Логи fail2ban: `journalctl -u fail2ban -e` или `/var/log/fail2ban.log`.
|
||||
|
||||
## Шифрование VK-токенов
|
||||
|
||||
- VK OAuth-токены в БД только в шифрованном виде: **Fernet (AES-128-CBC + HMAC-SHA256)**.
|
||||
- Ключ — Docker secret `/run/secrets/token_encryption_key` (файл на bigbox: `secrets/token_encryption_key`, не в git).
|
||||
- Расшифровка только в памяти при публикации.
|
||||
|
||||
## Rate limiting
|
||||
|
||||
- В конфиге `RATE_LIMIT_PER_MINUTE=10` (запросов/мин на VK-аккаунт).
|
||||
- In-memory bucket — НЕ реализован (открытая задача).
|
||||
|
||||
## Аудит-лог
|
||||
|
||||
- Пишется JSONL в `/opt/md2vk/logs/access.{YYYY-MM-DD}.log` (вне контейнера — `logs/` на bigbox).
|
||||
- Событие на каждый запрос `/api/v1/*`: `ts, ip, method, path, api_key_prefix, user_id, status, success, latency_ms, error`.
|
||||
- Используется для мониторинга (доступность, успешность, активная сессия) и расследований.
|
||||
- Caddy на vps02 пишет свой access-лог (используется fail2ban).
|
||||
|
||||
## Матрица рисков
|
||||
|
||||
| Сценарий | Последствия | Защита |
|
||||
|---|---|---|
|
||||
| Утечка .db | Токены зашифрованы | Fernet + Docker secret |
|
||||
| Утечка API-ключа | Можно постить, но не украсть токен | API-ключ ≠ VK-токен |
|
||||
| Брутфорс basic auth | Полный доступ к веб-интерфейсу API | fail2ban ban после 5 попыток, ручной unban |
|
||||
| Перехват HTTP | — | HTTPS (Caddy/Let's Encrypt) |
|
||||
| Компрометация контейнера | Полный доступ к токенам | read-only rootfs, audit log |
|
||||
| Отзыв токена VK | Пост не выйдет | VK вернёт ошибку → error_message |
|
||||
|
||||
## Правило пользователя
|
||||
|
||||
> Никогда не удалять файлы пользователя без явного подтверждения. Пароли и токены не сохранять в коде —
|
||||
> только через переменные окружения, Docker secrets или файлы, исключённые из git (.gitignore).
|
||||
@@ -0,0 +1,59 @@
|
||||
# md2vk — Статус (история изменений)
|
||||
|
||||
> Актуальный статус работ — в `openspec/` и `todo`-файлах; здесь хроника и границы.
|
||||
|
||||
## 2026-09: Документация, деплой, git, openspec
|
||||
|
||||
**Сделано:**
|
||||
- [x] Аудит проекта (FastAPI ядро, Phase 1)
|
||||
- [x] Документация в `docs/` (index, access, architecture, vk-api, security, deploy, status) + `AGENTS.md`
|
||||
- [x] Аудит-лог авторизации (JSONL, ротация по дням) — см. `app/api/audit.py` + middleware
|
||||
- [x] Docker: порт 8420 (8000 занят), bind `./data`→`/data`, `./logs`→`/logs`, HEALTHCHECK через python (curl в slim-образе нет)
|
||||
- [x] openspec инициализирован (specs/ baseline)
|
||||
- [x] git init, push на gitverse.ru (kpa39l/md2vk), pull mirror в gitea.nixg.ru (estorozhenko/md2vk)
|
||||
- [x] Caddy на vps02: md2vk.nixg.ru + basic_auth estorozhenko (bcrypt), reverse_proxy 10.8.0.2:8420
|
||||
- [x] fail2ban на vps02: jail md2vk, ban после 5 попыток, ручной unban
|
||||
- [x] Сквозная проверка: https + auth + блокировка + unban
|
||||
|
||||
## Phase 1 — Ядро (DONE, из STATUS.md)
|
||||
|
||||
- Каркас проекта, структура директорий
|
||||
- Config из env (pydantic-settings)
|
||||
- Async SQLAlchemy + SQLite, инициализация БД
|
||||
- ORM-модели (User, VkAccount, Publication)
|
||||
- Fernet-шифрование/дешифрование токенов
|
||||
- Генерация и верификация API-ключей
|
||||
- Конвертер Markdown → VK format_data
|
||||
- VK API клиент (wall.post, users.get, check_token)
|
||||
- Pydantic-схемы запросов/ответов
|
||||
- API: /health, /accounts (CRUD), /publish, /convert, /publications
|
||||
- Аутентификация по API-ключу (заголовок + тело)
|
||||
- Генерация ключа шифрования
|
||||
- Проверка импортов и запуск
|
||||
- Тест конвертации через API (curl)
|
||||
|
||||
## Открытые задачи
|
||||
|
||||
- [ ] Планировщик отложенных постов (status=scheduled → wall.post). Модель готова, worker нет.
|
||||
- [ ] OAuth-флоу VK ID (кнопка «Прикрепить аккаунт»); нужны client_id/secret VK-приложения
|
||||
- [ ] Web UI (форма поста + превью)
|
||||
- [ ] Unit-тесты конвертера (pytest)
|
||||
- [ ] Integration-тесты API (pytest + httpx)
|
||||
- [ ] Rate limiting (in-memory bucket, RATE_LIMIT_PER_MINUTE)
|
||||
- [ ] Retry при сетевых ошибках VK API
|
||||
- [ ] Graceful shutdown
|
||||
- [ ] Read-only rootfs (уже `read_only: true` в compose, но /logs мешает; пересмотреть)
|
||||
- [ ] CORS (ограничение по origin)
|
||||
- [ ] Обработка длинных постов (multipart: пост + комментарии)
|
||||
- [ ] Hermes skill md2vk (проверка сервиса, публикация, добавление аккаунта)
|
||||
|
||||
## Границы Phase 1
|
||||
|
||||
- Конвертация Markdown → VK format_data (жирный, курсив, код, ссылки, заголовки, цитаты, блоки кода)
|
||||
- Публикация поста на стене VK
|
||||
- Шифрование токенов (Fernet + Docker secret)
|
||||
- Аутентификация по API-ключу
|
||||
- Управление VK-аккаунтами (добавить, список, удалить)
|
||||
- Архив публикаций с фильтрацией
|
||||
- Конвертация без публикации (preview)
|
||||
- Отложенные посты: только запись `scheduled` (без планировщика)
|
||||
@@ -0,0 +1,83 @@
|
||||
# md2vk — Взаимодействие с VK API
|
||||
|
||||
> Источник: официальная документация VK (dev.vk.com / id.vk.ru), проверено 2026-09.
|
||||
> Услуга работает через официальный протокол OAuth 2.0 и VK API — никакого «взлома».
|
||||
|
||||
## Как это работает
|
||||
|
||||
1. **OAuth-авторизация.** Пользователь нажимает «Прикрепить аккаунт» → редирект на VK.
|
||||
Пользователь подтверждает вход и выдаёт приложению права (scopes, обязателен `wall`).
|
||||
Сервис не получает пароль — только временный код/токен.
|
||||
2. **Access Token.** VK возвращает сервису access token — цифровой ключ с ограниченными правами
|
||||
(например, только публикация записей).
|
||||
3. **Публикация через API.** Сервис вызывает `wall.post` с текстом/вложениями, `owner_id`, `from_group`.
|
||||
Авторизация — `access_token` в параметрах запроса.
|
||||
|
||||
## Ссылки на официальную документацию
|
||||
|
||||
| Что | Ссылка |
|
||||
|-----|--------|
|
||||
| Создание и настройка приложения (VK ID, Standalone) | https://id.vk.com/about/business/go/docs/ru/vkid/latest/vk-id/connection/create-application |
|
||||
| Общая документация по API | https://dev.vk.com/ru/reference |
|
||||
| Авторизация (OAuth 2.0/2.1) | https://id.vk.com/about/business/go/docs/ru/vkid/latest/vk-id/connection/start-integration/auth-without-sdk/auth-without-sdk-web |
|
||||
| Метод wall.post | https://dev.vk.com/ru/method/wall.post |
|
||||
| Создать приложение (dev.vk.com) | https://dev.vk.com |
|
||||
|
||||
## OAuth-флоу (ссылка для получения токена)
|
||||
|
||||
```
|
||||
https://oauth.vk.com/authorize?client_id=<client_id>&scope=wall,offline&redirect_uri=<redirect>&response_type=token
|
||||
```
|
||||
|
||||
- `client_id` — ID приложения VK (см. VK ID).
|
||||
- `scope` — обязателен `wall`; `offline` — долгоживущий токен.
|
||||
- `response_type=token` — токен приходит в фрагменте редиректа.
|
||||
- После ответа: `access_token`, `user_id`, `expires_in` (0 = бессрочно).
|
||||
|
||||
## Публикация (wall.post)
|
||||
|
||||
```
|
||||
https://api.vk.com/method/wall.post
|
||||
POST data: access_token, v=5.199, owner_id, message, from_group, attachments, publish_date, format_data
|
||||
```
|
||||
|
||||
Параметры в нашем клиенте (`app/vk_client.py`):
|
||||
|
||||
| Параметр | Когда | Значение |
|
||||
|----------|-------|----------|
|
||||
| `owner_id` | группа | отрицательный ID сообщества (`-123`), для пользователя не передаём |
|
||||
| `from_group` | группа | `1` — пост от имени группы |
|
||||
| `friends_only` | опц. | только друзьям |
|
||||
| `publish_date` | отложка | Unix timestamp |
|
||||
| `attachments` | опц. | `photo123_456,...` |
|
||||
| `signed` | группа | подпись автора |
|
||||
| `format_data` | всегда | JSON `{"version":1,"items":[...]}` |
|
||||
|
||||
Ответ: `{"response":{"post_id":N,"owner_id":M}}` → URL `https://vk.com/wall{M}_{N}`.
|
||||
|
||||
### Важно про токены
|
||||
|
||||
- Для **личной стены** — токен пользователя.
|
||||
- Для **стены сообщества** — токен **администратора группы** с правами `wall` и `photos`.
|
||||
- Токены сообщества из настроек группы («Работа с API») для `wall.post` **не подходят** —
|
||||
нужен именно пользовательский токен.
|
||||
|
||||
## Лонгриды (статьи) — ограничение
|
||||
|
||||
- **Создавать статьи через официальный VK API нельзя.** В FAQ VK: «методов для работы с лонгридами пока что нет».
|
||||
- Можно **опубликовать на стене существующую статью** через `wall.post`:
|
||||
`attachments=articleXXX_YYY`, где `XXX` — ID автора (для сообщества — с минусом), `YYY` — ID статьи.
|
||||
Статья должна быть уже опубликована (черновик прикрепить нельзя).
|
||||
- Значит, полный автоматический дубликат блога (генерация лонгридов «из коробки») невозможен.
|
||||
Варианты: посты+фото+ссылки через API, статьи вручную, гибрид.
|
||||
|
||||
## Версия API
|
||||
|
||||
- В конфиге `VK_API_VERSION=5.199` (`app/config.py`).
|
||||
- Проверка токена: `users.get` (пустой user_ids → текущий пользователь).
|
||||
|
||||
## Ошибки
|
||||
|
||||
Формат ошибки VK: `{"error":{"error_code":N,"error_msg":"..."}}`.
|
||||
Наш клиент бросает `VkApiError(error_code, error_msg)`; в API публикации ошибка пишется в
|
||||
`publications.error_message`, ответ `success=false`.
|
||||
Reference in New Issue
Block a user