package user import ( "crypto/rand" "encoding/base64" "errors" "fmt" "os" "regexp" "time" "rideaware/internal/config" "rideaware/internal/email" "rideaware/pkg/database" ) type Service struct { repo *Repository email *email.Service } func NewService() *Service { return &Service{ repo: NewRepository(), email: email.NewService(), } } func (s *Service) CreateUser(username, password, email, firstName, lastName string) (*User, error) { if username == "" || password == "" { return nil, errors.New("username and password are required") } if email != "" { if !isValidEmail(email) { return nil, errors.New("invalid email format") } } exists, err := s.repo.UserExists(username, email) if err != nil { return nil, err } if exists { return nil, errors.New("username or email already exists") } user := &User{ Username: username, Email: email, } if err := user.SetPassword(password); err != nil { return nil, err } if err := s.repo.CreateUser(user); err != nil { return nil, err } _ = s.email.SendWelcomeEmail(email, username) return user, nil } func (s *Service) VerifyUser(username, password string) (*User, error) { user, err := s.repo.GetUserByUsername(username) if err != nil { return nil, errors.New("invalid username or password") } if !user.CheckPassword(password) { return nil, errors.New("invalid username or password") } return user, nil } func (s *Service) RequestPasswordReset(email string) error { user, err := s.repo.GetUserByEmail(email) if err != nil { return nil } token, err := generateSecureToken(32) if err != nil { return err } resetToken := &PasswordReset{ UserID: user.ID, Token: token, ExpiresAt: time.Now().Add(config.JWT.ResetTokenDuration), } if err := database.DB.Create(resetToken).Error; err != nil { return err } appURL := os.Getenv("APP_URL") if appURL == "" { appURL = "https://dev.rideaware.org" } resetLink := fmt.Sprintf("%s/reset-password?token=%s", appURL, token) return s.email.SendPasswordResetEmail(user.Email, user.Username, resetLink) } func (s *Service) ResetPassword(token, newPassword string) error { if len(newPassword) < 8 { return errors.New("password must be at least 8 characters long") } var resetToken PasswordReset if err := database.DB.Where("token = ?", token).First(&resetToken).Error; err != nil { return errors.New("invalid or expired reset token") } if !resetToken.IsValid() { return errors.New("reset token has expired") } user, err := s.repo.GetUserByID(resetToken.UserID) if err != nil { return err } if err := user.SetPassword(newPassword); err != nil { return err } now := time.Now() tx := database.DB.Begin() defer func() { if r := recover(); r != nil { tx.Rollback() } }() if err := tx.Model(user).Update("password", user.Password).Error; err != nil { tx.Rollback() return err } if err := tx.Model(&resetToken).Update("used_at", now).Error; err != nil { tx.Rollback() return err } return tx.Commit().Error } func (s *Service) GetUserByID(userID uint) (*User, error) { return s.repo.GetUserByID(userID) } func (s *Service) UpdateUser(user *User) error { return s.repo.UpdateUser(user) } func isValidEmail(email string) bool { regex := regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`) return regex.MatchString(email) } func generateSecureToken(length int) (string, error) { b := make([]byte, length) if _, err := rand.Read(b); err != nil { return "", err } return base64.URLEncoding.EncodeToString(b), nil }