package auth import ( "context" "errors" "fmt" "strings" "tercul/internal/domain" "tercul/internal/platform/auth" "tercul/internal/platform/log" "time" "github.com/asaskevich/govalidator" ) var ( ErrInvalidCredentials = errors.New("invalid credentials") ErrUserAlreadyExists = errors.New("user already exists") ErrInvalidInput = errors.New("invalid input") ) // 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 *domain.User `json:"user"` ExpiresAt time.Time `json:"expires_at"` } // AuthCommands contains the command handlers for authentication. type AuthCommands struct { userRepo domain.UserRepository jwtManager auth.JWTManagement } // NewAuthCommands creates a new AuthCommands handler. func NewAuthCommands(userRepo domain.UserRepository, jwtManager auth.JWTManagement) *AuthCommands { return &AuthCommands{ userRepo: userRepo, jwtManager: jwtManager, } } // Login authenticates a user and returns a JWT token func (c *AuthCommands) Login(ctx context.Context, input LoginInput) (*AuthResponse, error) { if err := validateLoginInput(input); err != nil { log.LogWarn("Login validation failed", log.F("email", input.Email), log.F("error", err)) return nil, fmt.Errorf("%w: %v", ErrInvalidInput, err) } email := strings.TrimSpace(input.Email) log.LogDebug("Attempting to log in user", log.F("email", email)) user, err := c.userRepo.FindByEmail(ctx, email) if err != nil { log.LogWarn("Login failed - user not found", log.F("email", email)) return nil, ErrInvalidCredentials } if !user.Active { log.LogWarn("Login failed - user inactive", log.F("user_id", user.ID), log.F("email", email)) return nil, ErrInvalidCredentials } if !user.CheckPassword(input.Password) { log.LogWarn("Login failed - invalid password", log.F("user_id", user.ID), log.F("email", email)) return nil, ErrInvalidCredentials } token, err := c.jwtManager.GenerateToken(user) if err != nil { log.LogError("Failed to generate JWT token", log.F("user_id", user.ID), log.F("error", err)) return nil, fmt.Errorf("failed to generate token: %w", err) } now := time.Now() user.LastLoginAt = &now if err := c.userRepo.Update(ctx, user); err != nil { log.LogWarn("Failed to update last login time", log.F("user_id", user.ID), log.F("error", err)) // Do not fail the login if this update fails } log.LogInfo("User logged in successfully", log.F("user_id", user.ID), log.F("email", email)) return &AuthResponse{ Token: token, User: user, ExpiresAt: time.Now().Add(24 * time.Hour), // This should be configurable }, nil } // Register creates a new user account func (c *AuthCommands) Register(ctx context.Context, input RegisterInput) (*AuthResponse, error) { if err := validateRegisterInput(input); err != nil { log.LogWarn("Registration validation failed", log.F("email", input.Email), log.F("error", err)) return nil, fmt.Errorf("%w: %v", ErrInvalidInput, err) } email := strings.TrimSpace(input.Email) username := strings.TrimSpace(input.Username) log.LogDebug("Attempting to register new user", log.F("email", email), log.F("username", username)) existingUser, _ := c.userRepo.FindByEmail(ctx, email) if existingUser != nil { log.LogWarn("Registration failed - email already exists", log.F("email", email)) return nil, ErrUserAlreadyExists } existingUser, _ = c.userRepo.FindByUsername(ctx, username) if existingUser != nil { log.LogWarn("Registration failed - username already exists", log.F("username", username)) return nil, ErrUserAlreadyExists } user := &domain.User{ Username: username, Email: email, Password: input.Password, FirstName: strings.TrimSpace(input.FirstName), LastName: strings.TrimSpace(input.LastName), DisplayName: fmt.Sprintf("%s %s", strings.TrimSpace(input.FirstName), strings.TrimSpace(input.LastName)), Role: domain.UserRoleReader, Active: true, Verified: false, // Should be false until email verification } if err := c.userRepo.Create(ctx, user); err != nil { log.LogError("Failed to create user", log.F("email", email), log.F("error", err)) return nil, fmt.Errorf("failed to create user: %w", err) } token, err := c.jwtManager.GenerateToken(user) if err != nil { log.LogError("Failed to generate JWT token for new user", log.F("user_id", user.ID), log.F("error", err)) return nil, fmt.Errorf("failed to generate token: %w", err) } log.LogInfo("User registered successfully", log.F("user_id", user.ID)) return &AuthResponse{ Token: token, User: user, ExpiresAt: time.Now().Add(24 * time.Hour), // This should be configurable }, nil } func validateLoginInput(input LoginInput) error { if input.Email == "" { return errors.New("email is required") } if !govalidator.IsEmail(strings.TrimSpace(input.Email)) { return errors.New("invalid email format") } if len(input.Password) < 6 { return errors.New("password must be at least 6 characters") } return nil } func validateRegisterInput(input RegisterInput) error { if !govalidator.IsEmail(strings.TrimSpace(input.Email)) { return errors.New("invalid email format") } if len(input.Password) < 6 { return errors.New("password must be at least 6 characters") } 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") } return nil }