94 lines
2.2 KiB
Go
94 lines
2.2 KiB
Go
package user
|
|
|
|
import (
|
|
"Yimaru-Backend/internal/domain"
|
|
"context"
|
|
"crypto/subtle"
|
|
"errors"
|
|
"time"
|
|
)
|
|
|
|
func (s *Service) SendResetCode(ctx context.Context, medium domain.OtpMedium, sentTo string, provider domain.SMSProvider) error {
|
|
var user domain.User
|
|
var err error
|
|
|
|
// Look up user by email or phone to get the actual userID
|
|
switch medium {
|
|
case domain.OtpMediumEmail:
|
|
user, err = s.userStore.GetUserByEmailPhone(ctx, sentTo, "")
|
|
case domain.OtpMediumSms:
|
|
user, err = s.userStore.GetUserByEmailPhone(ctx, "", sentTo)
|
|
default:
|
|
return errors.New("invalid OTP medium")
|
|
}
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Use the actual user ID when storing OTP
|
|
return s.SendOtp(ctx, user.ID, sentTo, domain.OtpReset, medium, provider)
|
|
}
|
|
|
|
func (s *Service) ResetPassword(ctx context.Context, resetReq domain.ResetPasswordReq) error {
|
|
// Look up user by email or phone (don't trust client-provided user_id)
|
|
var user domain.User
|
|
var err error
|
|
|
|
if resetReq.Email != "" {
|
|
user, err = s.userStore.GetUserByEmailPhone(ctx, resetReq.Email, "")
|
|
} else if resetReq.PhoneNumber != "" {
|
|
user, err = s.userStore.GetUserByEmailPhone(ctx, "", resetReq.PhoneNumber)
|
|
} else {
|
|
return errors.New("email or phone number is required")
|
|
}
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Get OTP for the actual user
|
|
otp, err := s.otpStore.GetOtp(ctx, user.ID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Validate OTP purpose (should be for reset, not registration)
|
|
if otp.For != domain.OtpReset {
|
|
return domain.ErrInvalidOtp
|
|
}
|
|
|
|
if otp.Used {
|
|
return domain.ErrOtpAlreadyUsed
|
|
}
|
|
|
|
if time.Now().After(otp.ExpiresAt) {
|
|
return domain.ErrOtpExpired
|
|
}
|
|
|
|
// Use constant-time comparison for OTP
|
|
if subtle.ConstantTimeCompare([]byte(otp.Otp), []byte(resetReq.OtpCode)) != 1 {
|
|
return domain.ErrInvalidOtp
|
|
}
|
|
|
|
// Hash the new password before storing
|
|
hashedPassword, err := hashPassword(resetReq.Password)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Update password with hashed value
|
|
err = s.userStore.UpdatePasswordHash(ctx, hashedPassword, user.ID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Mark OTP as used to prevent replay attacks
|
|
err = s.otpStore.MarkOtpAsUsed(ctx, otp)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|