// Package service provides business logic for the application package service import ( "errors" "regexp" "time" "accounting-app/internal/config" "accounting-app/internal/models" "accounting-app/internal/repository" "github.com/golang-jwt/jwt/v5" "golang.org/x/crypto/bcrypt" ) // Auth service errors var ( ErrInvalidCredentials = errors.New("invalid email or password") ErrInvalidEmail = errors.New("invalid email format") ErrWeakPassword = errors.New("password must be at least 8 characters") ErrUserNotActive = errors.New("user account is not active") ErrInvalidToken = errors.New("invalid token") ErrTokenExpired = errors.New("token has expired") ErrUserExists = errors.New("user with this email already exists") ) // TokenClaims represents JWT token claims // Feature: api-interface-optimization // Validates: Requirements 12.3 type TokenClaims struct { UserID uint `json:"user_id"` Email string `json:"email"` jwt.RegisteredClaims } // TokenPair represents access and refresh tokens type TokenPair struct { AccessToken string `json:"access_token"` RefreshToken string `json:"refresh_token"` ExpiresIn int64 `json:"expires_in"` } // RegisterInput represents user registration input type RegisterInput struct { Email string `json:"email" binding:"required"` Password string `json:"password" binding:"required"` Username string `json:"username" binding:"required"` } // LoginInput represents user login input type LoginInput struct { Email string `json:"email" binding:"required"` Password string `json:"password" binding:"required"` } // AuthService handles authentication operations // Feature: api-interface-optimization // Validates: Requirements 12.1, 12.2, 12.3, 12.4, 12.5 type AuthService struct { userRepo *repository.UserRepository cfg *config.Config emailRegex *regexp.Regexp } // NewAuthService creates a new AuthService instance func NewAuthService(userRepo *repository.UserRepository, cfg *config.Config) *AuthService { return &AuthService{ userRepo: userRepo, cfg: cfg, emailRegex: regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`), } } // Register creates a new user account // Feature: api-interface-optimization // Validates: Requirements 12.1, 12.2, 12.5 func (s *AuthService) Register(input RegisterInput) (*models.User, *TokenPair, error) { // Validate email format if !s.ValidateEmail(input.Email) { return nil, nil, ErrInvalidEmail } // Validate password strength if len(input.Password) < 8 { return nil, nil, ErrWeakPassword } // Check if email already exists exists, err := s.userRepo.EmailExists(input.Email) if err != nil { return nil, nil, err } if exists { return nil, nil, ErrUserExists } // Hash password hashedPassword, err := bcrypt.GenerateFromPassword([]byte(input.Password), bcrypt.DefaultCost) if err != nil { return nil, nil, err } // Create user user := &models.User{ Email: input.Email, PasswordHash: string(hashedPassword), Username: input.Username, IsActive: true, } if err := s.userRepo.Create(user); err != nil { if errors.Is(err, repository.ErrUserEmailExists) { return nil, nil, ErrUserExists } return nil, nil, err } // Generate tokens tokens, err := s.generateTokenPair(user) if err != nil { return nil, nil, err } return user, tokens, nil } // Login authenticates a user and returns tokens // Feature: api-interface-optimization // Validates: Requirements 12.2 func (s *AuthService) Login(input LoginInput) (*models.User, *TokenPair, error) { // Get user by email user, err := s.userRepo.GetByEmail(input.Email) if err != nil { if errors.Is(err, repository.ErrUserNotFound) { return nil, nil, ErrInvalidCredentials } return nil, nil, err } // Check if user is active if !user.IsActive { return nil, nil, ErrUserNotActive } // Verify password if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(input.Password)); err != nil { return nil, nil, ErrInvalidCredentials } // Generate tokens tokens, err := s.generateTokenPair(user) if err != nil { return nil, nil, err } return user, tokens, nil } // RefreshToken generates new tokens using a refresh token // Feature: api-interface-optimization // Validates: Requirements 12.4 func (s *AuthService) RefreshToken(refreshToken string) (*TokenPair, error) { // Parse and validate refresh token claims, err := s.ValidateToken(refreshToken) if err != nil { return nil, err } // Get user user, err := s.userRepo.GetByID(claims.UserID) if err != nil { return nil, err } // Check if user is active if !user.IsActive { return nil, ErrUserNotActive } // Generate new tokens return s.generateTokenPair(user) } // ValidateToken validates a JWT token and returns claims // Feature: api-interface-optimization // Validates: Requirements 12.3 func (s *AuthService) ValidateToken(tokenString string) (*TokenClaims, error) { token, err := jwt.ParseWithClaims(tokenString, &TokenClaims{}, func(token *jwt.Token) (interface{}, error) { return []byte(s.cfg.JWTSecret), nil }) if err != nil { if errors.Is(err, jwt.ErrTokenExpired) { return nil, ErrTokenExpired } return nil, ErrInvalidToken } claims, ok := token.Claims.(*TokenClaims) if !ok || !token.Valid { return nil, ErrInvalidToken } return claims, nil } // ValidateEmail validates email format // Feature: api-interface-optimization // Validates: Requirements 12.1 (Property 10) func (s *AuthService) ValidateEmail(email string) bool { return s.emailRegex.MatchString(email) } // GetUserByID retrieves a user by ID func (s *AuthService) GetUserByID(id uint) (*models.User, error) { return s.userRepo.GetByID(id) } // generateTokenPair generates access and refresh tokens func (s *AuthService) generateTokenPair(user *models.User) (*TokenPair, error) { now := time.Now() // Generate access token accessClaims := &TokenClaims{ UserID: user.ID, Email: user.Email, RegisteredClaims: jwt.RegisteredClaims{ ExpiresAt: jwt.NewNumericDate(now.Add(s.cfg.JWTAccessExpiry)), IssuedAt: jwt.NewNumericDate(now), NotBefore: jwt.NewNumericDate(now), }, } accessToken := jwt.NewWithClaims(jwt.SigningMethodHS256, accessClaims) accessTokenString, err := accessToken.SignedString([]byte(s.cfg.JWTSecret)) if err != nil { return nil, err } // Generate refresh token refreshClaims := &TokenClaims{ UserID: user.ID, Email: user.Email, RegisteredClaims: jwt.RegisteredClaims{ ExpiresAt: jwt.NewNumericDate(now.Add(s.cfg.JWTRefreshExpiry)), IssuedAt: jwt.NewNumericDate(now), NotBefore: jwt.NewNumericDate(now), }, } refreshToken := jwt.NewWithClaims(jwt.SigningMethodHS256, refreshClaims) refreshTokenString, err := refreshToken.SignedString([]byte(s.cfg.JWTSecret)) if err != nil { return nil, err } return &TokenPair{ AccessToken: accessTokenString, RefreshToken: refreshTokenString, ExpiresIn: int64(s.cfg.JWTAccessExpiry.Seconds()), }, nil } // UpdatePassword updates a user's password func (s *AuthService) UpdatePassword(userID uint, oldPassword, newPassword string) error { user, err := s.userRepo.GetByID(userID) if err != nil { return err } // Verify old password if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(oldPassword)); err != nil { return ErrInvalidCredentials } // Validate new password if len(newPassword) < 8 { return ErrWeakPassword } // Hash new password hashedPassword, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.DefaultCost) if err != nil { return err } user.PasswordHash = string(hashedPassword) return s.userRepo.Update(user) }