1. CORS whitelist (was '*'): only buhapp.mygoodservice.ru, t.me, web.telegram.org 2. Rate limit on /auth/login (10/min/IP) and /auth/register (5/hour/IP) 3. TGHandler: removed unused JWTSecret, added WebhookSecret, real secret check 4. Login: constant-time bcrypt on user enumeration (dummy hash) 5. TgUsername saved in users (was lost) 6. User.IsVerified=true for Telegram users 7. Register: 409 instead of 500 on duplicate email 8. Bumped version to 0.4.0
171 lines
5.0 KiB
Go
171 lines
5.0 KiB
Go
package handlers
|
||
|
||
import (
|
||
"strings"
|
||
"time"
|
||
|
||
"github.com/gofiber/fiber/v2"
|
||
|
||
"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
|
||
WebhookSecret string
|
||
}
|
||
|
||
func NewTGHandler(bot *telegram.Bot, u *users.Repo, a *auth.Service, c *consent.Repo, au *audit.Repo, webhookSecret string) *TGHandler {
|
||
return &TGHandler{Bot: bot, Users: u, Auth: a, Consent: c, Audit: au, WebhookSecret: webhookSecret}
|
||
}
|
||
|
||
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,
|
||
TgUsername: tgData.User.Username,
|
||
IsVerified: true, // подтверждён через Telegram
|
||
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,
|
||
},
|
||
})
|
||
}
|
||
|
||
// BotWebhook — webhook от Telegram. Проверяем Secret Token.
|
||
func (h *TGHandler) BotWebhook(c *fiber.Ctx) error {
|
||
// Проверка секрета (если задан)
|
||
if h.WebhookSecret != "" {
|
||
if c.Get("X-Telegram-Bot-Api-Secret-Token") != h.WebhookSecret {
|
||
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "invalid secret"})
|
||
}
|
||
}
|
||
var u telegram.Update
|
||
if err := c.BodyParser(&u); err != nil {
|
||
return c.Status(fiber.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://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
|
||
}
|