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 }