package auth import ( "errors" "fmt" "time" "github.com/gin-gonic/gin" "github.com/golang-jwt/jwt/v5" "golang.org/x/crypto/bcrypt" "gorm.io/gorm" "customer-support-system/internal/database" "customer-support-system/internal/models" "customer-support-system/pkg/config" "customer-support-system/pkg/logger" ) var ( ErrInvalidCredentials = errors.New("invalid credentials") ErrUserNotFound = errors.New("user not found") ErrUserInactive = errors.New("user is inactive") ErrAccountLocked = errors.New("account is locked") ErrTokenExpired = errors.New("token has expired") ErrInvalidToken = errors.New("invalid token") ) // AuthService handles authentication operations type AuthService struct { db *gorm.DB } // NewAuthService creates a new authentication service func NewAuthService(db *gorm.DB) *AuthService { return &AuthService{db: db} } // Login authenticates a user and returns a JWT token func (s *AuthService) Login(username, password, clientIP string) (*models.LoginResponse, error) { // Find user by username var user models.User if err := s.db.Where("username = ?", username).First(&user).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { logger.WithField("username", username).Warn("Login attempt with non-existent username") return nil, ErrUserNotFound } return nil, fmt.Errorf("failed to find user: %w", err) } // Check if user is active if !user.Active { logger.WithField("user_id", user.ID).Warn("Login attempt by inactive user") return nil, ErrUserInactive } // Check password if !user.ComparePassword(password) { logger.WithField("user_id", user.ID).Warn("Login attempt with incorrect password") return nil, ErrInvalidCredentials } // Generate JWT token token, err := s.GenerateJWTToken(user.ID) if err != nil { logger.WithError(err).WithField("user_id", user.ID).Error("Failed to generate JWT token") return nil, fmt.Errorf("failed to generate token: %w", err) } // Log successful login logger.LogAuthEvent("login", fmt.Sprintf("%d", user.ID), clientIP, true, nil) return &models.LoginResponse{ Token: token, User: user.ToSafeUser(), }, nil } // Register creates a new user func (s *AuthService) Register(req *models.CreateUserRequest) (*models.SafeUser, error) { // Check if username already exists var existingUser models.User if err := s.db.Where("username = ?", req.Username).First(&existingUser).Error; err == nil { return nil, fmt.Errorf("username already exists") } else if !errors.Is(err, gorm.ErrRecordNotFound) { return nil, fmt.Errorf("failed to check username: %w", err) } // Check if email already exists if err := s.db.Where("email = ?", req.Email).First(&existingUser).Error; err == nil { return nil, fmt.Errorf("email already exists") } else if !errors.Is(err, gorm.ErrRecordNotFound) { return nil, fmt.Errorf("failed to check email: %w", err) } // Create new user user := models.User{ Username: req.Username, Email: req.Email, Password: req.Password, FirstName: req.FirstName, LastName: req.LastName, Role: req.Role, Active: true, } if err := s.db.Create(&user).Error; err != nil { logger.WithError(err).WithField("username", req.Username).Error("Failed to create user") return nil, fmt.Errorf("failed to create user: %w", err) } // Log user registration logger.WithField("user_id", user.ID).Info("User registered successfully") safeUser := user.ToSafeUser() return &safeUser, nil } // GenerateJWTToken generates a JWT token for a user func (s *AuthService) GenerateJWTToken(userID uint) (string, error) { // Get JWT configuration jwtConfig := config.AppConfig.JWT // Create claims claims := jwt.MapClaims{ "user_id": userID, "exp": time.Now().Add(time.Hour * time.Duration(jwtConfig.ExpirationHours)).Unix(), "iat": time.Now().Unix(), "iss": jwtConfig.Issuer, "aud": jwtConfig.Audience, } // Create token token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) // Sign token tokenString, err := token.SignedString(jwtConfig.GetJWTSigningKey()) if err != nil { return "", fmt.Errorf("failed to sign token: %w", err) } return tokenString, nil } // ValidateJWTToken validates a JWT token and returns the user ID func (s *AuthService) ValidateJWTToken(tokenString string) (uint, error) { // Get JWT configuration jwtConfig := config.AppConfig.JWT // Parse token token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { // Validate signing method if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) } return jwtConfig.GetJWTSigningKey(), nil }) if err != nil { return 0, fmt.Errorf("failed to parse token: %w", err) } // Validate claims if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid { // Check expiration if exp, ok := claims["exp"].(float64); ok { if time.Now().Unix() > int64(exp) { return 0, ErrTokenExpired } } // Get user ID userID, ok := claims["user_id"].(float64) if !ok { return 0, ErrInvalidToken } return uint(userID), nil } return 0, ErrInvalidToken } // GetUserByID returns a user by ID func (s *AuthService) GetUserByID(userID uint) (*models.User, error) { var user models.User if err := s.db.First(&user, userID).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, ErrUserNotFound } return nil, fmt.Errorf("failed to get user: %w", err) } return &user, nil } // UpdateUser updates a user func (s *AuthService) UpdateUser(userID uint, req *models.UpdateUserRequest) (*models.SafeUser, error) { var user models.User if err := s.db.First(&user, userID).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, ErrUserNotFound } return nil, fmt.Errorf("failed to get user: %w", err) } // Update fields if req.FirstName != "" { user.FirstName = req.FirstName } if req.LastName != "" { user.LastName = req.LastName } if req.Email != "" { user.Email = req.Email } if req.Active != nil { user.Active = *req.Active } if req.Role != "" { user.Role = req.Role } if err := s.db.Save(&user).Error; err != nil { logger.WithError(err).WithField("user_id", userID).Error("Failed to update user") return nil, fmt.Errorf("failed to update user: %w", err) } // Log user update logger.WithField("user_id", userID).Info("User updated successfully") safeUser := user.ToSafeUser() return &safeUser, nil } // ChangePassword changes a user's password func (s *AuthService) ChangePassword(userID uint, req *models.ChangePasswordRequest) error { var user models.User if err := s.db.First(&user, userID).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return ErrUserNotFound } return fmt.Errorf("failed to get user: %w", err) } // Check current password if !user.ComparePassword(req.CurrentPassword) { logger.WithField("user_id", userID).Warn("Password change attempt with incorrect current password") return ErrInvalidCredentials } // Update password user.Password = req.NewPassword if err := s.db.Save(&user).Error; err != nil { logger.WithError(err).WithField("user_id", userID).Error("Failed to change password") return fmt.Errorf("failed to change password: %w", err) } // Log password change logger.WithField("user_id", userID).Info("Password changed successfully") return nil } // HashPassword hashes a password using bcrypt func HashPassword(password string) (string, error) { hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) if err != nil { return "", fmt.Errorf("failed to hash password: %w", err) } return string(hashedPassword), nil } // CheckPassword checks if a password matches a hashed password func CheckPassword(password, hashedPassword string) bool { err := bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password)) return err == nil } // AuthMiddleware returns a gin middleware for authentication func AuthMiddleware() gin.HandlerFunc { return func(c *gin.Context) { // Get token from Authorization header tokenString := c.GetHeader("Authorization") if tokenString == "" { c.JSON(401, gin.H{"error": "Authorization header is required"}) c.Abort() return } // Remove "Bearer " prefix if len(tokenString) > 7 && tokenString[:7] == "Bearer " { tokenString = tokenString[7:] } // Validate token authService := NewAuthService(database.GetDB()) userID, err := authService.ValidateJWTToken(tokenString) if err != nil { c.JSON(401, gin.H{"error": "Invalid or expired token"}) c.Abort() return } // Get user user, err := authService.GetUserByID(userID) if err != nil { c.JSON(401, gin.H{"error": "User not found"}) c.Abort() return } // Check if user is active if !user.Active { c.JSON(401, gin.H{"error": "User account is inactive"}) c.Abort() return } // Set user ID in context c.Set("userID", userID) c.Set("userRole", user.Role) c.Next() } } // RoleMiddleware returns a gin middleware for role-based authorization func RoleMiddleware(roles ...string) gin.HandlerFunc { return func(c *gin.Context) { // Get user role from context userRole, exists := c.Get("userRole") if !exists { c.JSON(401, gin.H{"error": "User not authenticated"}) c.Abort() return } // Check if user has required role roleStr, ok := userRole.(string) if !ok { c.JSON(500, gin.H{"error": "Invalid user role"}) c.Abort() return } // Check if user role is in allowed roles allowed := false for _, role := range roles { if roleStr == role { allowed = true break } } if !allowed { c.JSON(403, gin.H{"error": "Insufficient permissions"}) c.Abort() return } c.Next() } }