Files
2025-09-13 06:48:55 +03:00

363 lines
9.6 KiB
Go

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()
}
}