# 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 (при написании отзывов). Это **не используется для авторизации** — только для отображения. Бэкенду ничего делать не нужно.