/* Copyright 2026 Arctel.net Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package cap import ( "context" "sync" "time" "github.com/redis/go-redis/v9" ) // Store defines the storage interface for challenge nonces and verification tokens type Store interface { Get(ctx context.Context, key string) (string, bool, error) Set(ctx context.Context, key string, val string, ttl time.Duration) error Delete(ctx context.Context, key string) error // SetNX atomically sets key=val with the given TTL only when the key does not // exist yet. It returns true when the key was actually written (i.e. this // caller "won" the race), and false when the key already existed. SetNX(ctx context.Context, key string, val string, ttl time.Duration) (bool, error) // GetAndDelete atomically retrieves the value of key and removes it in a // single operation. Returns ("", false, nil) when the key does not exist. GetAndDelete(ctx context.Context, key string) (string, bool, error) } type memoryItem struct { value string expiresAt time.Time } // MemoryStore is a thread-safe in-memory implementation of Store type MemoryStore struct { items map[string]memoryItem mu sync.Mutex // unified write-lock; promotes to exclusive for all ops } // NewMemoryStore creates and initializes a new MemoryStore func NewMemoryStore(cleanupInterval time.Duration) *MemoryStore { store := &MemoryStore{ items: make(map[string]memoryItem), } if cleanupInterval > 0 { go store.startCleanupLoop(cleanupInterval) } return store } // Get 从 MemoryStore 获取指定 key 的值 func (s *MemoryStore) Get(_ context.Context, key string) (string, bool, error) { s.mu.Lock() defer s.mu.Unlock() return s.getLocked(key) } // getLocked is the internal helper – caller must hold s.mu. func (s *MemoryStore) getLocked(key string) (string, bool, error) { item, found := s.items[key] if !found { return "", false, nil } if time.Now().After(item.expiresAt) { delete(s.items, key) return "", false, nil } return item.value, true, nil } // Set 向 MemoryStore 写入指定 key 的值 func (s *MemoryStore) Set(_ context.Context, key string, val string, ttl time.Duration) error { s.mu.Lock() defer s.mu.Unlock() s.items[key] = memoryItem{ value: val, expiresAt: time.Now().Add(ttl), } return nil } // Delete 从 MemoryStore 删除指定 key func (s *MemoryStore) Delete(_ context.Context, key string) error { s.mu.Lock() defer s.mu.Unlock() delete(s.items, key) return nil } // SetNX atomically sets key only when it is absent (or expired). // Returns true if the key was written by this call. func (s *MemoryStore) SetNX(_ context.Context, key string, val string, ttl time.Duration) (bool, error) { s.mu.Lock() defer s.mu.Unlock() _, exists, _ := s.getLocked(key) if exists { return false, nil } s.items[key] = memoryItem{ value: val, expiresAt: time.Now().Add(ttl), } return true, nil } // GetAndDelete atomically retrieves and removes key in one critical section. func (s *MemoryStore) GetAndDelete(_ context.Context, key string) (string, bool, error) { s.mu.Lock() defer s.mu.Unlock() val, exists, err := s.getLocked(key) if err != nil || !exists { return "", false, err } delete(s.items, key) return val, true, nil } func (s *MemoryStore) startCleanupLoop(interval time.Duration) { ticker := time.NewTicker(interval) for range ticker.C { s.cleanupExpired() } } func (s *MemoryStore) cleanupExpired() { now := time.Now() s.mu.Lock() defer s.mu.Unlock() for k, v := range s.items { if now.After(v.expiresAt) { delete(s.items, k) } } } // RedisStore is a GORM-compatible/standalone Redis-backed implementation of Store type RedisStore struct { client redis.UniversalClient } // NewRedisStore creates a new RedisStore wrapping a redis.UniversalClient func NewRedisStore(client redis.UniversalClient) *RedisStore { return &RedisStore{ client: client, } } // Get 从 RedisStore 获取指定 key 的值 func (s *RedisStore) Get(ctx context.Context, key string) (string, bool, error) { val, err := s.client.Get(ctx, key).Result() if err == redis.Nil { return "", false, nil } if err != nil { return "", false, err } return val, true, nil } // Set 向 RedisStore 写入指定 key 的值 func (s *RedisStore) Set(ctx context.Context, key string, val string, ttl time.Duration) error { return s.client.Set(ctx, key, val, ttl).Err() } // Delete 从 RedisStore 删除指定 key func (s *RedisStore) Delete(ctx context.Context, key string) error { return s.client.Del(ctx, key).Err() } // SetNX wraps Redis SET NX – returns true only when the key was newly created. func (s *RedisStore) SetNX(ctx context.Context, key string, val string, ttl time.Duration) (bool, error) { return s.client.SetNX(ctx, key, val, ttl).Result() } // GetAndDelete wraps Redis GETDEL (available since Redis 6.2). func (s *RedisStore) GetAndDelete(ctx context.Context, key string) (string, bool, error) { val, err := s.client.GetDel(ctx, key).Result() if err == redis.Nil { return "", false, nil } if err != nil { return "", false, err } return val, true, nil }