package auth import ( "context" "crypto/sha256" "encoding/hex" "strings" "time" "img_infinite_canvas/internal/domain/design" ) type CodeCache interface { Get(ctx context.Context, key string, target any) (bool, error) Set(ctx context.Context, key string, value any, ttl time.Duration) error Delete(ctx context.Context, key string) error } type CacheCodeStore struct { cache CodeCache ttl time.Duration prefix string } type codeIndex struct { Key string `json:"key"` } func NewCacheCodeStore(cache CodeCache, ttl time.Duration) *CacheCodeStore { if ttl <= 0 { ttl = 10 * time.Minute } return &CacheCodeStore{ cache: cache, ttl: ttl, prefix: "auth:code", } } func (s *CacheCodeStore) CreateVerificationCode(ctx context.Context, code VerificationCode) error { if err := ctx.Err(); err != nil { return err } if code.ExpiresAt.IsZero() { code.ExpiresAt = time.Now().Add(s.ttl) } key := s.activeKey(code.Region, code.Channel, code.Target, code.Purpose) ttl := s.remainingTTL(code) if err := s.cache.Set(ctx, key, code, ttl); err != nil { return err } return s.cache.Set(ctx, s.idKey(code.ID), codeIndex{Key: key}, ttl) } func (s *CacheCodeStore) FindActiveVerificationCode(ctx context.Context, region string, channel string, target string, purpose string) (VerificationCode, error) { if err := ctx.Err(); err != nil { return VerificationCode{}, err } key := s.activeKey(region, channel, target, purpose) var code VerificationCode found, err := s.cache.Get(ctx, key, &code) if err != nil { return VerificationCode{}, err } if !found || code.ID == "" { return VerificationCode{}, design.ErrNotFound } if code.ConsumedAt != nil || time.Now().After(code.ExpiresAt) { _ = s.cache.Delete(ctx, key) _ = s.cache.Delete(ctx, s.idKey(code.ID)) return VerificationCode{}, design.ErrNotFound } return code, nil } func (s *CacheCodeStore) MarkVerificationCodeConsumed(ctx context.Context, id string, _ time.Time) error { index, err := s.findIndex(ctx, id) if err != nil { return err } if err := s.cache.Delete(ctx, index.Key); err != nil { return err } return s.cache.Delete(ctx, s.idKey(id)) } func (s *CacheCodeStore) IncrementVerificationCodeAttempts(ctx context.Context, id string) error { index, err := s.findIndex(ctx, id) if err != nil { return err } var code VerificationCode found, err := s.cache.Get(ctx, index.Key, &code) if err != nil { return err } if !found || code.ID == "" { _ = s.cache.Delete(ctx, s.idKey(id)) return design.ErrNotFound } code.Attempts++ return s.cache.Set(ctx, index.Key, code, s.remainingTTL(code)) } func (s *CacheCodeStore) findIndex(ctx context.Context, id string) (codeIndex, error) { if err := ctx.Err(); err != nil { return codeIndex{}, err } var index codeIndex found, err := s.cache.Get(ctx, s.idKey(id), &index) if err != nil { return codeIndex{}, err } if !found || index.Key == "" { return codeIndex{}, design.ErrNotFound } return index, nil } func (s *CacheCodeStore) remainingTTL(code VerificationCode) time.Duration { if !code.ExpiresAt.IsZero() { ttl := time.Until(code.ExpiresAt) if ttl > 0 { return ttl } } return s.ttl } func (s *CacheCodeStore) activeKey(region string, channel string, target string, purpose string) string { raw := strings.Join([]string{ strings.TrimSpace(region), strings.TrimSpace(channel), strings.TrimSpace(target), strings.TrimSpace(purpose), }, "\x00") sum := sha256.Sum256([]byte(raw)) return s.prefix + ":active:" + hex.EncodeToString(sum[:]) } func (s *CacheCodeStore) idKey(id string) string { return s.prefix + ":id:" + strings.TrimSpace(id) }