- 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)
170 lines
4.9 KiB
Go
170 lines
4.9 KiB
Go
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 |