mirror of
https://github.com/serty2005/rmser.git
synced 2026-02-04 19:02:33 -06:00
2801-есть десктоп-версия. реализован ws для авторизации через тг-бота
This commit is contained in:
51
internal/transport/http/handlers/auth.go
Normal file
51
internal/transport/http/handlers/auth.go
Normal 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,
|
||||
})
|
||||
}
|
||||
@@ -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": "Пользователь не зарегистрирован. Начните диалог с ботом."})
|
||||
|
||||
@@ -19,6 +19,7 @@ import (
|
||||
"rmser/config"
|
||||
"rmser/internal/domain/account"
|
||||
"rmser/internal/infrastructure/rms"
|
||||
"rmser/internal/services/auth"
|
||||
"rmser/internal/services/billing"
|
||||
draftsService "rmser/internal/services/drafts"
|
||||
"rmser/internal/services/ocr"
|
||||
@@ -47,6 +48,7 @@ type Bot struct {
|
||||
rmsFactory *rms.Factory
|
||||
cryptoManager *crypto.CryptoManager
|
||||
draftsService *draftsService.Service
|
||||
authService *auth.Service
|
||||
draftEditor *DraftEditor
|
||||
|
||||
fsm *StateManager
|
||||
@@ -69,6 +71,7 @@ func NewBot(
|
||||
rmsFactory *rms.Factory,
|
||||
cryptoManager *crypto.CryptoManager,
|
||||
draftsService *draftsService.Service,
|
||||
authService *auth.Service,
|
||||
maintenanceMode bool,
|
||||
devIDs []int64,
|
||||
) (*Bot, error) {
|
||||
@@ -105,6 +108,7 @@ func NewBot(
|
||||
rmsFactory: rmsFactory,
|
||||
cryptoManager: cryptoManager,
|
||||
draftsService: draftsService,
|
||||
authService: authService,
|
||||
fsm: NewStateManager(),
|
||||
adminIDs: admins,
|
||||
devIDs: devs,
|
||||
@@ -198,6 +202,29 @@ func (bot *Bot) registrationMiddleware(next tele.HandlerFunc) tele.HandlerFunc {
|
||||
|
||||
func (bot *Bot) handleStartCommand(c tele.Context) error {
|
||||
payload := c.Message().Payload
|
||||
|
||||
// Обработка desktop авторизации
|
||||
if payload != "" && strings.HasPrefix(payload, "auth_") {
|
||||
sessionID := strings.TrimPrefix(payload, "auth_")
|
||||
telegramID := c.Sender().ID
|
||||
|
||||
logger.Log.Info("Обработка desktop авторизации",
|
||||
zap.String("session_id", sessionID),
|
||||
zap.Int64("telegram_id", telegramID),
|
||||
)
|
||||
|
||||
if err := bot.authService.ConfirmDesktopAuth(sessionID, telegramID); err != nil {
|
||||
logger.Log.Error("Ошибка подтверждения desktop авторизации",
|
||||
zap.String("session_id", sessionID),
|
||||
zap.Int64("telegram_id", telegramID),
|
||||
zap.Error(err),
|
||||
)
|
||||
return c.Send("❌ Ошибка авторизации. Попробуйте снова.", tele.ModeHTML)
|
||||
}
|
||||
|
||||
return c.Send("✅ Авторизация успешна! Вы можете вернуться в приложение.", tele.ModeHTML)
|
||||
}
|
||||
|
||||
if payload != "" && strings.HasPrefix(payload, "invite_") {
|
||||
return bot.handleInviteLink(c, strings.TrimPrefix(payload, "invite_"))
|
||||
}
|
||||
|
||||
129
internal/transport/ws/server.go
Normal file
129
internal/transport/ws/server.go
Normal file
@@ -0,0 +1,129 @@
|
||||
package ws
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gorilla/websocket"
|
||||
"go.uber.org/zap"
|
||||
"rmser/internal/domain/account"
|
||||
"rmser/pkg/logger"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
clients map[string]*websocket.Conn // session_id -> conn
|
||||
register chan *clientParams
|
||||
unregister chan string
|
||||
mu sync.RWMutex
|
||||
upgrader websocket.Upgrader
|
||||
}
|
||||
|
||||
type clientParams struct {
|
||||
sessionID string
|
||||
conn *websocket.Conn
|
||||
}
|
||||
|
||||
func NewServer() *Server {
|
||||
return &Server{
|
||||
clients: make(map[string]*websocket.Conn),
|
||||
register: make(chan *clientParams),
|
||||
unregister: make(chan string),
|
||||
upgrader: websocket.Upgrader{
|
||||
CheckOrigin: func(r *http.Request) bool { return true }, // Allow All CORS
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) Run() {
|
||||
for {
|
||||
select {
|
||||
case params := <-s.register:
|
||||
s.mu.Lock()
|
||||
// Если уже есть соединение с таким ID, закрываем старое
|
||||
if old, ok := s.clients[params.sessionID]; ok {
|
||||
old.Close()
|
||||
}
|
||||
s.clients[params.sessionID] = params.conn
|
||||
s.mu.Unlock()
|
||||
logger.Log.Info("WS Client connected", zap.String("session_id", params.sessionID))
|
||||
|
||||
case sessionID := <-s.unregister:
|
||||
s.mu.Lock()
|
||||
if conn, ok := s.clients[sessionID]; ok {
|
||||
conn.Close()
|
||||
delete(s.clients, sessionID)
|
||||
}
|
||||
s.mu.Unlock()
|
||||
logger.Log.Info("WS Client disconnected", zap.String("session_id", sessionID))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// HandleConnections обрабатывает входящие WS запросы
|
||||
func (s *Server) HandleConnections(c *gin.Context) {
|
||||
sessionID := c.Query("session_id")
|
||||
if sessionID == "" {
|
||||
c.Status(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
conn, err := s.upgrader.Upgrade(c.Writer, c.Request, nil)
|
||||
if err != nil {
|
||||
logger.Log.Error("WS Upgrade error", zap.Error(err))
|
||||
return
|
||||
}
|
||||
|
||||
s.register <- &clientParams{sessionID: sessionID, conn: conn}
|
||||
|
||||
// Читаем сообщения, чтобы держать соединение открытым и ловить Disconnect
|
||||
go func() {
|
||||
defer func() {
|
||||
s.unregister <- sessionID
|
||||
}()
|
||||
for {
|
||||
if _, _, err := conn.NextReader(); err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// SendAuthSuccess отправляет токен и закрывает соединение (так как задача выполнена)
|
||||
func (s *Server) SendAuthSuccess(sessionID string, token string, user account.User) {
|
||||
s.mu.RLock()
|
||||
conn, ok := s.clients[sessionID]
|
||||
s.mu.RUnlock()
|
||||
|
||||
if !ok {
|
||||
logger.Log.Warn("WS Client not found for auth", zap.String("session_id", sessionID))
|
||||
return
|
||||
}
|
||||
|
||||
resp := map[string]interface{}{
|
||||
"event": "auth_success",
|
||||
"data": map[string]interface{}{
|
||||
"token": token,
|
||||
"user": map[string]interface{}{
|
||||
"id": user.ID,
|
||||
"telegram_id": user.TelegramID,
|
||||
"username": user.Username,
|
||||
"first_name": user.FirstName,
|
||||
"last_name": user.LastName,
|
||||
"photo_url": user.PhotoURL,
|
||||
"is_system_admin": user.IsSystemAdmin,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if err := conn.WriteJSON(resp); err != nil {
|
||||
logger.Log.Error("WS Write error", zap.Error(err))
|
||||
} else {
|
||||
logger.Log.Info("WS Auth sent successfully", zap.String("session_id", sessionID))
|
||||
}
|
||||
|
||||
// Даем время на доставку и закрываем
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
s.unregister <- sessionID
|
||||
}
|
||||
Reference in New Issue
Block a user