- 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.
16 KiB
Telegram Bot
Telegram-бот — второй канал доставки (presentation-адаптер) поверх той же Application-логики,
что и REST API. Он не содержит бизнес-правил: обработчики апдейтов вызывают те же CQRS-команды/запросы
(ICommand/IQuery через собственный ISender), что и веб. Бизнес-инварианты живут в домене.
Возможности (реализовано)
- Мои конфиги —
/configsприсылает текстовый список (метка/локация, протокол, статус) — без ссылок и QR в самом боте; за ссылкой/QR пользователь идёт на сайт. Доступно только привязанному аккаунту. - Авторизация через Telegram (passwordless) — вход на сайт без пароля: инициируется на сайте, подтверждается в боте кнопками «Подтвердить/Отклонить». Требует предварительной привязки Telegram.
- Админ: обработка запросов активации — админ (по Telegram id из
Telegram__AdminTelegramUserIds) получает сообщение о каждом запросе активации с именем и комментарием заявителя, жмёт «✅ Активировать / ❌ Отклонить» прямо в сообщении./requestsпоказывает все ожидающие запросы по требованию. - DM-уведомления пользователю (если Telegram привязан): активация аккаунта, блокировка, принудительный отзыв конфига админом.
- Отвязка —
/unlink.
Не реализовано / backlog:
- Ссылки/QR/подписка в самом боте (только текстовый список конфигов).
- Отдельная команда
/resetpasswordс одноразовой ссылкой — восстановление пароля сейчас идёт только через обычный passwordless-вход (/start login_<n>) + смену пароля в настройках на сайте. - Webhook-транспорт — только long polling, конфигурации режима/URL в коде нет.
- Регистрация нового аккаунта из бота (только привязка существующего).
- Полное самообслуживание (создание/ротация/отзыв конфигов) — бот read-only по конфигам.
Размещение в архитектуре
- Бот работает в том же процессе, что и 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 → PnvBotUpdateHandler (Api/Telegram/)
│ ISender.Send(command/query) // свой диспетчер, свой DI-scope на апдейт
▼
Application (те же хендлеры, что и REST)
Модель данных (добавления)
AppUser.TelegramUserId : long?,TelegramUsername : string?,TelegramLinkedAt : DateTimeOffset?.TelegramLinkToken— короткоживущий одноразовый токен привязки (token,userId,expiresAt,consumedAt).TelegramLoginRequest— запрос passwordless-входа:id,status(Pending/Approved/Rejected/Expired/Consumed),userId?(после подтверждения),context?(IP инициатора, собирается, но в текст подтверждения в боте не выводится — известный TODO),createdAt,expiresAt.
Подробности полей — в domain-model.md.
Флоу 1 — Привязка Telegram к аккаунту
Предусловие: пользователь уже вошёл на сайте.
- На сайте «Привязать Telegram» →
POST /api/auth/telegram/link-token→{ deepLink, expiresAt },deepLinkвидаhttps://t.me/<bot>?start=link_<token>. Фронт рисует QR изdeepLinkсам (qrcode.react) — бэкенд картинку не генерирует. - Пользователь открывает бота по ссылке →
/start link_<token>. - Бот берёт
from.id, вызываетLinkTelegramCommand(token, telegramUserId, telegramUsername)— валидирует токен, проставляетTelegramUserId/TelegramUsername/TelegramLinkedAt, гасит токен. - Бот отвечает «✅ Telegram успешно привязан к вашему аккаунту» (или текст ошибки). Сайт узнаёт об успехе поллингом статуса активации/профиля.
Инварианты: один TelegramUserId ↔ один аккаунт; токен одноразовый и истекает.
Флоу 2 — Passwordless-вход через бота
Предусловие: Telegram уже привязан к аккаунту.
- На сайте «Войти через Telegram» →
POST /api/auth/telegram/login-request→{ requestId, deepLink, expiresAt }. Сайт начинает поллитьGET /api/auth/telegram/login-request/{requestId}. - Пользователь открывает
https://t.me/<bot>?start=login_<requestId>→ бот поfrom.idнаходит привязанный аккаунт (если не найден — просит сначала привязать) и показывает сообщение «Кто-то пытается войти в PnvPanel через ваш аккаунт. Подтвердить вход?» с инлайн-кнопками «✅ Подтвердить вход» / «❌ Отклонить». - Подтверждение →
ApproveTelegramLoginCommand/RejectTelegramLoginCommand→ запрос переходит вApproved/Rejected. - Сайт по следующему поллингу получает результат: при
Approved—accessTokenв теле,refreshуже пришёл в httpOnly cookie (та же логика cookie, что и обычный логин, включаяSecure = request.IsHttps). Запрос помечаетсяConsumed.
Если Telegram не привязан — бот сразу сообщает «Сначала привяжите Telegram к аккаунту на сайте», подтвердить вход невозможно. Регистрация целиком через Telegram — вне MVP.
Флоу 3 — Просмотр конфигов в боте
- Привязанный пользователь:
/configs. - Бот вызывает
GetMyConfigsQuery(тот же, что и веб) от пользователя, найденного поTelegramUserId. - Ответ — обычное текстовое сообщение, по строке на конфиг:
• {Label ?? Location} ({Protocol}) — {Status}. Если конфигов нет — «У вас пока нет конфигов.» Ссылок, QR и кнопок здесь нет — за подключением пользователь идёт на сайт.
Флоу 4 — Обработка активации админом в боте
- Пользователь отправляет запрос активации (сайт:
POST /api/activation/request { comment }) →RequestActivationCommandHandlerшлёт SignalRactivationRequestedгруппеadminsи вызываетITelegramNotifier.NotifyAdminsActivationRequestedAsync(прямой вызов из хендлера, без диспетчера событий). - Каждому админу (по
Telegram__AdminTelegramUserIds) уходит сообщение с именем и комментарием заявителя и кнопками «✅ Активировать / ❌ Отклонить». - Нажатие →
ApproveActivationCommand/RejectActivationCommand(те же, что на сайте) → пользователь активируется/отклоняется, ему уходит realtimeuserActivated(только при одобрении) + Telegram-DM, если привязан; нажавшему админу приходит короткое подтверждение («✅ Пользователь активирован.»/«❌ Запрос отклонён.») — само сообщение с кнопками не редактируется. - Проверка прав — на стороне бота (
TrySetAdminCurrentUserAsync) перед вызовом команды: Telegram id должен быть вTelegram__AdminTelegramUserIds.
/requests — тот же список запросов по требованию (до 10 штук, Pending), с теми же кнопками; для
кого он доступен — та же проверка админ-id.
Команды и клавиатуры
| Команда / кнопка | Действие | Требует привязки |
|---|---|---|
/start |
Приветствие + справка по командам | нет |
/start link_<token> |
Привязка аккаунта по токену | нет |
/start login_<requestId> |
Подтверждение passwordless-входа (deep-link с сайта) | да |
/configs |
Текстовый список конфигов | да |
/unlink |
Отвязать Telegram от аккаунта | да |
/help |
Справка (то же сообщение, что /start) |
нет |
| «✅ Активировать»/«❌ Отклонить» | (admin) решение по конкретному запросу активации | админ по env |
/requests |
(admin) список ожидающих запросов активации (до 10) | админ по env |
Любой другой текст → «Не понимаю эту команду. /help — список команд.»
Безопасность
- Токены привязки и
requestIdвхода: высокоэнтропийные, короткоживущие, одноразовые (см.TelegramLinkToken/TelegramLoginRequestв domain-model.md — точный TTL не вынесен в отдельное конфигурируемое значение, см. 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):
"Telegram": {
"BotToken": "…", // секрет; пусто = бот не стартует
"BotUsername": "PnvPanelBot", // для deepLink; null/пусто -> deepLink в ответах API тоже null
"AdminTelegramUserIds": "123456789,987654321" // через запятую
// "PublicSiteUrl" — поле есть в TelegramOptions, но нигде не читается (мёртвый код,
// не задавай его — эффекта не будет)
}
Переменные окружения — Telegram__BotToken, Telegram__BotUsername, Telegram__AdminTelegramUserIds
(см. .env.example). Именно они авторизуют админ-кнопки в боте и определяют,
кому слать уведомления о запросах активации — не сидируются в БД и не связаны с учёткой
сид-админа (AdminSeed:*), это независимый список.
Сообщения бота не локализованы по языку пользователя — все тексты на русском независимо от языка интерфейса на сайте (в отличие от веба, где RU/EN переключаются). Синхронизация языка бота с вебом — не реализована, backlog.