buhapp-backend/internal/handlers/telegram.go
ga c6d219c51f Telegram WebApp support + consent endpoint
- Telegram bot (long polling)
- /api/v1/auth/telegram (validate initData, auto-create user)
- /api/v1/auth/consents (record acceptances)
- users.telegram_id column, GetByTelegramID/CreateWithTelegramID
- ValidateInitData (HMAC-SHA256 with WebAppData secret)
2026-08-20 17:43:20 +00:00

170 lines
4.9 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package handlers
import (
"net/http"
"strings"
"time"
"github.com/gofiber/fiber/v2"
"github.com/google/uuid"
"golang.org/x/crypto/bcrypt"
"github.com/buhapp/backend/internal/audit"
"github.com/buhapp/backend/internal/auth"
"github.com/buhapp/backend/internal/consent"
"github.com/buhapp/backend/internal/telegram"
"github.com/buhapp/backend/internal/users"
)
// TGHandler — обработчики для Telegram WebApp
type TGHandler struct {
Bot *telegram.Bot
Users *users.Repo
Auth *auth.Service
Consent *consent.Repo
Audit *audit.Repo
JWTSecret []byte
}
func NewTGHandler(bot *telegram.Bot, u *users.Repo, a *auth.Service, c *consent.Repo, au *audit.Repo, secret []byte) *TGHandler {
return &TGHandler{Bot: bot, Users: u, Auth: a, Consent: c, Audit: au, JWTSecret: secret}
}
type tgAuthRequest struct {
InitData string `json:"init_data"`
}
// TGAuth — логин/регистрация через Telegram WebApp initData
func (h *TGHandler) TGAuth(c *fiber.Ctx) error {
var req tgAuthRequest
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid JSON"})
}
if h.Bot == nil {
return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{"error": "telegram not configured"})
}
// Проверяем подпись
tgData, err := telegram.ValidateInitData(h.Bot.Token, req.InitData, 5*time.Minute)
if err != nil {
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "invalid init data: " + err.Error()})
}
if tgData.User == nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "no user in init data"})
}
tgID := tgData.User.ID
uid := telegram.UserIDFromTelegram(tgID)
// Получаем или создаём пользователя
u, err := h.Users.GetByTelegramID(c.UserContext(), tgID)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "db error"})
}
if u == nil {
// Регистрируем нового
name := strings.TrimSpace(tgData.User.FirstName)
if tgData.User.LastName != "" {
name += " " + tgData.User.LastName
}
if name == "" {
name = "User"
}
u = &users.User{
ID: uid,
Name: name,
IsVerified: false,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
if err := h.Users.CreateWithTelegramID(c.UserContext(), u, tgID); err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "create user failed: " + err.Error()})
}
// аудит
_ = h.Audit.Log(c.UserContext(), &audit.Event{
UserID: &uid, Action: "user.register_telegram",
IP: c.IP(), UserAgent: c.Get("User-Agent"),
})
} else {
// обновить last_seen_at
_ = h.Users.TouchLastSeen(c.UserContext(), u.ID)
}
// Выпускаем JWT
tokens, err := h.Auth.Generate(u.ID)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "token generation failed"})
}
return c.JSON(fiber.Map{
"user": u,
"tokens": tokens,
"tg": fiber.Map{
"id": tgData.User.ID,
"username": tgData.User.Username,
"first_name": tgData.User.FirstName,
"photo_url": tgData.User.PhotoURL,
},
})
}
// Webhook для бота — если будешь использовать webhook
func (h *TGHandler) BotWebhook(c *fiber.Ctx) error {
var u telegram.Update
if err := c.BodyParser(&u); err != nil {
return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid"})
}
h.handleUpdate(c.UserContext(), u)
return c.JSON(fiber.Map{"ok": true})
}
// ProcessUpdate — обработчик апдейтов (вызывается из bot loop)
func (h *TGHandler) ProcessUpdate(u telegram.Update) {
h.handleUpdate(nil, u)
}
func (h *TGHandler) handleUpdate(ctx interface{}, u telegram.Update) {
if u.Message != nil {
h.handleMessage(u.Message)
}
if u.Callback != nil {
h.handleCallback(u.Callback)
}
}
func (h *TGHandler) handleMessage(m *telegram.Message) {
if m.Text == "" || m.Chat == nil {
return
}
switch m.Text {
case "/start":
name := ""
if m.From != nil {
name = m.From.FirstName
}
h.Bot.SendMessage(m.Chat.ID,
"Привет"+ifNonEmpty(name, ", "+name)+"!\n\nBuhApp — приложение для поиска компании.\n\nНажми кнопку ниже, чтобы открыть:",
func(opts *telegram.MessageOptions) {
opts.ParseMode = "HTML"
opts.ReplyMarkup = telegram.InlineKeyboard([][]telegram.InlineButton{{
{Text: "🚀 Открыть BuhApp", WebApp: &telegram.WebAppInfo{URL: "https://app.buhapp.mygoodservice.ru"}},
}})
},
)
}
}
func (h *TGHandler) handleCallback(cb *telegram.Callback) {
_ = h.Bot.AnswerCallback(cb.ID, "Открывай Mini App!")
}
func ifNonEmpty(s, prefix string) string {
if s == "" {
return ""
}
return prefix
}
// bcrypt не используется здесь, оставлю для импорта
var _ = bcrypt.MinCost
var _ = uuid.Nil