Files
marketplaces/docs/TELEGRAM_BOT_INTEGRATION.md

19 KiB
Raw Blame History

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 для хранения сессий:

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

Что должен сделать бот

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

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 открывается в браузере пользователя (по клику на кнопку в боте).

// 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).

// 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)

Фронтенд ожидает точно эти поля:

{
  "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

// 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-заголовки.

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:

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 (для начала)

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)
        }
    }
}

Переменные окружения

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. Запустить бот и сервер

BOT_TOKEN=... go run .

2. Открыть маркетплейс, нажать "Войти"

Фронтенд откроет https://t.me/DexarSupport_bot?start=auth_...

3. В Telegram — нажать "Start" → кнопку "Войти на сайт"

Бот создаёт сессию, кнопка ведёт на /auth/telegram/callback?token=...

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}  ← проверка статуса оплаты

Формат данных корзины:

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