Refactor environment configuration and update documentation for MVP status
- Removed deprecated Telegram user ID configuration from `.env.example` and added a new setting for admin Telegram user IDs. - Updated `CLAUDE.md` to reflect the current MVP status, detailing completed features and testing coverage. - Enhanced `README.md` with quick start instructions for Docker setup and clarified project status. - Revised API design documentation to include updated error handling and request/response structures. - Improved frontend documentation to outline the project structure and technologies used.
This commit is contained in:
+119
-96
@@ -1,151 +1,174 @@
|
||||
# Telegram Bot
|
||||
|
||||
Telegram-бот — **второй канал доставки** (presentation-адаптер) поверх той же Application-логики,
|
||||
что и REST API. Он не содержит бизнес-правил: команды бота вызывают те же CQRS-команды/запросы
|
||||
(`ICommand/IQuery`), что и веб. Бизнес-инварианты живут в домене, а не в обработчиках бота.
|
||||
что и REST API. Он не содержит бизнес-правил: обработчики апдейтов вызывают те же CQRS-команды/запросы
|
||||
(`ICommand`/`IQuery` через собственный `ISender`), что и веб. Бизнес-инварианты живут в домене.
|
||||
|
||||
## Возможности
|
||||
## Возможности (реализовано)
|
||||
|
||||
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 не реализована.
|
||||
1. **Мои конфиги** — `/configs` присылает текстовый список (метка/локация, протокол, статус) —
|
||||
**без ссылок и QR** в самом боте; за ссылкой/QR пользователь идёт на сайт. Доступно только
|
||||
привязанному аккаунту.
|
||||
2. **Авторизация через Telegram (passwordless)** — вход на сайт без пароля: инициируется на сайте,
|
||||
подтверждается в боте кнопками «Подтвердить/Отклонить». Требует предварительной привязки Telegram.
|
||||
3. **Админ: обработка запросов активации** — админ (по Telegram id из `Telegram__AdminTelegramUserIds`)
|
||||
получает сообщение о каждом запросе активации с именем и комментарием заявителя, жмёт
|
||||
«✅ Активировать / ❌ Отклонить» прямо в сообщении. `/requests` показывает все ожидающие запросы по требованию.
|
||||
4. **DM-уведомления пользователю** (если Telegram привязан): активация аккаунта, блокировка,
|
||||
принудительный отзыв конфига админом.
|
||||
5. **Отвязка** — `/unlink`.
|
||||
|
||||
> **Скоуп бота в MVP — просмотр (read-only) по конфигам.** Создание/ротация/отзыв конфигов — только
|
||||
> на сайте. Полное самообслуживание в боте (создание/отзыв) — в backlog.
|
||||
**Не реализовано / backlog:**
|
||||
- Ссылки/QR/подписка в самом боте (только текстовый список конфигов).
|
||||
- Отдельная команда `/resetpassword` с одноразовой ссылкой — восстановление пароля сейчас идёт
|
||||
только через обычный passwordless-вход (`/start login_<n>`) + смену пароля в настройках на сайте.
|
||||
- Webhook-транспорт — только long polling, конфигурации режима/URL в коде нет.
|
||||
- Регистрация нового аккаунта из бота (только привязка существующего).
|
||||
- Полное самообслуживание (создание/ротация/отзыв конфигов) — бот **read-only** по конфигам.
|
||||
|
||||
## Размещение в архитектуре
|
||||
|
||||
- Бот работает **в том же процессе**, что и API, как `BackgroundService`
|
||||
(`TelegramBotHostedService`) — это укладывается в требование «фронт+бек в одном контейнере».
|
||||
- Транспорт с Telegram: **long polling** для MVP (не требует публичного webhook-URL, проще в
|
||||
одиночном контейнере). Webhook — опциональная альтернатива для прод-нагрузки (тогда — секретный
|
||||
токен заголовка для верификации).
|
||||
- Библиотека — **Telegram.Bot** (де-факто стандарт для C#).
|
||||
- Код бота лежит в `PnvPanel.Api/Telegram/` (хендлеры апдейтов, построители клавиатур,
|
||||
форматтеры сообщений). Обращения к домену — **только** через собственный `ISender`.
|
||||
`Telegram.Bot` не проникает в Application/Domain.
|
||||
- Бот работает **в том же процессе**, что и API, как `BackgroundService` (`TelegramBotHostedService`,
|
||||
`PnvPanel.Api/Telegram/`) — условие «фронт+бек в одном контейнере».
|
||||
- Транспорт — **только long polling** (`ITelegramBotClient.ReceiveAsync`). Webhook рассматривался на
|
||||
этапе планирования, но не реализован: `TelegramOptions` (`Infrastructure/Telegram/TelegramOptions.cs`)
|
||||
содержит только `BotToken`, `BotUsername`, `PublicSiteUrl`, `AdminTelegramUserIds` — полей
|
||||
`Mode`/`WebhookUrl`/`WebhookSecret` в коде нет.
|
||||
- Библиотека — **Telegram.Bot**. Каждый апдейт обрабатывается в своём DI-scope (`PnvBotUpdateHandler`,
|
||||
как HTTP-запрос — свежие scoped-сервисы на апдейт).
|
||||
- Обращения к домену — **только** через `ISender`. `Telegram.Bot` не проникает в Application/Domain.
|
||||
- Если `Telegram:BotToken` не задан — `TelegramBotHostedService.ExecuteAsync` сразу возвращается,
|
||||
бот не стартует, панель работает без него (лог `Telegram__BotToken не задан — бот не стартует.`).
|
||||
|
||||
```
|
||||
Telegram ──updates──► TelegramBotHostedService (Api)
|
||||
│ ISender.Send(command/query) // свой диспетчер
|
||||
Telegram ──updates──► TelegramBotHostedService → PnvBotUpdateHandler (Api/Telegram/)
|
||||
│ ISender.Send(command/query) // свой диспетчер, свой DI-scope на апдейт
|
||||
▼
|
||||
Application (те же хендлеры, что и REST)
|
||||
```
|
||||
|
||||
## Модель данных (добавления)
|
||||
|
||||
- `AppUser.TelegramUserId : long?` — id пользователя Telegram (уникальный, nullable до привязки).
|
||||
- `AppUser.TelegramUsername : string?`, `AppUser.TelegramLinkedAt : DateTimeOffset?`.
|
||||
- `AppUser.TelegramUserId : long?`, `TelegramUsername : string?`, `TelegramLinkedAt : DateTimeOffset?`.
|
||||
- `TelegramLinkToken` — короткоживущий одноразовый токен привязки (`token`, `userId`, `expiresAt`, `consumedAt`).
|
||||
- `TelegramLoginRequest` — запрос passwordless-входа: `id/nonce`, `status`
|
||||
(`Pending/Approved/Rejected/Expired/Consumed`), `userId?` (после подтверждения), `createdAt`, `expiresAt`.
|
||||
- `TelegramLoginRequest` — запрос passwordless-входа: `id`, `status`
|
||||
(`Pending/Approved/Rejected/Expired/Consumed`), `userId?` (после подтверждения), `context?`
|
||||
(IP инициатора, собирается, но **в текст подтверждения в боте не выводится** — известный TODO),
|
||||
`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). Токен короткоживущий, одноразовый.
|
||||
1. На сайте «Привязать Telegram» → `POST /api/auth/telegram/link-token` → `{ deepLink, expiresAt }`,
|
||||
`deepLink` вида `https://t.me/<bot>?start=link_<token>`. Фронт рисует QR из `deepLink` сам
|
||||
(`qrcode.react`) — бэкенд картинку не генерирует.
|
||||
2. Пользователь открывает бота по ссылке → `/start link_<token>`.
|
||||
3. Бот берёт `from.id` (Telegram user id), валидирует токен (`LinkTelegramCommand`), проставляет
|
||||
`TelegramUserId`/`TelegramUsername`/`TelegramLinkedAt`, гасит токен.
|
||||
4. Бот подтверждает: «Аккаунт привязан». Сайт узнаёт об успехе (поллинг статуса или SignalR).
|
||||
3. Бот берёт `from.id`, вызывает `LinkTelegramCommand(token, telegramUserId, telegramUsername)` —
|
||||
валидирует токен, проставляет `TelegramUserId`/`TelegramUsername`/`TelegramLinkedAt`, гасит токен.
|
||||
4. Бот отвечает «✅ Telegram успешно привязан к вашему аккаунту» (или текст ошибки). Сайт узнаёт об
|
||||
успехе поллингом статуса активации/профиля.
|
||||
|
||||
Инварианты: один `TelegramUserId` ↔ один аккаунт; повторная привязка требует отвязки; токен
|
||||
нельзя переиспользовать и он истекает.
|
||||
Инварианты: один `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`.
|
||||
`{ requestId, deepLink, expiresAt }`. Сайт начинает поллить
|
||||
`GET /api/auth/telegram/login-request/{requestId}`.
|
||||
2. Пользователь открывает `https://t.me/<bot>?start=login_<requestId>` → бот по `from.id` находит
|
||||
привязанный аккаунт (если не найден — просит сначала привязать) и показывает сообщение
|
||||
«Кто-то пытается войти в PnvPanel через ваш аккаунт. Подтвердить вход?» с инлайн-кнопками
|
||||
**«✅ Подтвердить вход» / «❌ Отклонить»**.
|
||||
3. Подтверждение → `ApproveTelegramLoginCommand`/`RejectTelegramLoginCommand` → запрос переходит в
|
||||
`Approved`/`Rejected`.
|
||||
4. Сайт по следующему поллингу получает результат: при `Approved` — `accessToken` в теле,
|
||||
`refresh` уже пришёл в httpOnly cookie (та же логика cookie, что и обычный логин, включая
|
||||
`Secure = request.IsHttps`). Запрос помечается `Consumed`.
|
||||
|
||||
Если Telegram **не привязан** — passwordless-вход невозможен (бот предлагает сперва привязать
|
||||
аккаунт). Регистрация целиком через Telegram — вне MVP (см. backlog).
|
||||
Если Telegram **не привязан** — бот сразу сообщает «Сначала привяжите Telegram к аккаунту на сайте»,
|
||||
подтвердить вход невозможно. Регистрация целиком через Telegram — вне MVP.
|
||||
|
||||
## Флоу 3 — Просмотр конфигов в боте
|
||||
|
||||
1. Привязанный пользователь: `/configs` или кнопка «Мои конфиги».
|
||||
2. Бот вызывает `GetMyConfigsQuery` (тот же, что и веб) от имени `AppUser`, найденного по `TelegramUserId`.
|
||||
3. Ответ — список с трафиком/сроком/статусом; по каждому конфигу — inline-кнопки «Ссылка», «QR».
|
||||
QR отдаётся как изображение (генерация на сервере).
|
||||
1. Привязанный пользователь: `/configs`.
|
||||
2. Бот вызывает `GetMyConfigsQuery` (тот же, что и веб) от пользователя, найденного по `TelegramUserId`.
|
||||
3. Ответ — обычное текстовое сообщение, по строке на конфиг:
|
||||
`• {Label ?? Location} ({Protocol}) — {Status}`. Если конфигов нет — «У вас пока нет конфигов.»
|
||||
**Ссылок, QR и кнопок здесь нет** — за подключением пользователь идёт на сайт.
|
||||
|
||||
## Флоу 4 — Обработка активации админом в боте
|
||||
|
||||
1. Пользователь отправляет запрос активации (сайт: `POST /api/activation/request { comment }`);
|
||||
доменное событие `ActivationRequested`.
|
||||
2. Бот шлёт сообщение каждому админу (Telegram id из `AdminSeed__TelegramUserIds`) с username/комментарием
|
||||
заявителя и inline-кнопками **«✅ Активировать / ❌ Отклонить»**.
|
||||
3. Нажатие → `ApproveActivationCommand`/`RejectActivationCommand` (те же, что на сайте) → пользователь
|
||||
активируется, ему уходит realtime-пуш `userActivated`, админам обновляется сообщение (решение зафиксировано).
|
||||
4. Действие доступно только Telegram id из списка админов; проверка — на стороне бота перед вызовом команды.
|
||||
1. Пользователь отправляет запрос активации (сайт: `POST /api/activation/request { comment }`) →
|
||||
`RequestActivationCommandHandler` шлёт SignalR `activationRequested` группе `admins` **и** вызывает
|
||||
`ITelegramNotifier.NotifyAdminsActivationRequestedAsync` (прямой вызов из хендлера, без диспетчера событий).
|
||||
2. Каждому админу (по `Telegram__AdminTelegramUserIds`) уходит сообщение с именем и комментарием
|
||||
заявителя и кнопками **«✅ Активировать / ❌ Отклонить»**.
|
||||
3. Нажатие → `ApproveActivationCommand`/`RejectActivationCommand` (те же, что на сайте) →
|
||||
пользователь активируется/отклоняется, ему уходит realtime `userActivated` (только при одобрении) +
|
||||
Telegram-DM, если привязан; нажавшему админу приходит короткое подтверждение («✅ Пользователь
|
||||
активирован.»/«❌ Запрос отклонён.») — само сообщение с кнопками не редактируется.
|
||||
4. Проверка прав — на стороне бота (`TrySetAdminCurrentUserAsync`) перед вызовом команды: Telegram id
|
||||
должен быть в `Telegram__AdminTelegramUserIds`.
|
||||
|
||||
`/requests` — тот же список запросов по требованию (до 10 штук, `Pending`), с теми же кнопками; для
|
||||
кого он доступен — та же проверка админ-id.
|
||||
|
||||
## Команды и клавиатуры
|
||||
|
||||
| Команда / кнопка | Действие | Требует привязки |
|
||||
| ---------------------- | -------------------------------------------------------------- | ---------------- |
|
||||
| `/start` | Приветствие + справка по командам | нет |
|
||||
| `/start link_<t>` | Привязка аккаунта по токену | нет |
|
||||
| `/start login_<n>` | Подтверждение passwordless-входа (deep-link с сайта) | да |
|
||||
| `/configs` | Список конфигов | да |
|
||||
| `/unlink` | Отвязать Telegram от аккаунта | да |
|
||||
| `/help` | Справка | нет |
|
||||
| «Активировать/Отклонить» | (admin) решение по запросу активации | админ по env |
|
||||
| `/requests` | (admin) список ожидающих запросов активации | админ по env |
|
||||
| Команда / кнопка | Действие | Требует привязки |
|
||||
| -------------------------- | -------------------------------------------------------------- | ----------------- |
|
||||
| `/start` | Приветствие + справка по командам | нет |
|
||||
| `/start link_<token>` | Привязка аккаунта по токену | нет |
|
||||
| `/start login_<requestId>` | Подтверждение passwordless-входа (deep-link с сайта) | да |
|
||||
| `/configs` | Текстовый список конфигов | да |
|
||||
| `/unlink` | Отвязать Telegram от аккаунта | да |
|
||||
| `/help` | Справка (то же сообщение, что `/start`) | нет |
|
||||
| «✅ Активировать»/«❌ Отклонить» | (admin) решение по конкретному запросу активации | админ по env |
|
||||
| `/requests` | (admin) список ожидающих запросов активации (до 10) | админ по env |
|
||||
|
||||
> Passwordless-вход и `/resetpassword` инициируются с сайта (кнопка «Войти через Telegram»),
|
||||
> не отдельной командой бота — см. пункт 6 выше про `/resetpassword` (backlog).
|
||||
Любой другой текст → «Не понимаю эту команду. /help — список команд.»
|
||||
|
||||
## Безопасность
|
||||
|
||||
- Токены привязки и nonce входа: высокоэнтропийные, **короткоживущие** (≈2–5 мин), **одноразовые**.
|
||||
- Подтверждение входа показывает контекст (время/устройство) — защита от несанкционированных запросов.
|
||||
- Верификация источника апдейтов: webhook — секретный заголовок; long polling — прямой канал к Bot API по TLS.
|
||||
- Rate-limiting на создание login/link-запросов и на команды бота.
|
||||
- Токен бота — секрет (env/secret-store), в логи не попадает; апдейты логируются без чувствительных данных.
|
||||
- Passwordless-вход выпускает те же JWT/refresh, что и обычный — единые правила сессий и ротации.
|
||||
- Альтернатива боту для веб-входа — официальный **Telegram Login Widget** (HMAC-подпись данных
|
||||
ботом, верификация на бэке). Оставлено как опция; основной путь — подтверждение в боте.
|
||||
- Токены привязки и `requestId` входа: высокоэнтропийные, короткоживущие, одноразовые (см.
|
||||
`TelegramLinkToken`/`TelegramLoginRequest` в [domain-model.md](domain-model.md) — точный TTL не
|
||||
вынесен в отдельное конфигурируемое значение, см. [tech-stack.md](tech-stack.md)).
|
||||
- Подтверждение входа **не показывает** контекст (время/IP/устройство) инициатора — поле `Context`
|
||||
собирается (`CreateLoginRequestCommand`), но в текст сообщения бота не подставляется. Если это
|
||||
важно для защиты от фишинга — доработка на будущее, не текущее поведение.
|
||||
- Транспорт — только long polling: прямой канал к Bot API по TLS, без верификации webhook-заголовка
|
||||
(webhook не реализован).
|
||||
- `Telegram:BotToken` — секрет (env/secret-store), в логи не попадает; при пустом токене
|
||||
`TelegramBotClient` конструируется с синтаксической заглушкой вместо падения при старте — реальный
|
||||
HTTP-вызов всё равно не происходит, т.к. `TelegramBotHostedService` и `TelegramNotifier` сами
|
||||
проверяют `BotToken` перед использованием клиента.
|
||||
- Passwordless-вход выпускает те же JWT/refresh, что и обычный (тот же `AuthResult`, та же cookie-логика).
|
||||
- Явного rate-limit на команды бота нет (в отличие от HTTP-эндпоинтов `/api/auth/*`).
|
||||
|
||||
## Конфигурация
|
||||
|
||||
Реальные поля `TelegramOptions` (секция `Telegram`):
|
||||
|
||||
```jsonc
|
||||
"Telegram": {
|
||||
"BotToken": "…", // секрет (env/secret-store)
|
||||
"BotUsername": "PnvPanelBot",
|
||||
"Mode": "LongPolling", // или "Webhook"
|
||||
"WebhookUrl": null,
|
||||
"WebhookSecret": null,
|
||||
"PublicSiteUrl": "https://panel.example.com"
|
||||
"BotToken": "…", // секрет; пусто = бот не стартует
|
||||
"BotUsername": "PnvPanelBot", // для deepLink; null/пусто -> deepLink в ответах API тоже null
|
||||
"AdminTelegramUserIds": "123456789,987654321" // через запятую
|
||||
// "PublicSiteUrl" — поле есть в TelegramOptions, но нигде не читается (мёртвый код,
|
||||
// не задавай его — эффекта не будет)
|
||||
}
|
||||
```
|
||||
|
||||
Telegram id администраторов задаются отдельно — `AdminSeed__TelegramUserIds` (см.
|
||||
[`.env.example`](../.env.example)); именно они авторизуют админ-кнопки в боте и получают
|
||||
уведомления о запросах активации.
|
||||
Переменные окружения — `Telegram__BotToken`, `Telegram__BotUsername`, `Telegram__AdminTelegramUserIds`
|
||||
(см. [`.env.example`](../.env.example)). Именно они авторизуют админ-кнопки в боте и определяют,
|
||||
кому слать уведомления о запросах активации — **не** сидируются в БД и не связаны с учёткой
|
||||
сид-админа (`AdminSeed:*`), это независимый список.
|
||||
|
||||
Сообщения бота локализованы (**RU/EN**) по языку пользователя, синхронно с настройкой языка в вебе.
|
||||
|
||||
Строго типизированные `IOptions<TelegramOptions>` с валидацией на старте; при отсутствии
|
||||
`BotToken` бот не стартует (панель работает без него).
|
||||
Сообщения бота **не локализованы** по языку пользователя — все тексты на русском независимо от языка
|
||||
интерфейса на сайте (в отличие от веба, где RU/EN переключаются). Синхронизация языка бота с вебом —
|
||||
не реализована, backlog.
|
||||
|
||||
Reference in New Issue
Block a user