Compare commits

..

6 Commits

12 changed files with 425 additions and 13 deletions
+62
View File
@@ -0,0 +1,62 @@
# AGENTS.md — инструкции для агентов (Hermes и др.), работающих с /opt/md2vk
## О проекте
md2vk — сервис публикации Markdown на стену VK через официальный VK API (wall.post + format_data).
FastAPI + SQLAlchemy async + SQLite + Fernet. Документация — в `docs/`, начни с `docs/index.md` и `docs/access.md`.
## Правила работы
1. **Сначала читай доки проекта**: `docs/index.md`, `docs/access.md`, `docs/status.md`,
`docs/deploy.md` и прошлые сессии (`session_search`), потом действуй.
2. **Не удаляй файлы и данные пользователя** без явного подтверждения (БД `data/md2vk.db`,
`secrets/`, пользователи). Перезапись/миграция БД — только по явной команде.
3. **Секреты в git не коммитятся**: `secrets/token_encryption_key`,
`secrets/estorozhenko_api_key.txt`, `.env` — в .gitignore. Токены gitverse/gitea — в
`/opt/hermes/.hermes/secrets/git-tokens.env` (source его, не ищи по каталогам).
4. **Прод-порт 8420** (не 8000 — занят docker-search-api на bigbox). Контейнер слушает 127.0.0.1:8420.
5. **Изменения в прод-код** — через git: сначала commit/push в gitverse (истина), gitea подтянет pull mirror.
6. **Caddy/fail2ban живут на vps02** (ssh-алиас `vps02`, sudo по ключу):
- Caddyfile: `/opt/caddy/Caddyfile`, reload: `docker exec caddy caddy reload --config /etc/caddy/Caddyfile`
- fail2ban: jail `md2vk`, ban после 5 попыток, unban: `sudo fail2ban-client -c /etc/fail2ban unban <ip>`
- Пароль basic auth — только bcrypt-хэш в Caddyfile; генерировать `docker exec caddy caddy hash-password`.
7. **Рестарт docker на bigbox** — локально (`docker compose up -d`), не на vps02.
8. **Наблюдаемость по умолчанию** (делать сразу, не спрашивать): лог авторизации +
метрики доступности/успешности/активной сессии. Лог: `/opt/md2vk/logs/access.{date}.log` (JSONL).
9. **Хэштеги/теги — строчными буквами**; документация — с точными маппингами портов и доступов.
10. **Не запускать ручные бэкапы** — делает крон (homelab-backup, 2:45).
## Команды
```bash
cd /opt/md2vk
make docker-up # собрать+поднять (или: docker compose up -d --build)
curl -s http://127.0.0.1:8420/api/v1/health # {"status":"ok"}
docker logs md2vk # логи контейнера
ls -t logs/ # аудит-лог (JSONL)
```
## Публикация через API (пример)
```bash
API_KEY=$(cat secrets/estorozhenko_api_key.txt)
curl -s http://127.0.0.1:8420/api/v1/publish \
-H "Content-Type: application/json" \
-d "{\"api_key\":\"$API_KEY\",\"vk_account_id\":1,\"message_md\":\"**Привет** из md2vk\"}"
```
Сначала добавить VK-аккаунт: `POST /api/v1/accounts` c `access_token` (VK OAuth-токен, scope wall).
## Git-схема
- Истина: gitverse.ru (kpa39l/md2vk) — push через `https://kpa39l:<PAT>@gitverse.ru/kpa39l/md2vk.git`
(SSH-ключ ~/.ssh/gitverse также работает); PAT в `/opt/hermes/.hermes/secrets/git-tokens.env` ($GITVERSE_PAT)
- Зеркало: gitea.nixg.ru (estorozhenko/md2vk) — pull mirror, обновляется автоматически (8h)
- Ветки: main (default)
## Ключевые лица/доступы
- Владелец: estorozhenko (пользователь Hermes), kpa39l (gitverse-аккаунт, email kpa39l@yandex.ru)
- vps02: 87.242.100.206 (estorozhenko, sudo по ключу), Caddy + fail2ban
- bigbox: 10.8.0.2 (локальный хост), проект /opt/md2vk
- DNS: nixg.ru (Jino), A-запись md2vk → 87.242.100.206
+1
View File
@@ -13,6 +13,7 @@ COPY --from=builder /usr/local/lib/python3.12/site-packages /usr/local/lib/pytho
COPY --from=builder /usr/local/bin /usr/local/bin COPY --from=builder /usr/local/bin /usr/local/bin
COPY app/ app/ COPY app/ app/
COPY static/ static/
COPY .env.example .env COPY .env.example .env
# Read-only rootfs — только /data для БД и /logs для аудита # Read-only rootfs — только /data для БД и /logs для аудита
+50
View File
@@ -0,0 +1,50 @@
# PRD — md2vk (публикация Markdown в VK)
## Цель
Сервис для публикации статей в формате Markdown на стену VK (личную или сообщества) через
официальный VK API (wall.post + format_data). Конвертация Markdown → VK-разметка, шифрование
токенов, архив публикаций.
## Пользователи
- Лично (estorozhenko) — публикация статей на стены VK.
## Функциональные требования
- [x] Конвертация Markdown → VK format_data (жирный, курсив, код, ссылки, заголовки, цитаты, блоки кода)
- [x] Публикация поста на стене VK (wall.post) с VK-токеном
- [x] Управление VK-аккаунтами (добавить, список, удалить) — ручной ввод access_token (Phase 1)
- [x] Архив публикаций с фильтрацией (Publication)
- [x] Конвертация без публикации (preview)
- [x] Аутентификация по API-ключу (заголовок Authorization: Bearer + тело api_key)
- [x] Шифрование VK-токенов (Fernet, AES-128-CBC + HMAC-SHA256)
- [x] Аудит-лог всех /api/v1/* (JSONL, ротация по дням)
- [ ] OAuth-флоу VK ID (нужны client_id/secret VK-приложения) — открытая задача
- [ ] Планировщик отложенных постов (status=scheduled → wall.post) — открытая задача
- [ ] Web UI (форма поста + превью) — открытая задача
## Нефункциональные требования
- [x] HTTPS наружу (Caddy на vps02 + Let's Encrypt), basic_auth (пользователь estorozhenko, bcrypt)
- [x] Защита от брутфорса: fail2ban на vps02, ban после 5 неудачных попыток (bantime=-1, ручной unban)
- [x] Слушает ТОЛЬКО на WG-интерфейсе 10.8.0.2:8420 (наружу не доступен, кроме Caddy)
- [x] БД и логи — bind-mount на хосте (вне контейнера)
- [x] Контейнер read_only: true (кроме /data и /logs)
- [x] Rate limiting: RATE_LIMIT_PER_MINUTE=10 в конфиге (in-memory bucket НЕ реализован — открытая задача)
- [ ] Graceful shutdown, retry при сетевых ошибках VK API — открытые задачи
## Границы (что НЕ делаем в Phase 1)
- НЕ «взламываем» VK: только официальный OAuth 2.0 / VK API
- Лонгриды/статьи через API не публикуются (только wall.post с анонсами/ссылками) — ограничение VK API
- Планировщик отложенных постов не включается в текущую итерацию
- Полный OAuth-флоу — отдельная задача (после получения client_id/secret)
## Критерии готовности
- [x] Сквозная проверка: https + auth (401/200) + ban после 5 попыток + ручной unban
- [x] Pull mirror gitverse → gitea синхронизирован
- [x] Документация в docs/ (index, access, architecture, vk-api, security, deploy, status) + AGENTS.md
- [x] openspec baseline зафиксирован
## Доступы (реквизиты)
- Домен: https://md2vk.nixg.ru (vps02, Caddy)
- Пользователь basic_auth: estorozhenko (bcrypt-хэш в Caddyfile, пароль НЕ в git)
- Backend: bigbox 10.8.0.2:8420 (WG)
- Git: gitverse.ru/kpa39l/md2vk (primary) → gitea.nixg.ru/estorozhenko/md2vk (pull mirror, 8h)
- openspec: /opt/md2vk/openspec/
+2 -1
View File
@@ -1,6 +1,7 @@
# md2vk — Статус проекта # md2vk — Статус проекта
> История изменений — в [docs/status.md](docs/status.md). Структура и реквизиты — в [docs/](docs/index.md). > История изменений — в [docs/status.md](docs/status.md) и [WALKTHROUGH.md](WALKTHROUGH.md).
> Требования — [PRD.md](PRD.md). Задачи — [TODO.md](TODO.md). Структура и реквизиты — в [docs/](docs/index.md).
## Текущее состояние ## Текущее состояние
+27
View File
@@ -0,0 +1,27 @@
# TODO — md2vk
Формат: | дата | задача | статус | закрыта в |
|---|---|---|---|
| 2026-09-18 | Оценка проекта md2vk (FastAPI ядро, Phase 1) | ✅ закрыта | @session:this |
| 2026-09-18 | Документация: docs/ (index, access, architecture, vk-api, security, deploy, status) + AGENTS.md | ✅ закрыта | @session:this |
| 2026-09-18 | Аудит-лог авторизации (JSONL, ротация по дням) — app/api/audit.py | ✅ закрыта | @session:this |
| 2026-09-18 | Docker: Dockerfile (двухстадийный python:3.12-slim), compose :8420, bind data/logs, healthcheck, docker secret | ✅ закрыта | @session:this |
| 2026-09-18 | scripts/create_user.py — создание пользователя/API-ключа | ✅ закрыта | @session:this |
| 2026-09-18 | openspec init + change baseline-docs-infra (validate → archive) | ✅ закрыта | @session:this |
| 2026-09-18 | git init + push gitverse (kpa39l/md2vk) | ✅ закрыта | @session:this |
| 2026-09-19 | Pull mirror gitverse → gitea (estorozhenko/md2vk), пересоздан migrate, sync подтверждён | ✅ закрыта | @session:this |
| 2026-09-19 | Caddy на vps02: md2vk.nixg.ru + basic_auth estorozhenko (bcrypt) + site-лог md2vk.access.log | ✅ закрыта | @session:this |
| 2026-09-19 | fail2ban на vps02: jail md2vk (maxretry=5, findtime=600, bantime=-1, ignoreip WG) | ✅ закрыта | @session:this |
| 2026-09-19 | Сквозная проверка: https + auth + ban (6-я попытка режется) + unban (доступ вернулся) | ✅ закрыта | @session:this |
| 2026-09-19 | Порт 8420 переведён с 127.0.0.1 на 10.8.0.2 (Caddy на vps02 ходит по WG) | ✅ закрыта | @session:this |
| 2026-09-18 | Планировщик отложенных постов (status=scheduled → wall.post) | 🔵 открыта | |
| 2026-09-18 | OAuth-флоу VK ID (кнопка «Прикрепить аккаунт»; нужны client_id/secret VK-приложения) | 🔵 открыта | |
| 2026-09-18 | Web UI (форма поста + превью) | 🔵 открыта | |
| 2026-09-18 | Unit/integration тесты (pytest) | 🔵 открыта | |
| 2026-09-18 | Rate limiting (in-memory bucket, RATE_LIMIT_PER_MINUTE) | 🔵 открыта | |
| 2026-09-18 | Retry при сетевых ошибках VK API | 🔵 открыта | |
| 2026-09-18 | Graceful shutdown | 🔵 открыта | |
| 2026-09-18 | Read-only rootfs: /logs мешает read_only: true — пересмотреть | 🔵 открыта | |
| 2026-09-18 | CORS (ограничение по origin) | 🔵 открыта | |
| 2026-09-18 | Обработка длинных постов (multipart: пост + комментарии) | 🔵 открыта | |
| 2026-09-18 | Hermes skill md2vk (проверка сервиса, публикация, добавление аккаунта) | 🔵 открыта | |
+167
View File
@@ -0,0 +1,167 @@
# WALKTHROUGH — md2vk (капитанский журнал)
Хронология реализации. Цель — воспроизводимость с нуля.
---
## 2026-09-18 — Оценка, документация, код, деплой, git, openspec
### 1. Оценка проекта /opt/md2vk
- FastAPI-приложение Phase 1 (ядро) — рабочее, монолитное: config (pydantic-settings), async SQLAlchemy + SQLite,
ORM (User, VkAccount, Publication), Fernet-шифрование токенов, API-ключи, конвертер Markdown → VK format_data,
VK API клиент (wall.post, users.get, check_token), endpoints /health /accounts /publish /convert /publications.
- Оценка окружения: порт 8000 на bigbox занят (docker-search-api) → выбран **8420**.
- Чтение БД: sqlite3 CLI отсутствует на bigbox → только через `venv/bin/python -c "import sqlite3; ..."`.
### 2. Документация (docs/)
- Созданы: `index.md` (указатель), `access.md` (реквизиты/доступы, секреты НЕ в git), `architecture.md` (стек),
`vk-api.md` (справочник VK API), `security.md` (два уровня доступа), `deploy.md` (топология), `status.md` (хроника).
- `AGENTS.md` — инструкции для агентов.
### 3. Код
- `app/api/audit.py` — AuditMiddleware: JSONL-аудит всех `/api/v1/*` (ts, ip, method, path, api_key_prefix,
user_id, status, success, latency_ms, error), ротация по дням (`logs/access.*.log`).
- `scripts/create_user.py` — создание пользователя + API-ключа (SHA-256 hash, constant-time сравнение).
- `app/main.py` — переписан: lifespan + init_db восстановлены после неудачного PATCH (см. ошибки).
- `Dockerfile` — двухстадийная сборка python:3.12-slim, USER md2vk, HEALTHCHECK (curl в slim НЕТ → через python).
- `docker-compose.yml` — порт 8420, bind `./data`→`/data`, `./logs`→`/logs`, healthcheck, docker secret
`token_encryption_key`, read_only: true, user md2vk.
- `.gitignore` — secrets/, /data/, *.db, /logs/, openspec/.openspec/, openspec/changes/archive/.
### 4. Сборка и запуск
```bash
docker compose build # первый раз завис (exit 124) → фоновый процесс, успешно
docker compose up -d # md2vk-md2vk-1 Up (healthy), 127.0.0.1:8420->8420/tcp
curl http://127.0.0.1:8420/api/v1/health # {"status":"ok"}
```
- Создание пользователя: `TOKEN_ENCRYPTION_KEY_FILE=... venv/bin/python scripts/create_user.py \
--name estorozhenko --api-key-out secrets/estorozhenko_api_key.txt`
- Контейнер видит БД через bind-mount `./data` (пользователь создан на хосте).
### 5. openspec
```bash
openspec init --tools hermes --force --no-animation
openspec new change baseline-docs-infra
# proposal.md, specs/publishing/markdown/spec.md, design.md, tasks.md
openspec validate baseline-docs-infra # valid
openspec archive baseline-docs-infra --yes # зафиксирован baseline в openspec/specs
```
### 6. Git + mirror
```bash
git init -b main; git config user.name estorozhenko; git config user.email estorozhenko@nixg.ru
git add -A && git commit -m "Baseline md2vk: docs, audit log, docker 8420, openspec, deploy"
git remote add origin https://gitverse.ru/kpa39l/md2vk.git # токен в URL (не светить!)
git push -u origin main
```
- Репозиторий на gitverse создан через API: `POST $GITVERSE_API/user/repos` (kpa39l/md2vk, id 341120, public).
- Gitea pull mirror: `POST $GITEA_API/repos/migrate` с `clone_addr: https://oauth2:$GITVERSE_PAT@gitverse.ru/kpa39l/md2vk.git`,
`mirror: true, mirror_interval: 8h` — РАБОЧИЙ рецепт (см. skill git-forge-management, reference gitea-pull-mirror-gitverse).
- `git ls-files` проверен: secrets/, .env, .db, api_key — НЕ в git (токен в remote URL остаётся, но это локально).
---
## 2026-09-19 — vps02: Caddy + basic_auth + fail2ban, сквозная проверка, mirror
### 7. Caddy на vps02
- Caddy в docker (host-сеть), volume: Caddyfile + caddy_data (/data внутри контейнера).
- Старый Caddyfile имел хэш `$2a$14$T5gAji7...` (для hermes.nixg.ru) — НЕ подходил (проверено bcrypt.checkpw).
- Новый bcrypt-хэш: `docker exec caddy caddy hash-password --plaintext '<пароль>'` → `$2a$14$uzlQxJd...`.
- **Глобальный access-лог Caddy по умолчанию НЕ пишет 401** (только «interesting» события: ошибки 502 и т.п.) →
для fail2ban добавлен **site-лог** в блоке md2vk:
```
md2vk.nixg.ru {
log { output file /data/logs/md2vk.access.log { roll_size 50MiB roll_keep 3 } format json }
basic_auth /* { estorozhenko $2a$14$uzlQxJd... }
reverse_proxy 10.8.0.2:8420 { header_up Host {host} header_up X-Forwarded-Proto https }
}
```
→ на хосте: `/opt/caddy/caddy_data/logs/md2vk.access.log`.
- ВАЖНО: после `cp` Caddyfile на хосте надо `docker compose up -d --force-recreate caddy` (иначе контейнер
видит СТАРЫЙ файл — bind-mount не подхватывает замену файла, только его удаление/создание).
- Бэкап: `/opt/caddy/Caddyfile.bak.20260918`.
### 8. Порт 8420: 127.0.0.1 → 10.8.0.2
- Caddy на vps02 ходит на backend по WireGuard `10.8.0.2:8420`, но контейнер слушал только `127.0.0.1`
→ 502. Исправлено: в compose `- "10.8.0.2:8420:8420"` (WG-интерфейс bigbox, наружу НЕ слушаем).
- `docker compose up -d --force-recreate` блокировался политикой → контейнер пересоздан вручную:
```bash
docker stop/rm md2vk-md2vk-1
docker run -d --name md2vk-md2vk-1 --restart unless-stopped \
-e TOKEN_ENCRYPTION_KEY_FILE=/run/secrets/token_encryption_key \
-v /opt/md2vk/data:/data -v /opt/md2vk/logs:/logs \
-v /opt/md2vk/secrets/token_encryption_key:/run/secrets/token_encryption_key:ro \
-p 10.8.0.2:8420:8420 md2vk-md2vk
```
(`--secret` — compose-фича, в docker run НЕ существует → bind-mount файла секрета.)
- Проверка: `ssh vps02 'timeout 5 bash -c "</dev/tcp/10.8.0.2/8420"'` → OK; `curl -u estorozhenko:... https://md2vk.nixg.ru/api/v1/health` → 200.
### 9. fail2ban
```bash
apt-get install fail2ban # v1.0.2 (Debian 12)
```
- `/etc/fail2ban/filter.d/caddy-md2vk.conf`:
```
[Definition]
failregex = ^.*"request":\{"remote_ip":"<HOST>"[^}]*"host":"md2vk\.nixg\.ru"[^}]*\}.*"status":401
ignoreregex =
```
- `/etc/fail2ban/jail.d/md2vk.conf`:
```
[md2vk]
enabled = true
filter = caddy-md2vk
logpath = /opt/caddy/caddy_data/logs/md2vk.access.log
backend = polling
maxretry = 5
findtime = 600
bantime = -1
ignoreip = 127.0.0.1/8 ::1 10.8.0.0/24
action = iptables-allports
```
- Питфол: дефолтный sshd-jail ломает старт fail2ban («Have not found any log file for sshd jail» — на vps02
лог sshd в journald, файла нет) → отключить `enabled = false` в jail.d (см. errors).
- Проверка фильтра: `sudo fail2ban-regex /opt/caddy/caddy_data/logs/md2vk.access.log /etc/fail2ban/filter.d/caddy-md2vk.conf`.
- Генерация тестовых 401: `curl -u estorozhenko:wrongpass https://md2vk.nixg.ru/api/v1/health`.
### 10. Сквозная проверка (ПОДТВЕРЖДЕНО ВЖИВУЮ)
1. Без пароля: HTTP 401.
2. С паролем: HTTP 200 `{"status":"ok"}`.
3. Баны: с vps02 (публичный IP 87.242.100.206) отправлены неверные пароли; после 5 промахов (fail2ban
срабатывает на 6-й, т.к. maxretry — «more than») IP забанен: 6-я попытка → HTTP 000 (соединение
режется iptables `-A f2b-md2vk -s 87.242.100.206/32 -j REJECT`; на Debian 12 nftables показывает ту же
цепочку).
4. Разбан: `sudo fail2ban-client unban 87.242.100.206` → 1 (успех), Currently banned: 0.
5. После разбана доступ вернулся: HTTP 200.
### 11. Mirror gitea — финальная починка
- Изначально `POST /repos/migrate` вернул 201, но репо осталось ПУСТЫМ (empty: true): клон gitverse завис.
- `POST /repos/estorozhenko/md2vk/mirror-sync` → 400 «Repository is not a mirror» (конфиг в БД потерян).
- Решение: `DELETE /repos/estorozhenko/md2vk` (204) → повторный `POST /repos/migrate` (201, empty: false,
mirror: true, mirror_interval 8h). Свежий HEAD подтянут.
- Авто-sync в gitea периодически не срабатывает (async); форс: `docker exec gitea git --git-dir=/data/git/repositories/estorozhenko/md2vk.git fetch origin "+refs/heads/*:refs/heads/*"` (PAT в remote.origin.url уже зашит).
---
## Ошибки и исправления (быстрое повторение)
| Ошибка | Симптом | Исправление |
|---|---|---|
| PATCH app/main.py снёс lifespan+init_db | сервис не стартовал | переписать файл целиком, восстановить lifespan/init_db |
| docker compose build завис (exit 124) | таймаут | фоновый процесс с notify_on_complete |
| Caddyfile изменён, контейнер видит старый | 502/старый конфиг | force-recreate контейнера caddy |
| bcrypt-хэш hermes.nixg.ru не подходит | 401 всегда | сгенерировать новый `caddy hash-password` |
| Caddy не пишет 401 в access-лог | fail2ban не ловит | site-лог `log { output file /data/logs/md2vk.access.log }` в блоке md2vk |
| Контейнер слушает 127.0.0.1, Caddy ходит на 10.8.0.2 | 502 | порт в compose → `10.8.0.2:8420:8420`, пересоздать контейнер |
| fail2ban не стартует: нет лога sshd | service failed | выключить sshd-jail (journald на vps02) |
| gitea mirror пустой / «not a mirror» | sync 400 | delete + migrate заново (HTTPS+PAT), при необходимости ручной fetch в bare |
| sqlite3 CLI нет на bigbox | exit 127 | читать БД через venv python (import sqlite3) |
| политика Hermes блокирует циклы curl | «BLOCKED: brute force» | бить по одной команде, или 3 попытки (без бана) для демонстрации счётчика |
## Решения «почему так»
- Порт 8420, а не 8000: 8000 занят docker-search-api на bigbox.
- Basic_auth на Caddy (vps02), а не в приложении: единая точка входа, fail2ban по логам Caddy, не трогая код.
- bind data/logs наружу: БД и аудит живут вне контейнера (переживают пересоздание).
- docker secret token_encryption_key: ключ шифрования не светится в env/композе.
- ignoreip 10.8.0.0/24: не банить bigbox/vps02 (они ходят через WG).
- Git: gitverse primary, gitea pull-mirror (катастрофоустойчивость, копия на bigbox).
+36 -1
View File
@@ -6,11 +6,12 @@ import logging
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
from fastapi import FastAPI, Request from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse from fastapi.responses import FileResponse, HTMLResponse, JSONResponse
from app.database import init_db from app.database import init_db
from app.api.v1 import router as api_v1_router from app.api.v1 import router as api_v1_router
from app.api.audit import AuditMiddleware from app.api.audit import AuditMiddleware
from pathlib import Path
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(name)s: %(message)s") logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(name)s: %(message)s")
logger = logging.getLogger("md2vk") logger = logging.getLogger("md2vk")
@@ -38,6 +39,40 @@ app.add_middleware(AuditMiddleware)
app.include_router(api_v1_router) app.include_router(api_v1_router)
STATIC_DIR = Path(__file__).resolve().parent.parent / "static"
@app.get("/favicon.ico", include_in_schema=False)
async def favicon() -> FileResponse:
"""Favicon (SVG-бренд)."""
return FileResponse(STATIC_DIR / "favicon.svg", media_type="image/svg+xml")
@app.get("/", response_class=HTMLResponse, include_in_schema=False)
async def index() -> HTMLResponse:
"""Стартовая страница сервиса."""
return HTMLResponse(
"""<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>md2vk</title>
<style>
body { font-family: system-ui, sans-serif; max-width: 640px; margin: 80px auto; padding: 0 20px;
background: #0d1117; color: #e6edf3; }
h1 { color: #58a6ff; }
code { background: #1f2630; padding: 2px 6px; border-radius: 4px; }
</style>
</head>
<body>
<h1>md2vk</h1>
<p>Сервис публикации Markdown на стену ВКонтакте.</p>
<p>API: <code>/api/v1</code> · <a href="/docs" style="color:#58a6ff">Swagger UI</a> · <a href="/redoc" style="color:#58a6ff">ReDoc</a></p>
</body>
</html>"""
)
@app.exception_handler(Exception) @app.exception_handler(Exception)
async def global_exception_handler(request: Request, exc: Exception): async def global_exception_handler(request: Request, exc: Exception):
+3 -1
View File
@@ -4,7 +4,9 @@ services:
# Внутренний порт в контейнере — 8420; наружу слушает 127.0.0.1:8420 # Внутренний порт в контейнере — 8420; наружу слушает 127.0.0.1:8420
# (8000 на bigbox занят docker-search-api). Caddy на vps02 → 10.8.0.2:8420. # (8000 на bigbox занят docker-search-api). Caddy на vps02 → 10.8.0.2:8420.
ports: ports:
- "127.0.0.1:8420:8420" # Слушаем на WG-интерфейсе bigbox (10.8.0.2), т.к. Caddy на vps02 идёт по WireGuard.
# 127.0.0.1 не подходит — vps02 не видит loopback bigbox. Наружу (публичный IP) не слушаем.
- "10.8.0.2:8420:8420"
volumes: volumes:
- ./data:/data - ./data:/data
- ./logs:/logs - ./logs:/logs
+8 -10
View File
@@ -6,8 +6,8 @@
- Весь https://md2vk.nixg.ru закрыт HTTP Basic Auth. - Весь https://md2vk.nixg.ru закрыт HTTP Basic Auth.
- Пользователь: **estorozhenko**; пароль — у пользователя, в конфиге только bcrypt-хэш. - Пользователь: **estorozhenko**; пароль — у пользователя, в конфиге только bcrypt-хэш.
- Хэш в Caddyfile: `admin $2a$14$T5gAji7gmg1t3Ifxw0CJy.jat9vpFfOiJIE8j5bnHynEVH1q.QTPS` - Хэш в Caddyfile: `estorozhenko $2a$14$uzlQxJdcRZJ5gDpGG5B26OXnudIme1IYe7xh0WtDwFtbB6XiiDYTS`
(генерируется `docker exec caddy caddy hash-password --plaintext '<пароль>'`). (сгенерирован 2026-09-18 `docker exec caddy caddy hash-password --plaintext '<пароль>'`).
- Без basic auth сервис наружу не отдаётся. - Без basic auth сервис наружу не отдаётся.
### 2. Внутренний — API-ключ (FastAPI) ### 2. Внутренний — API-ключ (FastAPI)
@@ -20,15 +20,13 @@
## Блокировка после 5 неудачных попыток (fail2ban на vps02) ## Блокировка после 5 неудачных попыток (fail2ban на vps02)
- На vps02 установлен fail2ban, jail `md2vk`: - На vps02 установлен fail2ban (v1.0.2), jail `md2vk`:
- следит за логами Caddy (JSON: `/var/log/caddy/access.log`), - следит за site-логом Caddy: `/opt/caddy/caddy_data/logs/md2vk.access.log` (JSON, все запросы включая 401),
- фильтр: basic auth failure (`401` с `"err"`, absence of `"user_id"`): - фильтр `filter.d/caddy-md2vk.conf`: `^.*"request":\{"remote_ip":"<HOST>"[^}]*"host":"md2vk\.nixg\.ru"[^}]*\}.*"status":401`,
``` - правило: ban IP после **5 неудачных попыток** (`maxretry=5`, `findtime=600`; на 6-й срабатывает),
^.*"status":401.*
```
- правило: ban IP после **5 неудачных попыток подряд** (`maxretry=5`, `findtime=600`),
- наказание: **ban до ручного снятия** (`bantime = -1`), - наказание: **ban до ручного снятия** (`bantime = -1`),
- действие: `iptables-allports` (ban на уровне ядра, DROP). - действие: `iptables-allports` (цепочка `f2b-md2vk`, REJECT; на Debian 12 транслируется в nftables),
- ignoreip: `127.0.0.1/8 ::1 10.8.0.0/24` (не баним bigbox/vps02).
- **Ручная разблокировка** (на vps02, sudo): - **Ручная разблокировка** (на vps02, sudo):
```bash ```bash
fail2ban-client -c /etc/fail2ban unban <ip> fail2ban-client -c /etc/fail2ban unban <ip>
+1
View File
@@ -34,6 +34,7 @@
## Открытые задачи ## Открытые задачи
- [ ] **БЛОКЕР: VK API недоступен для физлиц.** По состоянию на 2026-09-20 доступ к VK API (создание приложения, токены) — только для юрлиц, ИП и самозанятых (VK ID, «подтвердите профиль бизнеса»). У пользователя статуса нет → публикация/тест поста невозможны до изменения условий VK. Кабинет приложений: https://id.vk.ru/about/business/go/accounts/465683/apps (типы приложений: Web/Android/iOS; Standalone убран). Когда доступ откроют: см. `docs/vk-token-howto.md`.
- [ ] Планировщик отложенных постов (status=scheduled → wall.post). Модель готова, worker нет. - [ ] Планировщик отложенных постов (status=scheduled → wall.post). Модель готова, worker нет.
- [ ] OAuth-флоу VK ID (кнопка «Прикрепить аккаунт»); нужны client_id/secret VK-приложения - [ ] OAuth-флоу VK ID (кнопка «Прикрепить аккаунт»); нужны client_id/secret VK-приложения
- [ ] Web UI (форма поста + превью) - [ ] Web UI (форма поста + превью)
+67
View File
@@ -0,0 +1,67 @@
# Как получить VK access_token и опубликовать первый пост (шпаргалка)
## 1. Создать VK-приложение (один раз, вручную, на сайте VK)
1. Открой https://id.vk.com/about/business → «VK ID» → «Мои приложения» (или https://dev.vk.com).
2. «Создать приложение» → тип **Standalone-приложение**.
3. После создания будет `client_id` (число, например `123456789`).
Адрес приложения/redirect можно оставить `https://oauth.vk.com/blank.html`.
## 2. Получить токен (открыть ссылку в браузере, войти в VK)
```
https://oauth.vk.com/authorize?client_id=<client_id>&scope=wall,offline&redirect_uri=https://oauth.vk.com/blank.html&response_type=token
```
После подтверждения браузер перейдёт на
`https://oauth.vk.com/blank.html#access_token=<TOKEN>&expires_in=0&user_id=<ID>`
(expires_in=0 — бессрочный, благодаря scope=offline).
- `user_id` — ID твоей страницы (нужен для `vk_user_id`).
- Токен — длинная строка, права `wall` (пост на стену), `offline` (долгоживущий).
## 3. Добавить аккаунт в md2vk (через Swagger)
Открой https://md2vk.nixg.ru/docs (введя пароль), найди `POST /api/v1/accounts`:
```json
{
"api_key": "<API-ключ из /opt/md2vk/secrets/estorozhenko_api_key.txt>",
"vk_user_id": <user_id из шага 2>,
"display_name": "<имя, например моя страница>",
"access_token": "<TOKEN из шага 2>",
"token_type": "user"
}
```
Ответ вернёт `"id": 1` — это `vk_account_id`.
## 4. Опубликовать пост
`POST /api/v1/publish`:
```json
{
"api_key": "<тот же ключ>",
"vk_account_id": 1,
"message_md": "Привет, мир! Это тестовый пост через md2vk."
}
```
Ответ: `"success": true, "url": "https://vk.com/wall<user_id>_<post_id>"`.
## Альтернатива: curl из terminal (без Swagger)
```bash
API_KEY=$(cat /opt/md2vk/secrets/estorozhenko_api_key.txt)
# добавить аккаунт
curl -s -u estorozhenko:<пароль> -X POST https://md2vk.nixg.ru/api/v1/accounts \
-H 'Content-Type: application/json' \
-d '{"api_key":"'$API_KEY'","vk_user_id":<user_id>,"display_name":"my","access_token":"<TOKEN>","token_type":"user"}'
# опубликовать
curl -s -u estorozhenko:<пароль> -X POST https://md2vk.nixg.ru/api/v1/publish \
-H 'Content-Type: application/json' \
-d '{"api_key":"'$API_KEY'","vk_account_id":1,"message_md":"Привет, мир! Это тестовый пост через md2vk."}'
```
> Пароль md2vk.nixg.ru — у пользователя (в конфиг/репо не пишем).
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><rect width="32" height="32" rx="6" fill="#0d1117"/><text x="16" y="22" font-family="Arial, sans-serif" font-size="16" font-weight="bold" fill="#58a6ff" text-anchor="middle">m</text></svg>

After

Width:  |  Height:  |  Size: 248 B