package user import ( "Yimaru-Backend/internal/domain" "Yimaru-Backend/internal/pkgs/helpers" "context" "fmt" "time" "golang.org/x/crypto/bcrypt" ) func (s *Service) VerifyOtp(ctx context.Context, email, phone, otpCode string) error { user, err := s.userStore.GetUserByEmailPhone(ctx, email, phone) if err != nil { return err } // 1. Retrieve the OTP from the store storedOtp, err := s.otpStore.GetOtp(ctx, user.UserName) if err != nil { return err // could be ErrOtpNotFound or other DB errors } // 2. Check if OTP was already used if storedOtp.Used { return domain.ErrOtpAlreadyUsed } // 3. Check if OTP has expired if time.Now().After(storedOtp.ExpiresAt) { return domain.ErrOtpExpired } // 4. Check if the provided OTP matches if storedOtp.Otp != otpCode { return domain.ErrInvalidOtp } // 5. Mark OTP as used storedOtp.Used = true storedOtp.UsedAt = timePtr(time.Now()) if err := s.otpStore.MarkOtpAsUsed(ctx, storedOtp); err != nil { return err } // user, err := s.userStore.GetUserByUserName(ctx, userName) // if err != nil { // return err // } newUser := domain.UpdateUserReq{ UserID: user.ID, Status: domain.ValidString{ Value: string(domain.UserStatusActive), Valid: true, }, } s.userStore.UpdateUserStatus(ctx, newUser) return nil } func (s *Service) ResendOtp( ctx context.Context, email, phone string, ) error { user, err := s.userStore.GetUserByEmailPhone(ctx, email, phone) if err != nil { return err } otpCode := helpers.GenerateOTP() message := fmt.Sprintf( "Welcome to Yimaru Online Learning Platform, your OTP is %s please don't share with anyone.", otpCode, ) otp, err := s.otpStore.GetOtp(ctx, user.UserName) if err != nil { return err } // Broadcast OTP (same logic as SendOtp) switch otp.Medium { case domain.OtpMediumSms: if err := s.messengerSvc.SendAfroMessageSMS(ctx, otp.SentTo, message); err != nil { return err } case domain.OtpMediumEmail: if err := s.messengerSvc.SendEmail( ctx, otp.SentTo, message, message, "Yimaru - One Time Password", ); err != nil { return err } default: return fmt.Errorf("invalid otp medium: %s", otp.Medium) } if err := s.otpStore.UpdateOtp(ctx, otpCode, user.UserName); err != nil { return err } return nil } func (s *Service) SendOtp(ctx context.Context, userName string, sentTo string, otpFor domain.OtpFor, medium domain.OtpMedium, provider domain.SMSProvider) error { otpCode := helpers.GenerateOTP() message := fmt.Sprintf("Welcome to Yimaru Online Learning Platform, your OTP is %s please don't share with anyone.", otpCode) switch medium { case domain.OtpMediumSms: switch provider { case domain.TwilioSms: if err := s.messengerSvc.SendTwilioSMS(ctx, sentTo, message); err != nil { return err } case domain.AfroMessage: if err := s.messengerSvc.SendAfroMessageSMS(ctx, sentTo, message); err != nil { return err } default: return fmt.Errorf("invalid sms provider: %s", provider) } case domain.OtpMediumEmail: if err := s.messengerSvc.SendEmail(ctx, sentTo, message, message, "Yimaru - One Time Password"); err != nil { return err } } otp := domain.Otp{ UserName: userName, SentTo: sentTo, Medium: medium, For: otpFor, Otp: otpCode, Used: false, CreatedAt: time.Now(), ExpiresAt: time.Now().Add(OtpExpiry), } return s.otpStore.CreateOtp(ctx, otp) } // helper function to get a pointer to time.Time func timePtr(t time.Time) time.Time { return t } func hashPassword(plaintextPassword string) ([]byte, error) { hash, err := bcrypt.GenerateFromPassword([]byte(plaintextPassword), 12) if err != nil { return []byte{}, err } return hash, nil }