package cache import ( "context" "fmt" "time" "github.com/google/uuid" ) // SessionData represents cached session information type SessionData struct { SessionID string `json:"sessionId"` UserID string `json:"userId"` RefreshToken string `json:"refreshToken"` UserAgent string `json:"userAgent"` IP string `json:"ip"` ExpiresAt time.Time `json:"expiresAt"` CreatedAt time.Time `json:"createdAt"` } // SessionCache provides session caching operations type SessionCache struct { service *Service keys *KeyBuilder } // NewSessionCache creates a new session cache func NewSessionCache(service *Service, namespace string) *SessionCache { return &SessionCache{ service: service, keys: NewKeyBuilder(namespace), } } // SetSession stores session data in cache func (sc *SessionCache) SetSession(ctx context.Context, session SessionData) error { key := sc.keys.SessionKey(session.SessionID) ttl := time.Until(session.ExpiresAt) if ttl <= 0 { return fmt.Errorf("session already expired") } return sc.service.Set(ctx, key, session, ttl) } // GetSession retrieves session data from cache func (sc *SessionCache) GetSession(ctx context.Context, sessionID string) (*SessionData, error) { key := sc.keys.SessionKey(sessionID) var session SessionData if err := sc.service.Get(ctx, key, &session); err != nil { return nil, err } return &session, nil } // DeleteSession removes session data from cache func (sc *SessionCache) DeleteSession(ctx context.Context, sessionID string) error { key := sc.keys.SessionKey(sessionID) return sc.service.Delete(ctx, key) } // GetSessionByRefreshToken retrieves session by refresh token // Note: This requires scanning, which is slower. Consider using a secondary index. func (sc *SessionCache) GetSessionByRefreshToken(ctx context.Context, refreshToken string) (*SessionData, error) { pattern := sc.keys.Build(PrefixSession, "*") keys, err := sc.service.Keys(ctx, pattern) if err != nil { return nil, err } for _, key := range keys { var session SessionData if err := sc.service.Get(ctx, key, &session); err != nil { continue } if session.RefreshToken == refreshToken { return &session, nil } } return nil, ErrCacheMiss } // ExtendSession extends the TTL of a session func (sc *SessionCache) ExtendSession(ctx context.Context, sessionID string, duration time.Duration) error { key := sc.keys.SessionKey(sessionID) return sc.service.Expire(ctx, key, duration) } // UserData represents cached user information type UserData struct { ID string `json:"id"` Email string `json:"email"` DisplayName string `json:"displayName"` Role string `json:"role"` CachedAt time.Time `json:"cachedAt"` } // SetUser stores user data in cache func (sc *SessionCache) SetUser(ctx context.Context, user UserData) error { key := sc.keys.UserKey(user.ID) return sc.service.Set(ctx, key, user, TTLUser) } // GetUser retrieves user data from cache func (sc *SessionCache) GetUser(ctx context.Context, userID string) (*UserData, error) { key := sc.keys.UserKey(userID) var user UserData if err := sc.service.Get(ctx, key, &user); err != nil { return nil, err } return &user, nil } // DeleteUser removes user data from cache func (sc *SessionCache) DeleteUser(ctx context.Context, userID string) error { key := sc.keys.UserKey(userID) return sc.service.Delete(ctx, key) } // InvalidateUserSessions removes all sessions for a user func (sc *SessionCache) InvalidateUserSessions(ctx context.Context, userID string) error { pattern := sc.keys.Build(PrefixSession, "*") keys, err := sc.service.Keys(ctx, pattern) if err != nil { return err } toDelete := make([]string, 0) for _, key := range keys { var session SessionData if err := sc.service.Get(ctx, key, &session); err != nil { continue } if session.UserID == userID { toDelete = append(toDelete, key) } } if len(toDelete) > 0 { return sc.service.Delete(ctx, toDelete...) } return nil } // RateLimitCheck checks if an action is rate limited func (sc *SessionCache) RateLimitCheck(ctx context.Context, identifier, action string, limit int64, window time.Duration) (bool, error) { key := sc.keys.RateLimitKey(identifier, action) count, err := sc.service.Increment(ctx, key) if err != nil { return false, err } // Set expiry on first increment if count == 1 { if err := sc.service.Expire(ctx, key, window); err != nil { return false, err } } return count <= limit, nil } // AcquireLock attempts to acquire a distributed lock func (sc *SessionCache) AcquireLock(ctx context.Context, resource string, ttl time.Duration) (string, bool, error) { lockID := uuid.New().String() key := sc.keys.LockKey(resource) acquired, err := sc.service.SetNX(ctx, key, lockID, ttl) if err != nil { return "", false, err } return lockID, acquired, nil } // ReleaseLock releases a distributed lock func (sc *SessionCache) ReleaseLock(ctx context.Context, resource, lockID string) error { key := sc.keys.LockKey(resource) // Verify we own the lock before deleting var storedLockID string if err := sc.service.Get(ctx, key, &storedLockID); err != nil { if err == ErrCacheMiss { return nil // Lock already released } return err } if storedLockID != lockID { return fmt.Errorf("lock owned by different process") } return sc.service.Delete(ctx, key) }