Files
SEEN/backend/internal/integrations/cache/session_cache.go
T
2026-04-10 12:06:24 +02:00

206 lines
5.3 KiB
Go

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)
}