2801-есть десктоп-версия. реализован ws для авторизации через тг-бота

This commit is contained in:
2026-01-28 08:12:41 +03:00
parent a536b3ff3c
commit b99e328d35
26 changed files with 2258 additions and 82 deletions

View File

@@ -0,0 +1,51 @@
package handlers
import (
"fmt"
"net/http"
"rmser/internal/services/auth"
"rmser/pkg/logger"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
)
// AuthHandler обрабатывает HTTP запросы авторизации
type AuthHandler struct {
service *auth.Service
botUsername string
}
// NewAuthHandler создает новый экземпляр AuthHandler
func NewAuthHandler(s *auth.Service, botUsername string) *AuthHandler {
return &AuthHandler{service: s, botUsername: botUsername}
}
// InitDesktopAuth инициализирует desktop авторизацию
// POST /api/auth/init-desktop
func (h *AuthHandler) InitDesktopAuth(c *gin.Context) {
// Вызываем сервис для генерации session_id
sessionID, err := h.service.InitDesktopAuth()
if err != nil {
logger.Log.Error("Ошибка инициализации desktop авторизации", zap.Error(err))
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Ошибка инициализации авторизации",
})
return
}
// Формируем 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),
)
// Возвращаем ответ с session_id и qr_url
c.JSON(http.StatusOK, gin.H{
"session_id": sessionID,
"qr_url": qrURL,
})
}

View File

@@ -14,37 +14,79 @@ import (
"rmser/internal/domain/account"
"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt/v5"
"github.com/google/uuid"
)
// AuthMiddleware проверяет initData от Telegram
func AuthMiddleware(accountRepo account.Repository, botToken string, maintenanceMode bool, devIDs []int64) gin.HandlerFunc {
// Claims представляет JWT claims для токена авторизации
type Claims struct {
UserID uuid.UUID `json:"user_id"`
TelegramID int64 `json:"telegram_id"`
jwt.RegisteredClaims
}
// AuthMiddleware проверяет JWT токен (Desktop) или initData от Telegram
func AuthMiddleware(accountRepo account.Repository, botToken string, secretKey string, maintenanceMode bool, devIDs []int64) gin.HandlerFunc {
return func(c *gin.Context) {
// 1. Извлекаем данные авторизации
authHeader := c.GetHeader("Authorization")
var initData string
var authData string
if strings.HasPrefix(authHeader, "Bearer ") {
initData = strings.TrimPrefix(authHeader, "Bearer ")
authData = strings.TrimPrefix(authHeader, "Bearer ")
} else {
// Оставляем лазейку для отладки ТОЛЬКО если это не production режим
// В реальности лучше всегда требовать подпись
initData = c.Query("_auth")
authData = c.Query("_auth")
}
if initData == "" {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Авторизация отклонена: отсутствует подпись Telegram"})
if authData == "" {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Авторизация отклонена: отсутствует токен или подпись"})
return
}
// 2. Проверяем подпись (HMAC-SHA256)
isValid, err := verifyTelegramInitData(initData, botToken)
// 2. Попытка 1: Проверяем JWT токен (Desktop)
token, err := jwt.ParseWithClaims(authData, &Claims{}, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("неожиданный метод подписи: %v", token.Header["alg"])
}
return []byte(secretKey), nil
})
if err == nil && token.Valid {
// JWT токен валиден
if claims, ok := token.Claims.(*Claims); ok {
// Проверка режима обслуживания: если включен, разрешаем доступ только разработчикам
if maintenanceMode {
isDev := false
for _, devID := range devIDs {
if claims.TelegramID == devID {
isDev = true
break
}
}
if !isDev {
c.AbortWithStatusJSON(503, gin.H{"error": "maintenance_mode", "message": "Сервис на обслуживании"})
return
}
}
c.Set("userID", claims.UserID)
c.Set("telegramID", claims.TelegramID)
c.Next()
return
}
}
// 3. Попытка 2: Проверяем Telegram InitData
isValid, err := verifyTelegramInitData(authData, botToken)
if !isValid || err != nil {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Критическая ошибка безопасности: поддельная подпись"})
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Критическая ошибка безопасности: недействительный токен или подпись"})
return
}
// 3. Извлекаем User ID из проверенных данных
values, _ := url.ParseQuery(initData)
// Извлекаем User ID из проверенных данных
values, _ := url.ParseQuery(authData)
userJSON := values.Get("user")
// Извлекаем id вручную из JSON-подобной строки или через простой парсинг
@@ -70,7 +112,7 @@ func AuthMiddleware(accountRepo account.Repository, botToken string, maintenance
}
}
// 4. Ищем пользователя в БД
// Ищем пользователя в БД
user, err := accountRepo.GetUserByTelegramID(tgID)
if err != nil {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Пользователь не зарегистрирован. Начните диалог с ботом."})