- Updated docker-compose.yml to include environment variables and health checks for the app service. - Modified Dockerfile to install curl for health checks and adjusted the build process for backend services. - Improved user notification handling in activation, blocking, and config revocation commands by integrating Telegram notifications. - Added new test cases to validate the updated command handlers and Telegram notifier functionality. - Enhanced documentation to reflect the new Telegram bot features and user management improvements.
152 lines
13 KiB
Markdown
152 lines
13 KiB
Markdown
# Telegram Bot
|
||
|
||
Telegram-бот — **второй канал доставки** (presentation-адаптер) поверх той же Application-логики,
|
||
что и REST API. Он не содержит бизнес-правил: команды бота вызывают те же CQRS-команды/запросы
|
||
(`ICommand/IQuery`), что и веб. Бизнес-инварианты живут в домене, а не в обработчиках бота.
|
||
|
||
## Возможности
|
||
|
||
1. **Ссылка на сайт** — кнопка/команда, открывающая веб-панель (при желании — с одноразовым
|
||
deep-link авто-входом для уже привязанного пользователя).
|
||
2. **Мои конфиги** — список VPN-конфигов пользователя (протокол, локация, трафик, срок, статус),
|
||
ссылка-подписка и QR по каждому. Доступно только привязанному аккаунту.
|
||
3. **Авторизация через Telegram (passwordless)** — вход на сайт без пароля: подтверждение входа
|
||
в боте. Требует предварительной **привязки Telegram** к аккаунту.
|
||
4. **Админ: обработка запросов активации** — админ (по Telegram id из env) получает уведомление
|
||
о запросе активации с комментарием заявителя и жмёт «Активировать / Отклонить» прямо в боте.
|
||
5. **DM-уведомления пользователю** — если Telegram привязан, бот шлёт личные уведомления о ключевых
|
||
событиях: «аккаунт активирован», «конфиг отозван админом», «вы заблокированы».
|
||
6. **Восстановление пароля** — реализовано через passwordless-вход: привязанный пользователь входит
|
||
через бота (`/login`) и меняет пароль в настройках (`ChangePasswordCommand`). Без привязки
|
||
Telegram сброс делает только админ (`ResetUserPasswordCommand`). Отдельная команда бота
|
||
`/resetpassword` с одноразовой ссылкой на смену пароля — **backlog**, в MVP не реализована.
|
||
|
||
> **Скоуп бота в MVP — просмотр (read-only) по конфигам.** Создание/ротация/отзыв конфигов — только
|
||
> на сайте. Полное самообслуживание в боте (создание/отзыв) — в backlog.
|
||
|
||
## Размещение в архитектуре
|
||
|
||
- Бот работает **в том же процессе**, что и API, как `BackgroundService`
|
||
(`TelegramBotHostedService`) — это укладывается в требование «фронт+бек в одном контейнере».
|
||
- Транспорт с Telegram: **long polling** для MVP (не требует публичного webhook-URL, проще в
|
||
одиночном контейнере). Webhook — опциональная альтернатива для прод-нагрузки (тогда — секретный
|
||
токен заголовка для верификации).
|
||
- Библиотека — **Telegram.Bot** (де-факто стандарт для C#).
|
||
- Код бота лежит в `PnvPanel.Api/Telegram/` (хендлеры апдейтов, построители клавиатур,
|
||
форматтеры сообщений). Обращения к домену — **только** через собственный `ISender`.
|
||
`Telegram.Bot` не проникает в Application/Domain.
|
||
|
||
```
|
||
Telegram ──updates──► TelegramBotHostedService (Api)
|
||
│ ISender.Send(command/query) // свой диспетчер
|
||
▼
|
||
Application (те же хендлеры, что и REST)
|
||
```
|
||
|
||
## Модель данных (добавления)
|
||
|
||
- `AppUser.TelegramUserId : long?` — id пользователя Telegram (уникальный, nullable до привязки).
|
||
- `AppUser.TelegramUsername : string?`, `AppUser.TelegramLinkedAt : DateTimeOffset?`.
|
||
- `TelegramLinkToken` — короткоживущий одноразовый токен привязки (`token`, `userId`, `expiresAt`, `consumedAt`).
|
||
- `TelegramLoginRequest` — запрос passwordless-входа: `id/nonce`, `status`
|
||
(`Pending/Approved/Rejected/Expired/Consumed`), `userId?` (после подтверждения), `createdAt`, `expiresAt`.
|
||
|
||
Подробности полей — в [domain-model.md](domain-model.md).
|
||
|
||
## Флоу 1 — Привязка Telegram к аккаунту
|
||
|
||
Предусловие: пользователь уже вошёл на сайте (изначально аккаунт создаётся с username+пароль).
|
||
|
||
1. На сайте «Привязать Telegram» → `POST /api/auth/telegram/link-token` → `{ deepLink }`
|
||
вида `https://t.me/<bot>?start=link_<token>` (+ QR). Токен короткоживущий, одноразовый.
|
||
2. Пользователь открывает бота по ссылке → `/start link_<token>`.
|
||
3. Бот берёт `from.id` (Telegram user id), валидирует токен (`LinkTelegramCommand`), проставляет
|
||
`TelegramUserId`/`TelegramUsername`/`TelegramLinkedAt`, гасит токен.
|
||
4. Бот подтверждает: «Аккаунт привязан». Сайт узнаёт об успехе (поллинг статуса или SignalR).
|
||
|
||
Инварианты: один `TelegramUserId` ↔ один аккаунт; повторная привязка требует отвязки; токен
|
||
нельзя переиспользовать и он истекает.
|
||
|
||
## Флоу 2 — Passwordless-вход через бота
|
||
|
||
Предусловие: Telegram уже привязан к аккаунту.
|
||
|
||
1. На сайте «Войти через Telegram» → `POST /api/auth/telegram/login-request` →
|
||
`{ requestId, deepLink, qr, expiresAt }`. Сайт начинает ждать результат (поллинг
|
||
`GET /api/auth/telegram/login-request/{requestId}` или событие SignalR).
|
||
2. Пользователь открывает `https://t.me/<bot>?start=login_<nonce>` → бот по `from.id` находит
|
||
привязанный аккаунт и показывает inline-кнопки **«Подтвердить вход / Отклонить»**
|
||
(с деталями: время, IP/устройство инициатора — для защиты от фишинга).
|
||
3. Подтверждение (`ApproveTelegramLoginCommand`) → запрос переходит в `Approved`, привязывается к `userId`.
|
||
4. Сайт (по поллингу/SignalR) получает результат: бэкенд выпускает **стандартные JWT** —
|
||
access в теле ответа, refresh в httpOnly cookie. Запрос помечается `Consumed`.
|
||
|
||
Если Telegram **не привязан** — passwordless-вход невозможен (бот предлагает сперва привязать
|
||
аккаунт). Регистрация целиком через Telegram — вне MVP (см. backlog).
|
||
|
||
## Флоу 3 — Просмотр конфигов в боте
|
||
|
||
1. Привязанный пользователь: `/configs` или кнопка «Мои конфиги».
|
||
2. Бот вызывает `GetMyConfigsQuery` (тот же, что и веб) от имени `AppUser`, найденного по `TelegramUserId`.
|
||
3. Ответ — список с трафиком/сроком/статусом; по каждому конфигу — inline-кнопки «Ссылка», «QR».
|
||
QR отдаётся как изображение (генерация на сервере).
|
||
|
||
## Флоу 4 — Обработка активации админом в боте
|
||
|
||
1. Пользователь отправляет запрос активации (сайт: `POST /api/activation/request { comment }`);
|
||
доменное событие `ActivationRequested`.
|
||
2. Бот шлёт сообщение каждому админу (Telegram id из `AdminSeed__TelegramUserIds`) с username/комментарием
|
||
заявителя и inline-кнопками **«✅ Активировать / ❌ Отклонить»**.
|
||
3. Нажатие → `ApproveActivationCommand`/`RejectActivationCommand` (те же, что на сайте) → пользователь
|
||
активируется, ему уходит realtime-пуш `userActivated`, админам обновляется сообщение (решение зафиксировано).
|
||
4. Действие доступно только Telegram id из списка админов; проверка — на стороне бота перед вызовом команды.
|
||
|
||
## Команды и клавиатуры
|
||
|
||
| Команда / кнопка | Действие | Требует привязки |
|
||
| ---------------------- | -------------------------------------------------------------- | ---------------- |
|
||
| `/start` | Приветствие + справка по командам | нет |
|
||
| `/start link_<t>` | Привязка аккаунта по токену | нет |
|
||
| `/start login_<n>` | Подтверждение passwordless-входа (deep-link с сайта) | да |
|
||
| `/configs` | Список конфигов | да |
|
||
| `/unlink` | Отвязать Telegram от аккаунта | да |
|
||
| `/help` | Справка | нет |
|
||
| «Активировать/Отклонить» | (admin) решение по запросу активации | админ по env |
|
||
| `/requests` | (admin) список ожидающих запросов активации | админ по env |
|
||
|
||
> Passwordless-вход и `/resetpassword` инициируются с сайта (кнопка «Войти через Telegram»),
|
||
> не отдельной командой бота — см. пункт 6 выше про `/resetpassword` (backlog).
|
||
|
||
## Безопасность
|
||
|
||
- Токены привязки и nonce входа: высокоэнтропийные, **короткоживущие** (≈2–5 мин), **одноразовые**.
|
||
- Подтверждение входа показывает контекст (время/устройство) — защита от несанкционированных запросов.
|
||
- Верификация источника апдейтов: webhook — секретный заголовок; long polling — прямой канал к Bot API по TLS.
|
||
- Rate-limiting на создание login/link-запросов и на команды бота.
|
||
- Токен бота — секрет (env/secret-store), в логи не попадает; апдейты логируются без чувствительных данных.
|
||
- Passwordless-вход выпускает те же JWT/refresh, что и обычный — единые правила сессий и ротации.
|
||
- Альтернатива боту для веб-входа — официальный **Telegram Login Widget** (HMAC-подпись данных
|
||
ботом, верификация на бэке). Оставлено как опция; основной путь — подтверждение в боте.
|
||
|
||
## Конфигурация
|
||
|
||
```jsonc
|
||
"Telegram": {
|
||
"BotToken": "…", // секрет (env/secret-store)
|
||
"BotUsername": "PnvPanelBot",
|
||
"Mode": "LongPolling", // или "Webhook"
|
||
"WebhookUrl": null,
|
||
"WebhookSecret": null,
|
||
"PublicSiteUrl": "https://panel.example.com"
|
||
}
|
||
```
|
||
|
||
Telegram id администраторов задаются отдельно — `AdminSeed__TelegramUserIds` (см.
|
||
[`.env.example`](../.env.example)); именно они авторизуют админ-кнопки в боте и получают
|
||
уведомления о запросах активации.
|
||
|
||
Сообщения бота локализованы (**RU/EN**) по языку пользователя, синхронно с настройкой языка в вебе.
|
||
|
||
Строго типизированные `IOptions<TelegramOptions>` с валидацией на старте; при отсутствии
|
||
`BotToken` бот не стартует (панель работает без него).
|