init
This commit is contained in:
292
internal/service/auth_service.go
Normal file
292
internal/service/auth_service.go
Normal file
@@ -0,0 +1,292 @@
|
||||
// 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)
|
||||
}
|
||||
Reference in New Issue
Block a user