238 lines
6.5 KiB
Go
238 lines
6.5 KiB
Go
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
|
||
}
|