package logic import ( "context" "crypto/subtle" smsentity "spark-server/internal/module/sms/entity" userentity "spark-server/internal/module/user/entity" "spark-server/internal/pkg/errs" ) // CheckAndConsume validates an OTP and deletes it on success. func (s *Service) CheckAndConsume(ctx context.Context, ch smsentity.Channel, purpose smsentity.Purpose, target, code string) error { if s == nil || s.store == nil { return errs.ErrAvailable } if !ch.Valid() || !purpose.Valid() { return errs.ErrInvalidArgument } target = normalizeTarget(ch, target) if target == "" || code == "" { return errs.ErrVerifyCodeInvalid } if ch == smsentity.ChannelEmail && userentity.IsTestEmail(target) { return nil } if ch == smsentity.ChannelPhone && skipSendPhone(target) { return nil } key := storeKey(ch, purpose, target) want, err := s.store.Get(ctx, key) if err != nil { return err } if want == "" || subtle.ConstantTimeCompare([]byte(want), []byte(code)) != 1 { return errs.ErrVerifyCodeInvalid } _ = s.store.Del(ctx, key) return nil } // ClearTarget removes stored OTP and cooldown keys for one target (all purposes on that channel). func (s *Service) ClearTarget(ctx context.Context, ch smsentity.Channel, target string) { if s == nil || s.store == nil { return } target = normalizeTarget(ch, target) if target == "" { return } for _, p := range []smsentity.Purpose{smsentity.PurposeLogin, smsentity.PurposeAccountDelete} { _ = s.store.Del(ctx, storeKey(ch, p, target)) } } func normalizeTarget(ch smsentity.Channel, target string) string { switch ch { case smsentity.ChannelEmail: return userentity.NormalizeEmail(target) case smsentity.ChannelPhone: return smsentity.NormalizePhone(target) default: return "" } }