package services import ( "context" "errors" "fmt" "strings" "time" "github.com/asaskevich/govalidator" "tercul/auth" "tercul/logger" "tercul/models" "tercul/repositories" ) var ( ErrInvalidCredentials = errors.New("invalid credentials") ErrUserNotFound = errors.New("user not found") ErrUserAlreadyExists = errors.New("user already exists") ErrInvalidInput = errors.New("invalid input") ErrContextRequired = errors.New("context is required") ) // AuthService interface defines authentication operations type AuthService interface { Login(ctx context.Context, input LoginInput) (*AuthResponse, error) Register(ctx context.Context, input RegisterInput) (*AuthResponse, error) GetUserFromContext(ctx context.Context) (*models.User, error) ValidateToken(ctx context.Context, tokenString string) (*models.User, error) } // authService handles authentication operations type authService struct { userRepo repositories.UserRepository jwtManager *auth.JWTManager } // NewAuthService creates a new authentication service func NewAuthService(userRepo repositories.UserRepository) AuthService { return &authService{ userRepo: userRepo, jwtManager: auth.NewJWTManager(), } } // LoginInput represents login request data type LoginInput struct { Email string `json:"email" validate:"required,email"` Password string `json:"password" validate:"required,min=6"` } // RegisterInput represents registration request data type RegisterInput struct { Username string `json:"username" validate:"required,min=3,max=50"` Email string `json:"email" validate:"required,email"` Password string `json:"password" validate:"required,min=6"` FirstName string `json:"first_name" validate:"required,min=1,max=50"` LastName string `json:"last_name" validate:"required,min=1,max=50"` } // AuthResponse represents authentication response type AuthResponse struct { Token string `json:"token"` User *models.User `json:"user"` ExpiresAt time.Time `json:"expires_at"` } // Login authenticates a user and returns a JWT token func (s *authService) Login(ctx context.Context, input LoginInput) (*AuthResponse, error) { // Validate context if ctx == nil { return nil, ErrContextRequired } // Validate input if err := s.validateLoginInput(input); err != nil { logger.LogWarn("Login failed - invalid input", logger.F("email", input.Email), logger.F("error", err)) return nil, fmt.Errorf("%w: %v", ErrInvalidInput, err) } // Sanitize email email := strings.TrimSpace(input.Email) // Find user by email user, err := s.userRepo.FindByEmail(ctx, email) if err != nil { logger.LogWarn("Login failed - user not found", logger.F("email", email)) return nil, ErrInvalidCredentials } // Check if user is active if !user.Active { logger.LogWarn("Login failed - user inactive", logger.F("user_id", user.ID), logger.F("email", email)) return nil, ErrInvalidCredentials } // Verify password if !user.CheckPassword(input.Password) { logger.LogWarn("Login failed - invalid password", logger.F("user_id", user.ID), logger.F("email", email)) return nil, ErrInvalidCredentials } // Generate JWT token token, err := s.jwtManager.GenerateToken(user) if err != nil { logger.LogError("Failed to generate JWT token", logger.F("user_id", user.ID), logger.F("email", email), logger.F("error", err)) return nil, fmt.Errorf("failed to generate token: %w", err) } // Update last login time now := time.Now() user.LastLoginAt = &now if err := s.userRepo.Update(ctx, user); err != nil { logger.LogWarn("Failed to update last login time", logger.F("user_id", user.ID), logger.F("error", err)) // Don't fail the login if we can't update the timestamp } logger.LogInfo("User logged in successfully", logger.F("user_id", user.ID), logger.F("email", email), logger.F("role", user.Role)) return &AuthResponse{ Token: token, User: user, ExpiresAt: time.Now().Add(24 * time.Hour), // This should match JWT expiration }, nil } // Register creates a new user account func (s *authService) Register(ctx context.Context, input RegisterInput) (*AuthResponse, error) { // Validate context if ctx == nil { return nil, ErrContextRequired } // Validate input if err := s.validateRegisterInput(input); err != nil { logger.LogWarn("Registration failed - invalid input", logger.F("email", input.Email), logger.F("username", input.Username), logger.F("error", err)) return nil, fmt.Errorf("%w: %v", ErrInvalidInput, err) } // Sanitize inputs email := strings.TrimSpace(input.Email) username := strings.TrimSpace(input.Username) firstName := strings.TrimSpace(input.FirstName) lastName := strings.TrimSpace(input.LastName) // Check if user already exists by email existingUser, err := s.userRepo.FindByEmail(ctx, email) if err == nil && existingUser != nil { logger.LogWarn("Registration failed - email already exists", logger.F("email", email)) return nil, ErrUserAlreadyExists } // Check if user already exists by username existingUser, err = s.userRepo.FindByUsername(ctx, username) if err == nil && existingUser != nil { logger.LogWarn("Registration failed - username already exists", logger.F("username", username)) return nil, ErrUserAlreadyExists } // Create new user user := &models.User{ Username: username, Email: email, Password: input.Password, // Will be hashed by BeforeSave hook FirstName: firstName, LastName: lastName, DisplayName: firstName + " " + lastName, Role: models.UserRoleReader, // Default role Active: true, Verified: false, // Email verification required } // Save user to database if err := s.userRepo.Create(ctx, user); err != nil { logger.LogError("Failed to create user", logger.F("email", email), logger.F("username", username), logger.F("error", err)) return nil, fmt.Errorf("failed to create user: %w", err) } // Generate JWT token token, err := s.jwtManager.GenerateToken(user) if err != nil { logger.LogError("Failed to generate JWT token for new user", logger.F("user_id", user.ID), logger.F("email", email), logger.F("error", err)) return nil, fmt.Errorf("failed to generate token: %w", err) } logger.LogInfo("User registered successfully", logger.F("user_id", user.ID), logger.F("email", email), logger.F("username", username), logger.F("role", user.Role)) return &AuthResponse{ Token: token, User: user, ExpiresAt: time.Now().Add(24 * time.Hour), }, nil } // GetUserFromContext extracts user from context func (s *authService) GetUserFromContext(ctx context.Context) (*models.User, error) { // Validate context if ctx == nil { return nil, ErrContextRequired } claims, err := auth.RequireAuth(ctx) if err != nil { logger.LogWarn("Failed to get user from context - authentication required", logger.F("error", err)) return nil, err } user, err := s.userRepo.GetByID(ctx, claims.UserID) if err != nil { logger.LogWarn("Failed to get user from context - user not found", logger.F("user_id", claims.UserID), logger.F("error", err)) return nil, ErrUserNotFound } // Check if user is still active if !user.Active { logger.LogWarn("Failed to get user from context - user inactive", logger.F("user_id", user.ID)) return nil, ErrInvalidCredentials } return user, nil } // ValidateToken validates a JWT token and returns the user func (s *authService) ValidateToken(ctx context.Context, tokenString string) (*models.User, error) { // Validate context if ctx == nil { return nil, ErrContextRequired } // Validate token string if tokenString == "" { logger.LogWarn("Token validation failed - empty token") return nil, auth.ErrMissingToken } claims, err := s.jwtManager.ValidateToken(tokenString) if err != nil { logger.LogWarn("Token validation failed - invalid token", logger.F("error", err)) return nil, err } user, err := s.userRepo.GetByID(ctx, claims.UserID) if err != nil { logger.LogWarn("Token validation failed - user not found", logger.F("user_id", claims.UserID), logger.F("error", err)) return nil, ErrUserNotFound } if !user.Active { logger.LogWarn("Token validation failed - user inactive", logger.F("user_id", user.ID)) return nil, ErrInvalidCredentials } logger.LogInfo("Token validated successfully", logger.F("user_id", user.ID), logger.F("role", user.Role)) return user, nil } // validateLoginInput validates login input func (s *authService) validateLoginInput(input LoginInput) error { if input.Email == "" { return errors.New("email is required") } if input.Password == "" { return errors.New("password is required") } // Sanitize and validate email email := strings.TrimSpace(input.Email) if !govalidator.IsEmail(email) { return errors.New("invalid email format") } // Validate password length if len(input.Password) < 6 { return errors.New("password must be at least 6 characters") } return nil } // validateRegisterInput validates registration input func (s *authService) validateRegisterInput(input RegisterInput) error { if input.Username == "" { return errors.New("username is required") } if input.Email == "" { return errors.New("email is required") } if input.Password == "" { return errors.New("password is required") } if input.FirstName == "" { return errors.New("first name is required") } if input.LastName == "" { return errors.New("last name is required") } // Sanitize and validate username username := strings.TrimSpace(input.Username) if len(username) < 3 || len(username) > 50 { return errors.New("username must be between 3 and 50 characters") } if !govalidator.Matches(username, `^[a-zA-Z0-9_-]+$`) { return errors.New("username can only contain letters, numbers, underscores, and hyphens") } // Sanitize and validate email email := strings.TrimSpace(input.Email) if !govalidator.IsEmail(email) { return errors.New("invalid email format") } // Validate password strength if len(input.Password) < 6 { return errors.New("password must be at least 6 characters") } // Sanitize and validate names firstName := strings.TrimSpace(input.FirstName) lastName := strings.TrimSpace(input.LastName) if len(firstName) < 1 || len(firstName) > 50 { return errors.New("first name must be between 1 and 50 characters") } if len(lastName) < 1 || len(lastName) > 50 { return errors.New("last name must be between 1 and 50 characters") } return nil }