package auth import ( "context" "fmt" "time" "github.com/google/uuid" "golang.org/x/crypto/bcrypt" "base/internal/domain/auth" "base/internal/dto" "base/internal/pkg/oauth" "base/pkg/email" "base/pkg/hashids" "base/pkg/jwt" ) // SendResetPasswordEmail sends a password reset email func (s *service) SendResetPasswordEmail(ctx context.Context, request dto.SendResetPasswordEmailRequest) error { user, err := s.userRepo.FindByEmail(ctx, request.Email) if err != nil { // Don't reveal if user exists or not for security return err } // Generate reset code code := hashids.GenerateCode(int64(user.ID.Time())) key := fmt.Sprintf("reset_password:%s", user.ID.String()) // Store code in cache (15 minutes TTL) if storeErr := s.resetPasswordStore.Set(ctx, key, code, 15*time.Minute); storeErr != nil { return fmt.Errorf("failed to store reset password code: %w", storeErr) } // Send email emailData := map[string]interface{}{ "Code": code, "Name": user.FirstName, } emailMsg := email.Request{ To: user.Email, Subject: "Reset Your Password", Template: email.TemplateData{ EmailTemplateName: email.TemplatePasswordReset, Data: emailData, }, } if _, sendEmailErr := s.emailService.Send(ctx, emailMsg); sendEmailErr != nil { return fmt.Errorf("failed to send reset password email: %w", sendEmailErr) } return nil } // ResetPassword resets a user's password with the provided code func (s *service) ResetPassword(ctx context.Context, request dto.ResetPasswordRequest) (*dto.TokenResponse, error) { user, err := s.userRepo.FindByEmail(ctx, request.Email, auth.WithAccounts()) if err != nil { return nil, ErrUserNotFound } // Get code from cache key := fmt.Sprintf("reset_password:%s", user.ID.String()) storedCode, found, getErr := s.resetPasswordStore.Get(ctx, key) if getErr != nil || !found { return nil, ErrInvalidVerificationCode } if storedCode != request.Code { return nil, ErrInvalidVerificationCode } // Find credentials account var credentialsAccount *auth.Account for _, acc := range user.Accounts { if acc.Provider == oauth.Credentials { credentialsAccount = &acc break } } // Hash new password hashedPassword, genHashPassErr := bcrypt.GenerateFromPassword([]byte(request.Password), bcrypt.DefaultCost) if genHashPassErr != nil { return nil, fmt.Errorf("failed to hash password: %w", genHashPassErr) } hashedPasswordStr := string(hashedPassword) if credentialsAccount != nil { // Update existing account credentialsAccount.Password = &hashedPasswordStr credentialsAccount.UpdatedAt = time.Now() if err := s.accountRepo.Update(ctx, credentialsAccount); err != nil { return nil, fmt.Errorf("failed to update account: %w", err) } } else { // Create new credentials account account := &auth.Account{ ID: uuid.New(), UserID: user.ID, Provider: oauth.Credentials, Password: &hashedPasswordStr, CreatedAt: time.Now(), UpdatedAt: time.Now(), } if err := s.accountRepo.Create(ctx, account); err != nil { return nil, fmt.Errorf("failed to create account: %w", err) } } // Delete reset code from cache _ = s.resetPasswordStore.Delete(ctx, key) // Generate tokens tokens, err := s.jwtService.GenerateAccessRefreshTokenPair(ctx, &jwt.TokenData{Sub: user.ID.String()}) if err != nil { return nil, fmt.Errorf("failed to generate tokens: %w", err) } return &dto.TokenResponse{ AccessToken: tokens.AccessToken, RefreshToken: tokens.RefreshToken, }, nil }