wishli-api/pkg/auth/telegram.go
2025-03-23 20:05:51 +03:00

212 lines
4.6 KiB
Go

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
}