Files
lijiaoqiao/supply-api/internal/sms/verifier.go
Your Name 193372ca95 test(supply-api): add sms module coverage and iam db service tests
Add the untracked SMS service package and its security-focused tests, plus database-backed IAM service unit coverage. This keeps the verification surface small and brings previously validated test/runtime support sources into version control as an isolated change.
2026-04-11 10:28:26 +08:00

114 lines
2.5 KiB
Go

package sms
import (
"context"
"fmt"
"sync"
"time"
)
// CodeEntry represents a stored verification code entry.
type CodeEntry struct {
Code string
Phone string
ExpiresAt time.Time
Used bool
}
// SMSCodeVerifier is an SMS verifier that stores and verifies codes.
type SMSCodeVerifier struct {
mu sync.RWMutex
codes map[string]*CodeEntry
store *InMemoryCodeStore
}
// NewSMSCodeVerifier creates a new SMS code verifier with in-memory storage.
func NewSMSCodeVerifier() *SMSCodeVerifier {
return &SMSCodeVerifier{
codes: make(map[string]*CodeEntry),
store: NewInMemoryCodeStore(),
}
}
// SendVerificationCode sends a verification code and stores it.
func (v *SMSCodeVerifier) SendVerificationCode(ctx context.Context, phoneNumber string) (string, error) {
code, err := GenerateCode(6)
if err != nil {
return "", fmt.Errorf("failed to generate code: %w", err)
}
codeID := fmt.Sprintf("verifier-%d", time.Now().UnixNano())
expiresAt := time.Now().Add(5 * time.Minute)
v.mu.Lock()
v.codes[codeID] = &CodeEntry{
Code: code,
Phone: phoneNumber,
ExpiresAt: expiresAt,
Used: false,
}
v.mu.Unlock()
// In production, send SMS via provider
fmt.Printf("[SMSCodeVerifier] Code '%s' for phone %s (ID: %s)\n", code, phoneNumber, codeID)
return codeID, nil
}
// Verify verifies the code for the given phone number.
func (v *SMSCodeVerifier) Verify(ctx context.Context, phone string, code string) (bool, error) {
v.mu.Lock()
defer v.mu.Unlock()
// Check all entries for matching phone and code
for id, entry := range v.codes {
if entry.Phone == phone && entry.Code == code && !entry.Used {
if time.Now().After(entry.ExpiresAt) {
delete(v.codes, id)
return false, nil
}
entry.Used = true
delete(v.codes, id)
return true, nil
}
}
return false, nil
}
// VerifyByID verifies the code by code ID.
func (v *SMSCodeVerifier) VerifyByID(ctx context.Context, codeID string, phone string, code string) (bool, error) {
v.mu.Lock()
defer v.mu.Unlock()
entry, ok := v.codes[codeID]
if !ok {
return false, nil
}
if time.Now().After(entry.ExpiresAt) {
delete(v.codes, codeID)
return false, nil
}
if entry.Phone != phone || entry.Code != code || entry.Used {
return false, nil
}
entry.Used = true
delete(v.codes, codeID)
return true, nil
}
// Cleanup removes expired codes.
func (v *SMSCodeVerifier) Cleanup() {
v.mu.Lock()
defer v.mu.Unlock()
now := time.Now()
for id, entry := range v.codes {
if now.After(entry.ExpiresAt) {
delete(v.codes, id)
}
}
}