package logic import ( "context" "crypto/rand" "encoding/binary" "fmt" smsentity "spark-server/internal/module/sms/entity" userentity "spark-server/internal/module/user/entity" ) const ( defaultCodeTTLSeconds = 300 defaultCodeCooldownSeconds = 60 ) // CodeTTLSeconds is the OTP lifetime returned to clients. const CodeTTLSeconds = defaultCodeTTLSeconds // AccountEmail resolves the bound mailbox for an authenticated user (account delete OTP). type AccountEmail interface { EmailByUserID(ctx context.Context, userID int64) (string, error) } // Service issues and checks OTPs for email and phone. type Service struct { store CodeStore email EmailSender phone []PhoneCarrierSender accounts AccountEmail } func NewService(store CodeStore, email EmailSender, phone []PhoneCarrierSender, accounts AccountEmail) *Service { return &Service{store: store, email: email, phone: phone, accounts: accounts} } func storeKey(ch smsentity.Channel, purpose smsentity.Purpose, target string) string { return fmt.Sprintf("%d:%d:%s", ch, purpose, target) } 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 } func skipSendEmail(email string) bool { return userentity.IsTestEmail(email) } func skipSendPhone(phone string) bool { // Dev-friendly test MSISDNs matching spark test convention. switch phone { case "+8613800138000", "+8613800138001", "+8613800138002": return true default: return false } }