api_key_cache.go 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134
  1. package repository
  2. import (
  3. "context"
  4. "encoding/json"
  5. "errors"
  6. "fmt"
  7. "log"
  8. "time"
  9. "github.com/Wei-Shaw/sub2api/internal/service"
  10. "github.com/redis/go-redis/v9"
  11. )
  12. const (
  13. apiKeyRateLimitKeyPrefix = "apikey:ratelimit:"
  14. apiKeyRateLimitDuration = 24 * time.Hour
  15. apiKeyAuthCachePrefix = "apikey:auth:"
  16. authCacheInvalidateChannel = "auth:cache:invalidate"
  17. )
  18. // apiKeyRateLimitKey generates the Redis key for API key creation rate limiting.
  19. func apiKeyRateLimitKey(userID int64) string {
  20. return fmt.Sprintf("%s%d", apiKeyRateLimitKeyPrefix, userID)
  21. }
  22. func apiKeyAuthCacheKey(key string) string {
  23. return fmt.Sprintf("%s%s", apiKeyAuthCachePrefix, key)
  24. }
  25. type apiKeyCache struct {
  26. rdb *redis.Client
  27. }
  28. func NewAPIKeyCache(rdb *redis.Client) service.APIKeyCache {
  29. return &apiKeyCache{rdb: rdb}
  30. }
  31. func (c *apiKeyCache) GetCreateAttemptCount(ctx context.Context, userID int64) (int, error) {
  32. key := apiKeyRateLimitKey(userID)
  33. count, err := c.rdb.Get(ctx, key).Int()
  34. if errors.Is(err, redis.Nil) {
  35. return 0, nil
  36. }
  37. return count, err
  38. }
  39. func (c *apiKeyCache) IncrementCreateAttemptCount(ctx context.Context, userID int64) error {
  40. key := apiKeyRateLimitKey(userID)
  41. pipe := c.rdb.Pipeline()
  42. pipe.Incr(ctx, key)
  43. pipe.Expire(ctx, key, apiKeyRateLimitDuration)
  44. _, err := pipe.Exec(ctx)
  45. return err
  46. }
  47. func (c *apiKeyCache) DeleteCreateAttemptCount(ctx context.Context, userID int64) error {
  48. key := apiKeyRateLimitKey(userID)
  49. return c.rdb.Del(ctx, key).Err()
  50. }
  51. func (c *apiKeyCache) IncrementDailyUsage(ctx context.Context, apiKey string) error {
  52. return c.rdb.Incr(ctx, apiKey).Err()
  53. }
  54. func (c *apiKeyCache) SetDailyUsageExpiry(ctx context.Context, apiKey string, ttl time.Duration) error {
  55. return c.rdb.Expire(ctx, apiKey, ttl).Err()
  56. }
  57. func (c *apiKeyCache) GetAuthCache(ctx context.Context, key string) (*service.APIKeyAuthCacheEntry, error) {
  58. val, err := c.rdb.Get(ctx, apiKeyAuthCacheKey(key)).Bytes()
  59. if err != nil {
  60. return nil, err
  61. }
  62. var entry service.APIKeyAuthCacheEntry
  63. if err := json.Unmarshal(val, &entry); err != nil {
  64. return nil, err
  65. }
  66. return &entry, nil
  67. }
  68. func (c *apiKeyCache) SetAuthCache(ctx context.Context, key string, entry *service.APIKeyAuthCacheEntry, ttl time.Duration) error {
  69. if entry == nil {
  70. return nil
  71. }
  72. payload, err := json.Marshal(entry)
  73. if err != nil {
  74. return err
  75. }
  76. return c.rdb.Set(ctx, apiKeyAuthCacheKey(key), payload, ttl).Err()
  77. }
  78. func (c *apiKeyCache) DeleteAuthCache(ctx context.Context, key string) error {
  79. return c.rdb.Del(ctx, apiKeyAuthCacheKey(key)).Err()
  80. }
  81. // PublishAuthCacheInvalidation publishes a cache invalidation message to all instances
  82. func (c *apiKeyCache) PublishAuthCacheInvalidation(ctx context.Context, cacheKey string) error {
  83. return c.rdb.Publish(ctx, authCacheInvalidateChannel, cacheKey).Err()
  84. }
  85. // SubscribeAuthCacheInvalidation subscribes to cache invalidation messages
  86. func (c *apiKeyCache) SubscribeAuthCacheInvalidation(ctx context.Context, handler func(cacheKey string)) error {
  87. pubsub := c.rdb.Subscribe(ctx, authCacheInvalidateChannel)
  88. // Verify subscription is working
  89. _, err := pubsub.Receive(ctx)
  90. if err != nil {
  91. _ = pubsub.Close()
  92. return fmt.Errorf("subscribe to auth cache invalidation: %w", err)
  93. }
  94. defer func() {
  95. if err := pubsub.Close(); err != nil {
  96. log.Printf("Warning: failed to close auth cache invalidation pubsub: %v", err)
  97. }
  98. }()
  99. service.NotifyAuthCacheSubscriptionReady(ctx)
  100. ch := pubsub.Channel()
  101. for {
  102. select {
  103. case <-ctx.Done():
  104. return ctx.Err()
  105. case msg, ok := <-ch:
  106. if !ok {
  107. return errors.New("auth cache invalidation pubsub channel closed")
  108. }
  109. if msg != nil {
  110. handler(msg.Payload)
  111. }
  112. }
  113. }
  114. }