0302-добавил куки и сломал десктоп авторизацию.

сложно поддерживать однояйцевых близнецов - desktop и TMA, подготовил к рефакторингу структуры
This commit is contained in:
2026-02-03 09:32:02 +03:00
parent 88620f3fb6
commit ea1e5bbf6a
14 changed files with 547 additions and 86 deletions

View File

@@ -1,34 +1,33 @@
package handlers
import (
"fmt"
"net/http"
"rmser/internal/domain/account"
"rmser/internal/services/auth"
"rmser/pkg/logger"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
"github.com/google/uuid"
)
// AuthHandler обрабатывает HTTP запросы авторизации
type AuthHandler struct {
service *auth.Service
botUsername string
authService *auth.Service
accountService account.Repository
botUsername string
}
// NewAuthHandler создает новый экземпляр AuthHandler
func NewAuthHandler(s *auth.Service, botUsername string) *AuthHandler {
return &AuthHandler{service: s, botUsername: botUsername}
func NewAuthHandler(s *auth.Service, accountRepo account.Repository, botUsername string) *AuthHandler {
return &AuthHandler{authService: s, accountService: accountRepo, botUsername: botUsername}
}
// InitDesktopAuth инициализирует desktop авторизацию
// POST /api/auth/init-desktop
func (h *AuthHandler) InitDesktopAuth(c *gin.Context) {
// Вызываем сервис для генерации session_id
sessionID, err := h.service.InitDesktopAuth()
sessionID, err := h.authService.InitDesktopAuth()
if err != nil {
logger.Log.Error("Ошибка инициализации desktop авторизации", zap.Error(err))
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Ошибка инициализации авторизации",
})
@@ -36,12 +35,7 @@ func (h *AuthHandler) InitDesktopAuth(c *gin.Context) {
}
// Формируем QR URL для Telegram бота
qrURL := fmt.Sprintf("https://t.me/%s?start=auth_%s", h.botUsername, sessionID)
logger.Log.Info("Desktop авторизация инициализирована",
zap.String("session_id", sessionID),
zap.String("qr_url", qrURL),
)
qrURL := "https://t.me/" + h.botUsername + "?start=auth_" + sessionID
// Возвращаем ответ с session_id и qr_url
c.JSON(http.StatusOK, gin.H{
@@ -49,3 +43,56 @@ func (h *AuthHandler) InitDesktopAuth(c *gin.Context) {
"qr_url": qrURL,
})
}
// CreateSession создает HTTP сессию и устанавливает HttpOnly Cookie
func (h *AuthHandler) CreateSession(c *gin.Context) {
userID, exists := c.Get("userID")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{"error": "user not found in context"})
return
}
userAgent := c.Request.UserAgent()
ip := c.ClientIP()
session, err := h.authService.CreateWebSession(userID.(uuid.UUID), userAgent, ip)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// Устанавливаем HttpOnly Cookie на 7 дней
c.SetCookie("rmser_session", session.RefreshToken, 7*24*60*60, "/", "", false, true)
c.JSON(http.StatusOK, gin.H{"status": "ok"})
}
// GetMe возвращает информацию о текущем пользователе
func (h *AuthHandler) GetMe(c *gin.Context) {
userID, exists := c.Get("userID")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{"error": "user not found"})
return
}
user, err := h.accountService.GetUserByID(userID.(uuid.UUID))
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, user)
}
// Logout удаляет сессию и очищает cookie
func (h *AuthHandler) Logout(c *gin.Context) {
token, err := c.Cookie("rmser_session")
if err == nil && token != "" {
h.authService.DeleteSession(token)
}
// Очищаем куку
c.SetCookie("rmser_session", "", -1, "/", "", false, true)
c.JSON(http.StatusOK, gin.H{"status": "ok"})
}

View File

@@ -26,27 +26,28 @@ type Claims struct {
}
// AuthMiddleware проверяет JWT токен (Desktop) или initData от Telegram
func AuthMiddleware(accountRepo account.Repository, botToken string, secretKey string, maintenanceMode bool, devIDs []int64) gin.HandlerFunc {
func AuthMiddleware(accountRepo account.Repository, authService interface {
ValidateAndExtendSession(token string) (*account.User, error)
}, botToken string, secretKey string, maintenanceMode bool, devIDs []int64) gin.HandlerFunc {
return func(c *gin.Context) {
// 1. Извлекаем данные авторизации
authHeader := c.GetHeader("Authorization")
var authData string
// 1. Пробуем извлечь токен из Authorization header
tokenString := extractToken(c)
if strings.HasPrefix(authHeader, "Bearer ") {
authData = strings.TrimPrefix(authHeader, "Bearer ")
} else {
// Оставляем лазейку для отладки ТОЛЬКО если это не production режим
// В реальности лучше всегда требовать подпись
authData = c.Query("_auth")
// 2. Если нет токена в header, проверяем куку rmser_session
if tokenString == "" {
cookie, err := c.Cookie("rmser_session")
if err == nil && cookie != "" {
tokenString = cookie
}
}
if authData == "" {
if tokenString == "" {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Авторизация отклонена: отсутствует токен или подпись"})
return
}
// 2. Попытка 1: Проверяем JWT токен (Desktop)
token, err := jwt.ParseWithClaims(authData, &Claims{}, func(token *jwt.Token) (interface{}, error) {
// 3. Попытка 1: Проверяем JWT токен (Desktop)
token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("неожиданный метод подписи: %v", token.Header["alg"])
}
@@ -78,15 +79,42 @@ func AuthMiddleware(accountRepo account.Repository, botToken string, secretKey s
}
}
// 3. Попытка 2: Проверяем Telegram InitData
isValid, err := verifyTelegramInitData(authData, botToken)
// 4. Попытка 2: Проверяем сессию через authService (HttpOnly Cookie)
// Важно: проверяем длину, чтобы не слать initData (длинную строку) в БД как токен сессии
if authService != nil && len(tokenString) <= 128 {
user, err := authService.ValidateAndExtendSession(tokenString)
if err == nil && user != nil {
// Сессия валидна и продлена
if maintenanceMode {
isDev := false
for _, devID := range devIDs {
if user.TelegramID == devID {
isDev = true
break
}
}
if !isDev {
c.AbortWithStatusJSON(503, gin.H{"error": "maintenance_mode", "message": "Сервис на обслуживании"})
return
}
}
c.Set("userID", user.ID)
c.Set("telegramID", user.TelegramID)
c.Next()
return
}
}
// 5. Попытка 3: Проверяем Telegram InitData
isValid, err := verifyTelegramInitData(tokenString, botToken)
if !isValid || err != nil {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Критическая ошибка безопасности: недействительный токен или подпись"})
return
}
// Извлекаем User ID из проверенных данных
values, _ := url.ParseQuery(authData)
values, _ := url.ParseQuery(tokenString)
userJSON := values.Get("user")
// Извлекаем id вручную из JSON-подобной строки или через простой парсинг
@@ -125,6 +153,16 @@ func AuthMiddleware(accountRepo account.Repository, botToken string, secretKey s
}
}
// extractToken извлекает токен из Authorization header
func extractToken(c *gin.Context) string {
authHeader := c.GetHeader("Authorization")
if strings.HasPrefix(authHeader, "Bearer ") {
return strings.TrimPrefix(authHeader, "Bearer ")
}
// Оставляем лазейку для отладки ТОЛЬКО если это не production режим
return c.Query("_auth")
}
// verifyTelegramInitData реализует алгоритм проверки Telegram
func verifyTelegramInitData(initData, token string) (bool, error) {
values, err := url.ParseQuery(initData)