136 lines
3.4 KiB
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
|
|
}
|