Files
QuoteForge/internal/services/auth.go
2026-01-26 18:30:45 +03:00

181 lines
4.3 KiB
Go

package services
import (
"errors"
"time"
"github.com/golang-jwt/jwt/v5"
"git.mchus.pro/mchus/quoteforge/internal/config"
"git.mchus.pro/mchus/quoteforge/internal/models"
"git.mchus.pro/mchus/quoteforge/internal/repository"
"golang.org/x/crypto/bcrypt"
)
var (
ErrInvalidCredentials = errors.New("invalid username or password")
ErrUserNotFound = errors.New("user not found")
ErrUserInactive = errors.New("user account is inactive")
ErrInvalidToken = errors.New("invalid token")
ErrTokenExpired = errors.New("token expired")
)
type AuthService struct {
userRepo *repository.UserRepository
config config.AuthConfig
}
func NewAuthService(userRepo *repository.UserRepository, cfg config.AuthConfig) *AuthService {
return &AuthService{
userRepo: userRepo,
config: cfg,
}
}
type TokenPair struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
ExpiresAt int64 `json:"expires_at"`
}
type Claims struct {
UserID uint `json:"user_id"`
Username string `json:"username"`
Role models.UserRole `json:"role"`
jwt.RegisteredClaims
}
func (s *AuthService) Login(username, password string) (*TokenPair, *models.User, error) {
user, err := s.userRepo.GetByUsername(username)
if err != nil {
return nil, nil, ErrInvalidCredentials
}
if !user.IsActive {
return nil, nil, ErrUserInactive
}
if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(password)); err != nil {
return nil, nil, ErrInvalidCredentials
}
tokens, err := s.generateTokenPair(user)
if err != nil {
return nil, nil, err
}
return tokens, user, nil
}
func (s *AuthService) RefreshTokens(refreshToken string) (*TokenPair, error) {
claims, err := s.ValidateToken(refreshToken)
if err != nil {
return nil, err
}
user, err := s.userRepo.GetByID(claims.UserID)
if err != nil {
return nil, ErrUserNotFound
}
if !user.IsActive {
return nil, ErrUserInactive
}
return s.generateTokenPair(user)
}
func (s *AuthService) ValidateToken(tokenString string) (*Claims, error) {
token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) {
return []byte(s.config.JWTSecret), nil
})
if err != nil {
if errors.Is(err, jwt.ErrTokenExpired) {
return nil, ErrTokenExpired
}
return nil, ErrInvalidToken
}
claims, ok := token.Claims.(*Claims)
if !ok || !token.Valid {
return nil, ErrInvalidToken
}
return claims, nil
}
func (s *AuthService) generateTokenPair(user *models.User) (*TokenPair, error) {
now := time.Now()
accessExpiry := now.Add(s.config.TokenExpiry)
refreshExpiry := now.Add(s.config.RefreshExpiry)
accessClaims := &Claims{
UserID: user.ID,
Username: user.Username,
Role: user.Role,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(accessExpiry),
IssuedAt: jwt.NewNumericDate(now),
Subject: user.Username,
},
}
accessToken := jwt.NewWithClaims(jwt.SigningMethodHS256, accessClaims)
accessTokenString, err := accessToken.SignedString([]byte(s.config.JWTSecret))
if err != nil {
return nil, err
}
refreshClaims := &Claims{
UserID: user.ID,
Username: user.Username,
Role: user.Role,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(refreshExpiry),
IssuedAt: jwt.NewNumericDate(now),
Subject: user.Username,
},
}
refreshToken := jwt.NewWithClaims(jwt.SigningMethodHS256, refreshClaims)
refreshTokenString, err := refreshToken.SignedString([]byte(s.config.JWTSecret))
if err != nil {
return nil, err
}
return &TokenPair{
AccessToken: accessTokenString,
RefreshToken: refreshTokenString,
ExpiresAt: accessExpiry.Unix(),
}, nil
}
func (s *AuthService) HashPassword(password string) (string, error) {
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return "", err
}
return string(hash), nil
}
func (s *AuthService) CreateUser(username, email, password string, role models.UserRole) (*models.User, error) {
hash, err := s.HashPassword(password)
if err != nil {
return nil, err
}
user := &models.User{
Username: username,
Email: email,
PasswordHash: hash,
Role: role,
IsActive: true,
}
if err := s.userRepo.Create(user); err != nil {
return nil, err
}
return user, nil
}