package logic import ( "context" "crypto/rand" "encoding/binary" "fmt" "spark-server/internal/module/user/entity" "spark-server/internal/pkg/errs" ) const ( codeTTLSeconds = 300 codeCooldownSeconds = 60 ) // EmailCodeTTLSeconds is the OTP lifetime returned to clients. const EmailCodeTTLSeconds = codeTTLSeconds // CodeStore is the email OTP cache (Redis). type CodeStore interface { Put(ctx context.Context, email, code string, ttlSeconds int) error Get(ctx context.Context, email string) (string, error) Del(ctx context.Context, email string) error SetCooldown(ctx context.Context, email string, ttlSeconds int) (bool, error) } func (s *Service) SendEmailCode(ctx context.Context, email string) error { if s == nil || s.codes == nil { return errs.ErrAvailable } email = entity.NormalizeEmail(email) if email == "" { return errs.ErrInvalidArgument } if entity.IsTestEmail(email) { return nil } ok, err := s.codes.SetCooldown(ctx, email, codeCooldownSeconds) if err != nil { return err } if !ok { return errs.ErrRateLimited } code, err := randomDigits(6) if err != nil { return errs.ErrInner.WithError("email-code", err) } return s.codes.Put(ctx, email, code, codeTTLSeconds) } func randomDigits(n int) (string, error) { if n <= 0 { return "", fmt.Errorf("digits") } var buf [8]byte out := make([]byte, n) for i := 0; i < n; i++ { if _, err := rand.Read(buf[:4]); err != nil { return "", err } out[i] = '0' + byte(binary.BigEndian.Uint32(buf[:4])%10) } return string(out), nil }