diff --git a/docs/TELEGRAM_BOT_INTEGRATION.md b/docs/TELEGRAM_BOT_INTEGRATION.md new file mode 100644 index 0000000..7bc5761 --- /dev/null +++ b/docs/TELEGRAM_BOT_INTEGRATION.md @@ -0,0 +1,489 @@ +# Telegram-бот авторизация — Руководство по реализации + +> **Цель документа**: пошаговая инструкция для Go-разработчика. После выполнения всех шагов — авторизация через Telegram-бота будет работать. + +--- + +## Статус + +| Компонент | Готов? | +|-----------|--------| +| Frontend (Angular) — диалог входа, поллинг, сессия, корзина | ✅ Готов | +| Telegram бот (обработка `/start`) | ❌ Нужно сделать | +| Backend — 3 HTTP-эндпоинта авторизации | ❌ Нужно сделать | +| Хранилище сессий (Redis / PostgreSQL / in-memory) | ❌ Нужно сделать | +| CORS для cookie-based запросов | ❌ Нужно настроить | + +--- + +## Архитектура + +``` +┌──────────┐ 1. t.me/Bot?start=auth_token ┌──────────────┐ +│ │ ──────────────────────────────────────>│ Telegram │ +│ Браузер │ │ (облако) │ +│ │ 2. /start auth_{token} │ │ +│ │ └──────┬───────┘ +│ │ │ +│ │ v +│ │ ┌──────────────┐ +│ │ 4. GET /auth/session │ Go Backend │ +│ │ (Cookie) — поллинг 3 сек │ │ +│ │ ──────────────────────────────────────>│ - Bot handler│ +│ │ │ - Auth API │ +│ │ 5. { sessionId, displayName, ... } │ - Sessions │ +│ │ <─────────────────────────────────────│ │ +│ │ └──────┬───────┘ +│ │ 3. GET /auth/telegram/callback │ +│ │ + Set-Cookie: session=... ◄──────┘ +│ │ <───────────────────────────────────── +└──────────┘ +``` + +--- + +## Шаг 1: Создать бота в @BotFather + +1. Открыть https://t.me/BotFather +2. `/newbot` (или использовать существующего) +3. Сохранить **BOT_TOKEN** — он понадобится в шаге 3 + +Нужны два бота: + +| Бренд | Username бота | Домен фронтенда | API сервер | +|-------|---------------|------------------|------------| +| Dexar | `DexarSupport_bot` | `dexarmarket.ru` | `api.dexarmarket.ru:445` | +| Novo | `novomarket_bot` | `novo.market` | `api.novo.market:444` | + +--- + +## Шаг 2: Хранилище сессий + +Нужна таблица или Map для хранения сессий: + +```go +type Session struct { + SessionID string `json:"sessionId"` + TelegramUserID int64 `json:"telegramUserId"` + Username *string `json:"username"` // может быть null + DisplayName string `json:"displayName"` + Active bool `json:"active"` + ExpiresAt time.Time `json:"expiresAt"` +} +``` + +Можно использовать: +- **Redis** (рекомендуется для продакшена) — `SET session:{id} {json} EX 86400` +- **PostgreSQL** — таблица `auth_sessions` +- **sync.Map** — для MVP/тестирования (не переживёт рестарт) + +**Время жизни сессии:** 24 часа (фронтенд перепроверяет за 60 сек до `expiresAt`). + +--- + +## Шаг 3: Обработчик команды `/start` в боте + +### Что получает бот + +Когда пользователь кликает ссылку: +``` +https://t.me/DexarSupport_bot?start=auth_https%3A%2F%2Fapi.dexarmarket.ru%3A445%2Fauth%2Ftelegram%2Fcallback +``` + +Бот получает Update с `Message.Text`: +``` +/start auth_https%3A%2F%2Fapi.dexarmarket.ru%3A445%2Fauth%2Ftelegram%2Fcallback +``` + +### Что должен сделать бот + +```go +func handleStart(update tgbotapi.Update) { + text := update.Message.Text + user := update.Message.From + + // 1. Извлечь callback URL + if !strings.HasPrefix(text, "/start auth_") { + // Обычный /start — показать приветствие + return + } + callbackURL, _ := url.QueryUnescape(strings.TrimPrefix(text, "/start auth_")) + + // 2. Создать сессию + session := Session{ + SessionID: uuid.New().String(), + TelegramUserID: user.ID, + Username: stringPtr(user.UserName), // может быть "" + DisplayName: buildDisplayName(user), // "Имя Фамилия" + Active: true, + ExpiresAt: time.Now().Add(24 * time.Hour), + } + saveSession(session) // сохранить в Redis/DB + + // 3. Отправить пользователю кнопку "Войти" + // Кнопка открывает callback URL с токеном сессии + loginURL := callbackURL + "?token=" + session.SessionID + + msg := tgbotapi.NewMessage(update.Message.Chat.ID, "Нажмите кнопку чтобы войти:") + msg.ReplyMarkup = tgbotapi.NewInlineKeyboardMarkup( + tgbotapi.NewInlineKeyboardRow( + tgbotapi.NewInlineKeyboardButtonURL("🔐 Войти на сайт", loginURL), + ), + ) + bot.Send(msg) +} + +func buildDisplayName(user *tgbotapi.User) string { + name := user.FirstName + if user.LastName != "" { + name += " " + user.LastName + } + return name +} +``` + +> **Важно**: Telegram ограничивает параметр `start` до 64 символов. URL callback'а ~62 символа — проходит впритык. Альтернатива — не передавать callback URL в параметре, а захардкодить его в боте (рекомендуется). + +### Рекомендация: захардкодить callback URL + +```go +const callbackURL = "https://api.dexarmarket.ru:445/auth/telegram/callback" + +func handleStart(update tgbotapi.Update) { + text := update.Message.Text + if !strings.HasPrefix(text, "/start auth") { + return + } + // ... создать сессию ... + loginURL := callbackURL + "?token=" + session.SessionID + // ... отправить кнопку ... +} +``` + +Тогда фронтенд может упростить ссылку до: +``` +https://t.me/DexarSupport_bot?start=auth +``` + +--- + +## Шаг 4: HTTP-эндпоинт `GET /auth/telegram/callback` + +Этот URL открывается в **браузере пользователя** (по клику на кнопку в боте). + +```go +// GET /auth/telegram/callback?token={sessionId} +func handleTelegramCallback(w http.ResponseWriter, r *http.Request) { + token := r.URL.Query().Get("token") + if token == "" { + http.Error(w, "missing token", 400) + return + } + + // 1. Проверить что сессия существует + session, err := getSession(token) + if err != nil || !session.Active { + http.Error(w, "invalid or expired token", 401) + return + } + + // 2. Установить HttpOnly cookie + http.SetCookie(w, &http.Cookie{ + Name: "dx_session", + Value: session.SessionID, + Path: "/", + HttpOnly: true, + Secure: true, // обязательно для HTTPS + SameSite: http.SameSiteNoneMode, // кросс-доменные запросы + MaxAge: 86400, // 24 часа + Domain: ".dexarmarket.ru", // доступна для поддоменов + }) + + // 3. Редирект на сайт + http.Redirect(w, r, "https://dexarmarket.ru", http.StatusFound) +} +``` + +### Про SameSite и Domain + +| Параметр | Значение | Почему | +|----------|----------|--------| +| `SameSite` | `None` | Frontend на `dexarmarket.ru` шлёт запросы к `api.dexarmarket.ru:445` — разные origins | +| `Secure` | `true` | Обязательно при `SameSite=None` | +| `Domain` | `.dexarmarket.ru` | Cookie доступна и на `dexarmarket.ru` и на `api.dexarmarket.ru` | +| `HttpOnly` | `true` | Недоступна из JavaScript — защита от XSS | + +--- + +## Шаг 5: HTTP-эндпоинт `GET /auth/session` + +Фронтенд вызывает каждые 3 секунды с `withCredentials: true` (браузер подставляет cookie). + +```go +// GET /auth/session +func handleGetSession(w http.ResponseWriter, r *http.Request) { + cookie, err := r.Cookie("dx_session") + if err != nil { + http.Error(w, "unauthorized", 401) + return + } + + session, err := getSession(cookie.Value) + if err != nil { + http.Error(w, "unauthorized", 401) + return + } + + // Проверить не истекла ли + if time.Now().After(session.ExpiresAt) { + session.Active = false + saveSession(session) // обновить в хранилище + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(session) +} +``` + +### Формат ответа (200) + +Фронтенд ожидает **точно эти поля**: + +```json +{ + "sessionId": "550e8400-e29b-41d4-a716-446655440000", + "telegramUserId": 123456789, + "username": "ivan_petrov", + "displayName": "Иван Петров", + "active": true, + "expiresAt": "2026-03-25T14:30:00Z" +} +``` + +| Поле | Тип | Обязательно | Примечание | +|------|-----|-------------|------------| +| `sessionId` | string (UUID) | да | Используется для websession эндпоинтов покупки | +| `telegramUserId` | number | да | Telegram user ID | +| `username` | string / null | нет | Telegram @username (может отсутствовать) | +| `displayName` | string | да | "Имя Фамилия" — показывается в UI | +| `active` | boolean | да | `false` = сессия истекла | +| `expiresAt` | string (ISO 8601) | да | Время истечения. Фронтенд перепроверяет за 60 сек до | + +### Ответ при ошибке (401) + +Любой HTTP статус не-200 → фронтенд считает "не авторизован". + +--- + +## Шаг 6: HTTP-эндпоинт `POST /auth/logout` + +```go +// POST /auth/logout +func handleLogout(w http.ResponseWriter, r *http.Request) { + cookie, err := r.Cookie("dx_session") + if err == nil { + deleteSession(cookie.Value) + } + + // Очистить cookie + http.SetCookie(w, &http.Cookie{ + Name: "dx_session", + Value: "", + Path: "/", + HttpOnly: true, + Secure: true, + SameSite: http.SameSiteNoneMode, + MaxAge: -1, // удалить + Domain: ".dexarmarket.ru", + }) + + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(`{"message":"ok"}`)) +} +``` + +--- + +## Шаг 7: CORS + +Фронтенд шлёт запросы с `withCredentials: true` (cookie). Бэкенд **обязан** вернуть правильные CORS-заголовки. + +```go +func corsMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + origin := r.Header.Get("Origin") + + // Только для наших доменов + allowed := map[string]bool{ + "https://dexarmarket.ru": true, + "https://www.dexarmarket.ru": true, + "https://novo.market": true, + "https://www.novo.market": true, + "http://localhost:4200": true, // для разработки + } + + if allowed[origin] { + w.Header().Set("Access-Control-Allow-Origin", origin) // НЕ "*" — запрещено с credentials + w.Header().Set("Access-Control-Allow-Credentials", "true") + w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS") + w.Header().Set("Access-Control-Allow-Headers", "Content-Type") + } + + if r.Method == "OPTIONS" { + w.WriteHeader(200) + return + } + + next.ServeHTTP(w, r) + }) +} +``` + +> **Критично**: `Access-Control-Allow-Origin` **не может быть** `"*"` если используется `withCredentials`. Нужно вернуть конкретный origin. + +--- + +## Шаг 8: Роутинг — добавить к существующему API + +Эти 3 эндпоинта добавляются к существующему Go серверу на `api.dexarmarket.ru:445`: + +```go +mux := http.NewServeMux() + +// Существующие эндпоинты +mux.HandleFunc("GET /items/{id}", handleGetItem) +mux.HandleFunc("GET /category", handleGetCategories) +mux.HandleFunc("POST /websession/{id}", handleWebSession) +mux.HandleFunc("POST /websession/{id}/qr", handleCreateQR) +mux.HandleFunc("GET /websession/{id}/{qrId}", handleCheckPayment) + +// НОВЫЕ — авторизация +mux.HandleFunc("GET /auth/session", handleGetSession) +mux.HandleFunc("GET /auth/telegram/callback", handleTelegramCallback) +mux.HandleFunc("POST /auth/logout", handleLogout) + +// Обернуть в CORS middleware +handler := corsMiddleware(mux) +http.ListenAndServeTLS(":445", "cert.pem", "key.pem", handler) +``` + +--- + +## Шаг 9: Запуск Telegram бота + +Бот может работать в режиме **long polling** (проще) или **webhook** (production). + +### Long Polling (для начала) + +```go +func main() { + bot, _ := tgbotapi.NewBotAPI(os.Getenv("BOT_TOKEN")) + + u := tgbotapi.NewUpdate(0) + u.Timeout = 60 + updates := bot.GetUpdatesChan(u) + + for update := range updates { + if update.Message != nil && strings.HasPrefix(update.Message.Text, "/start") { + handleStart(update) + } + } +} +``` + +### Переменные окружения + +```env +BOT_TOKEN=123456:ABC-DEF... # от @BotFather +CALLBACK_URL=https://api.dexarmarket.ru:445/auth/telegram/callback +FRONTEND_URL=https://dexarmarket.ru +SESSION_TTL=24h +REDIS_URL=localhost:6379 # если используете Redis +``` + +--- + +## Шаг 10: Проверка — как тестировать + +### 1. Запустить бот и сервер + +```bash +BOT_TOKEN=... go run . +``` + +### 2. Открыть маркетплейс, нажать "Войти" + +Фронтенд откроет `https://t.me/DexarSupport_bot?start=auth_...` + +### 3. В Telegram — нажать "Start" → кнопку "Войти на сайт" + +Бот создаёт сессию, кнопка ведёт на `/auth/telegram/callback?token=...` + +### 4. Браузер получает cookie, редиректит на маркетплейс + +### 5. Фронтенд (поллинг каждые 3 сек) находит сессию → диалог закрывается + +### Логи для отладки + +Что проверять если не работает: + +| Проблема | Где смотреть | +|----------|-------------| +| Бот не отвечает | `BOT_TOKEN` правильный? Бот получает updates? | +| Кнопка "Войти" не открывается | URL `/auth/telegram/callback` доступен? SSL? | +| Cookie не устанавливается | `SameSite`, `Secure`, `Domain` правильные? HTTPS? | +| Фронтенд не находит сессию | CORS настроен? `Access-Control-Allow-Credentials: true`? | +| Сессия не находится | Хранилище работает? TTL не слишком маленький? | + +--- + +## Полный чеклист + +- [ ] Бот создан в @BotFather, токен получен +- [ ] `BOT_TOKEN` сохранён в переменных окружения сервера +- [ ] Хранилище сессий настроено (Redis / PostgreSQL / sync.Map) +- [ ] Обработчик `/start auth_...` в боте создаёт сессию и отправляет inline-кнопку +- [ ] `GET /auth/telegram/callback?token=...` устанавливает HttpOnly cookie и редиректит +- [ ] `GET /auth/session` читает cookie и возвращает JSON сессии (или 401) +- [ ] `POST /auth/logout` удаляет сессию и cookie +- [ ] CORS настроен для `dexarmarket.ru` / `novo.market` с `credentials: true` +- [ ] SSL сертификат установлен (обязательно для `Secure` cookie) +- [ ] Тест: клик "Войти" → Telegram → кнопка → cookie → сессия найдена ✅ + +--- + +## Как фронтенд использует `sessionId` после авторизации + +После успешного входа `sessionId` из ответа `/auth/session` используется для **покупки**: + +``` +POST /websession/{sessionId} ← синхронизация корзины +POST /websession/{sessionId}/qr ← создание QR для СБП +GET /websession/{sessionId}/{qrId} ← проверка статуса оплаты +``` + +Формат данных корзины: +```json +[ + { + "itemID": 123, + "quantity": 2, + "colour": "#ff0000", + "size": "XL", + "price": 1500 + } +] +``` + +> Цвет приходит от бэкенда как `0xff0000`, фронтенд конвертирует в `#ff0000`. +> Размер `"default"` — не показывается пользователю. + +--- + +## Дополнительно: Telegram CloudStorage + +Внутри Telegram Mini App корзина дополнительно сохраняется в `CloudStorage` (API Telegram). Это **автоматически** — бэкенду ничего делать не нужно. Работает только когда сайт открыт через Telegram. + +## Дополнительно: `initDataUnsafe` + +Фронтенд читает `window.Telegram.WebApp.initDataUnsafe.user` для отображения имени пользователя в UI (при написании отзывов). Это **не используется для авторизации** — только для отображения. Бэкенду ничего делать не нужно. diff --git a/src/app/pages/info/about/en/about-en.component.html b/src/app/pages/info/about/en/about-en.component.html index 0633a90..27d725b 100644 --- a/src/app/pages/info/about/en/about-en.component.html +++ b/src/app/pages/info/about/en/about-en.component.html @@ -1,10 +1,10 @@