package auth import ( "context" "strings" "time" memberdom "gateway/internal/model/member/domain" dommember "gateway/internal/model/member/domain/usecase" notifenum "gateway/internal/model/notification/domain/enum" notifuc "gateway/internal/model/notification/domain/usecase" "gateway/internal/svc" "gateway/internal/types" ) func sendPasswordResetOTP( ctx context.Context, sc *svc.ServiceContext, tenantID, uid, email string, ) (*types.PasswordForgotData, error) { cfg := sc.Config.Member.Defaults() rateKey := memberdom.GetVerifyRateRedisKey(tenantID, uid, string(passwordResetPurpose())) if err := sc.MemberVerifyRate.AssertResendAllowed(ctx, rateKey, time.Duration(cfg.OTP.ResendCooldownSeconds)*time.Second); err != nil { return nil, err } dto, plainCode, err := sc.MemberOTP.Generate(ctx, &dommember.GenerateOTPRequest{ TenantID: tenantID, UID: uid, Purpose: passwordResetPurpose(), Target: email, }) if err != nil { return nil, err } locale := sc.Config.Notification.DefaultLocale if strings.TrimSpace(locale) == "" { locale = "en-us" } if _, sendErr := sc.Notifier.Send(ctx, ¬ifuc.SendRequest{ TenantID: tenantID, UID: uid, Channel: notifenum.ChannelEmail, Kind: notifenum.NotifyVerifyEmail, Target: email, Locale: locale, Data: map[string]any{"code": plainCode, "expires_in": dto.ExpiresIn}, IdempotencyKey: dto.ChallengeID, DoNotPersistBody: true, Severity: notifenum.SeverityInfo, }); sendErr != nil { if invErr := sc.MemberOTP.Invalidate(ctx, dto.ChallengeID); invErr != nil { return nil, invErr } return nil, sendErr } return &types.PasswordForgotData{ ChallengeID: dto.ChallengeID, ExpiresIn: dto.ExpiresIn, }, nil }