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

238 lines
6.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
}