package auth import ( "errors" "os" "strconv" "time" "wish-list-api/pkg/entities" "wish-list-api/pkg/user" "github.com/golang-jwt/jwt/v5" "golang.org/x/crypto/bcrypt" ) type Service interface { Login(credentials *entities.LoginRequest) (*entities.TokenPair, error) Register(userData *entities.RegisterRequest) (*entities.User, error) RefreshToken(refreshToken string) (*entities.TokenPair, error) ValidateToken(tokenString string) (*jwt.Token, error) GetUserIDFromToken(tokenString string) (string, error) HashPassword(password string) (string, error) ComparePasswords(hashedPassword string, plainPassword string) bool } const ( DefaultAccessTokenExpiry = 30 * 60 DefaultRefreshTokenExpiry = 7 * 24 * 3600 DefaultSecretKey = "94cb4ff3-2396-4903-9266-9cdd9b885767" ) type service struct { userService user.Service accessTokenExp int64 refreshTokenExp int64 accessTokenSecret string refreshTokenSecret string } type ServiceConfig struct { UserService user.Service AccessTokenExp int64 RefreshTokenExp int64 AccessTokenSecret string RefreshTokenSecret string } func NewService(config ServiceConfig) Service { if config.AccessTokenExp == 0 { if envExp := os.Getenv("ACCESS_TOKEN_EXPIRY"); envExp != "" { if exp, err := strconv.ParseInt(envExp, 10, 64); err == nil { config.AccessTokenExp = exp } else { config.AccessTokenExp = DefaultAccessTokenExpiry } } else { config.AccessTokenExp = DefaultAccessTokenExpiry } } if config.RefreshTokenExp == 0 { if envExp := os.Getenv("REFRESH_TOKEN_EXPIRY"); envExp != "" { if exp, err := strconv.ParseInt(envExp, 10, 64); err == nil { config.RefreshTokenExp = exp } else { config.RefreshTokenExp = DefaultRefreshTokenExpiry } } else { config.RefreshTokenExp = DefaultRefreshTokenExpiry } } if config.AccessTokenSecret == "" { if secret := os.Getenv("ACCESS_TOKEN_SECRET"); secret != "" { config.AccessTokenSecret = secret } else { config.AccessTokenSecret = DefaultSecretKey } } if config.RefreshTokenSecret == "" { if secret := os.Getenv("REFRESH_TOKEN_SECRET"); secret != "" { config.RefreshTokenSecret = secret } else { config.RefreshTokenSecret = DefaultSecretKey + "_refresh" } } return &service{ userService: config.UserService, accessTokenExp: config.AccessTokenExp, refreshTokenExp: config.RefreshTokenExp, accessTokenSecret: config.AccessTokenSecret, refreshTokenSecret: config.RefreshTokenSecret, } } func (s *service) Login(credentials *entities.LoginRequest) (*entities.TokenPair, error) { user, err := s.userService.GetUserByEmail(credentials.Email) if err != nil { return nil, errors.New("неверный email или пароль") } if !s.ComparePasswords(user.Password, credentials.Password) { return nil, errors.New("неверный email или пароль") } return s.generateTokenPair(user.ID.Hex(), user.Email) } func (s *service) Register(userData *entities.RegisterRequest) (*entities.User, error) { existingUser, _ := s.userService.GetUserByEmail(userData.Email) if existingUser != nil { return nil, errors.New("пользователь с таким email уже существует") } hashedPassword, err := s.HashPassword(userData.Password) if err != nil { return nil, err } newUser := &entities.User{ Email: userData.Email, Password: hashedPassword, } return s.userService.CreateUser(newUser) } func (s *service) RefreshToken(refreshToken string) (*entities.TokenPair, error) { token, err := jwt.Parse(refreshToken, func(token *jwt.Token) (interface{}, error) { if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { return nil, errors.New("неожиданный метод подписи") } return []byte(s.refreshTokenSecret), nil }) if err != nil { return nil, errors.New("недействительный refresh token") } if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid { if exp, ok := claims["exp"].(float64); ok { if time.Now().Unix() > int64(exp) { return nil, errors.New("refresh token истек") } } userID, ok := claims["user_id"].(string) if !ok { return nil, errors.New("недействительный refresh token") } user, err := s.userService.GetUser(userID) if err != nil { return nil, errors.New("пользователь не найден") } return s.generateTokenPair(user.ID.Hex(), user.Email) } return nil, errors.New("недействительный refresh token") } func (s *service) ValidateToken(tokenString string) (*jwt.Token, error) { return jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { return nil, errors.New("неожиданный метод подписи") } return []byte(s.accessTokenSecret), nil }) } func (s *service) GetUserIDFromToken(tokenString string) (string, error) { if len(tokenString) > 7 && tokenString[:7] == "Bearer " { tokenString = tokenString[7:] } token, err := s.ValidateToken(tokenString) if err != nil { return "", err } claims, ok := token.Claims.(jwt.MapClaims) if !ok { return "", errors.New("invalid token claims") } userID, ok := claims["user_id"].(string) if !ok { return "", errors.New("user_id not found in token") } return userID, nil } func (s *service) generateTokenPair(userID, email string) (*entities.TokenPair, error) { now := time.Now().Unix() accessClaims := jwt.MapClaims{ "user_id": userID, "email": email, "exp": now + s.accessTokenExp, } accessToken := jwt.NewWithClaims(jwt.SigningMethodHS256, accessClaims) accessTokenString, err := accessToken.SignedString([]byte(s.accessTokenSecret)) if err != nil { return nil, err } refreshClaims := jwt.MapClaims{ "user_id": userID, "exp": now + s.refreshTokenExp, } refreshToken := jwt.NewWithClaims(jwt.SigningMethodHS256, refreshClaims) refreshTokenString, err := refreshToken.SignedString([]byte(s.refreshTokenSecret)) if err != nil { return nil, err } return &entities.TokenPair{ AccessToken: accessTokenString, RefreshToken: refreshTokenString, }, nil } func (s *service) HashPassword(password string) (string, error) { bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) return string(bytes), err } func (s *service) ComparePasswords(hashedPassword string, plainPassword string) bool { err := bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(plainPassword)) return err == nil }