Files
goyco/internal/services/password_reset_service.go

136 lines
3.4 KiB
Go

package services
import (
"fmt"
"time"
"goyco/internal/database"
"goyco/internal/repositories"
"goyco/internal/validation"
)
type PasswordResetService struct {
userRepo repositories.UserRepository
emailService *EmailService
}
func NewPasswordResetService(userRepo repositories.UserRepository, emailService *EmailService) *PasswordResetService {
return &PasswordResetService{
userRepo: userRepo,
emailService: emailService,
}
}
func (s *PasswordResetService) RequestPasswordReset(usernameOrEmail string) error {
trimmed := TrimString(usernameOrEmail)
if trimmed == "" {
return fmt.Errorf("username or email is required")
}
var user *database.User
var err error
normalized, emailErr := normalizeEmail(trimmed)
if emailErr == nil {
user, err = s.userRepo.GetByEmail(normalized)
if err != nil && !IsRecordNotFound(err) {
return fmt.Errorf("lookup user by email: %w", err)
}
}
if user == nil {
user, err = s.userRepo.GetByUsername(trimmed)
if err != nil {
if IsRecordNotFound(err) {
return nil
}
return fmt.Errorf("lookup user by username: %w", err)
}
}
token, hashed, err := generateVerificationToken()
if err != nil {
return err
}
now := time.Now()
expiresAt := now.Add(time.Duration(defaultTokenExpirationHours) * time.Hour)
user.PasswordResetToken = hashed
user.PasswordResetSentAt = &now
user.PasswordResetExpiresAt = &expiresAt
if err := s.userRepo.Update(user); err != nil {
return fmt.Errorf("update user: %w", err)
}
if err := s.emailService.SendPasswordResetEmail(user, token); err != nil {
user.PasswordResetToken = ""
user.PasswordResetSentAt = nil
user.PasswordResetExpiresAt = nil
_ = s.userRepo.Update(user)
return fmt.Errorf("send password reset email: %w", err)
}
return nil
}
func (s *PasswordResetService) GetUserByResetToken(token string) (*database.User, error) {
trimmed := TrimString(token)
if trimmed == "" {
return nil, fmt.Errorf("reset token is required")
}
hashed := HashVerificationToken(trimmed)
user, err := s.userRepo.GetByPasswordResetToken(hashed)
if err != nil {
if IsRecordNotFound(err) {
return nil, fmt.Errorf("invalid or expired reset token")
}
return nil, fmt.Errorf("lookup reset token: %w", err)
}
if user.PasswordResetExpiresAt == nil || time.Now().After(*user.PasswordResetExpiresAt) {
return nil, fmt.Errorf("invalid or expired reset token")
}
return user, nil
}
func (s *PasswordResetService) ResetPassword(token, newPassword string) error {
if err := validation.ValidatePassword(newPassword); err != nil {
return err
}
user, err := s.GetUserByResetToken(token)
if err != nil {
hashed := HashVerificationToken(TrimString(token))
expiredUser, lookupErr := s.userRepo.GetByPasswordResetToken(hashed)
if lookupErr == nil && expiredUser != nil {
if expiredUser.PasswordResetExpiresAt == nil || time.Now().After(*expiredUser.PasswordResetExpiresAt) {
expiredUser.PasswordResetToken = ""
expiredUser.PasswordResetSentAt = nil
expiredUser.PasswordResetExpiresAt = nil
_ = s.userRepo.Update(expiredUser)
}
}
return err
}
hashedPassword, err := HashPassword(newPassword, DefaultBcryptCost)
if err != nil {
return err
}
user.Password = string(hashedPassword)
user.PasswordResetToken = ""
user.PasswordResetSentAt = nil
user.PasswordResetExpiresAt = nil
if err := s.userRepo.Update(user); err != nil {
return fmt.Errorf("update password: %w", err)
}
return nil
}