package auth import ( "crypto/hmac" "crypto/rand" "crypto/sha256" "encoding/hex" "errors" "fmt" "os" "sort" "strconv" "strings" "time" "wish-list-api/pkg/entities" "wish-list-api/pkg/user" ) const MaxAuthAge = 86400 type TelegramAuthService interface { AuthenticateWithTelegram(data *entities.TelegramAuthRequest) (*entities.TokenPair, error) ValidateTelegramAuthData(data *entities.TelegramAuthRequest) error } type telegramAuthService struct { userService user.Service authService Service botToken string telegramAppUrl string } type TelegramAuthConfig struct { UserService user.Service AuthService Service BotToken string TelegramAppUrl string } func NewTelegramAuthService(config TelegramAuthConfig) TelegramAuthService { if config.BotToken == "" { config.BotToken = os.Getenv("TELEGRAM_BOT_TOKEN") } if config.TelegramAppUrl == "" { config.TelegramAppUrl = os.Getenv("TELEGRAM_APP_URL") } return &telegramAuthService{ userService: config.UserService, authService: config.AuthService, botToken: config.BotToken, telegramAppUrl: config.TelegramAppUrl, } } func (s *telegramAuthService) AuthenticateWithTelegram(data *entities.TelegramAuthRequest) (*entities.TokenPair, error) { err := s.ValidateTelegramAuthData(data) if err != nil { return nil, err } user, err := s.userService.GetUserByTelegramID(data.TelegramID) if err != nil { return nil, err } if user == nil { randomPassword, err := generateRandomPassword(16) if err != nil { return nil, err } email := fmt.Sprintf("telegram_%d@%s", data.TelegramID, s.telegramAppUrl) user = &entities.User{ Email: email, Password: randomPassword, TelegramID: data.TelegramID, TelegramUsername: data.Username, FirstName: data.FirstName, LastName: data.LastName, PhotoURL: data.PhotoURL, CreatedAt: time.Now(), UpdatedAt: time.Now(), LastLoginDate: time.Now(), } hashedPassword, err := s.authService.HashPassword(randomPassword) if err != nil { return nil, err } user.Password = hashedPassword user, err = s.userService.CreateUser(user) if err != nil { return nil, err } } else { updated := false if user.TelegramUsername != data.Username { user.TelegramUsername = data.Username updated = true } if user.FirstName != data.FirstName { user.FirstName = data.FirstName updated = true } if user.LastName != data.LastName { user.LastName = data.LastName updated = true } if user.PhotoURL != data.PhotoURL && data.PhotoURL != "" { user.PhotoURL = data.PhotoURL updated = true } user.LastLoginDate = time.Now() updated = true if updated { _, err = s.userService.UpdateUserTelegramData(user) if err != nil { return nil, err } } } loginRequest := &entities.LoginRequest{ Email: user.Email, } return s.authService.Login(loginRequest) } func (s *telegramAuthService) ValidateTelegramAuthData(data *entities.TelegramAuthRequest) error { now := time.Now().Unix() if now-data.AuthDate > MaxAuthAge { return errors.New("данные аутентификации Telegram устарели") } secretKey := generateSecretKey(s.botToken) dataCheckString := buildDataCheckString(data) hash := generateHash(dataCheckString, secretKey) if hash != data.Hash { return errors.New("неверная подпись данных аутентификации Telegram") } return nil } func generateSecretKey(botToken string) []byte { h := sha256.New() h.Write([]byte(botToken)) return h.Sum(nil) } func buildDataCheckString(data *entities.TelegramAuthRequest) string { params := map[string]string{ "auth_date": strconv.FormatInt(data.AuthDate, 10), "first_name": data.FirstName, "telegram_id": strconv.FormatInt(data.TelegramID, 10), } if data.LastName != "" { params["last_name"] = data.LastName } if data.Username != "" { params["username"] = data.Username } if data.PhotoURL != "" { params["photo_url"] = data.PhotoURL } var keys []string for k := range params { keys = append(keys, k) } sort.Strings(keys) var pairs []string for _, k := range keys { pairs = append(pairs, fmt.Sprintf("%s=%s", k, params[k])) } return strings.Join(pairs, "\n") } func generateHash(data string, secretKey []byte) string { h := hmac.New(sha256.New, secretKey) h.Write([]byte(data)) return hex.EncodeToString(h.Sum(nil)) } func generateRandomPassword(length int) (string, error) { bytes := make([]byte, length) _, err := rand.Read(bytes) if err != nil { return "", err } return hex.EncodeToString(bytes)[:length], nil }