package auth import ( "sync" "time") // sessionEntry stores auth binding with expiration.type sessionEntry struct { authID string expiresAt time.Time} // SessionCache provides TTL-based session to auth mapping with automatic cleanup.type SessionCache struct { mu sync.RWMutex entries map[string]sessionEntry ttl time.Duration stopCh chan struct{}} // NewSessionCache creates a cache with the specified TTL.// A background goroutine periodically cleans expired entries.func NewSessionCache(ttl time.Duration) *SessionCache { if ttl <= 0 { ttl = 30 * time.Minute } c := &SessionCache{ entries: make(map[string]sessionEntry), ttl: ttl, stopCh: make(chan struct{}), } go c.cleanupLoop() return c} // Get retrieves the auth ID bound to a session, if still valid.// Does NOT refresh the TTL on access.func (c *SessionCache) Get(sessionID string) (string, bool) { if sessionID == "" { return "", false } c.mu.RLock() entry, ok := c.entries[sessionID] c.mu.RUnlock() if !ok { return "", false } if time.Now().After(entry.expiresAt) { c.mu.Lock() delete(c.entries, sessionID) c.mu.Unlock() return "", false } return entry.authID, true} // GetAndRefresh retrieves the auth ID bound to a session and refreshes TTL on hit.// This extends the binding lifetime for active sessions.func (c *SessionCache) GetAndRefresh(sessionID string) (string, bool) { if sessionID == "" { return "", false } now := time.Now() c.mu.Lock() entry, ok := c.entries[sessionID] if !ok { c.mu.Unlock() return "", false } if now.After(entry.expiresAt) { delete(c.entries, sessionID) c.mu.Unlock() return "", false } // Refresh TTL on successful access entry.expiresAt = now.Add(c.ttl) c.entries[sessionID] = entry c.mu.Unlock() return entry.authID, true} // Set binds a session to an auth ID with TTL refresh.func (c *SessionCache) Set(sessionID, authID string) { if sessionID == "" || authID == "" { return } c.mu.Lock() c.entries[sessionID] = sessionEntry{ authID: authID, expiresAt: time.Now().Add(c.ttl), } c.mu.Unlock()} // Invalidate removes a specific session binding.func (c *SessionCache) Invalidate(sessionID string) { if sessionID == "" { return } c.mu.Lock() delete(c.entries, sessionID) c.mu.Unlock()} // InvalidateAuth removes all sessions bound to a specific auth ID.// Used when an auth becomes unavailable.func (c *SessionCache) InvalidateAuth(authID string) { if authID == "" { return } c.mu.Lock() for sid, entry := range c.entries { if entry.authID == authID { delete(c.entries, sid) } } c.mu.Unlock()} // Stop terminates the background cleanup goroutine.func (c *SessionCache) Stop() { select { case <-c.stopCh: default: close(c.stopCh) }} func (c *SessionCache) cleanupLoop() { ticker := time.NewTicker(c.ttl / 2) defer ticker.Stop() for { select { case <-c.stopCh: return case <-ticker.C: c.cleanup() } }} func (c *SessionCache) cleanup() { now := time.Now() c.mu.Lock() for sid, entry := range c.entries { if now.After(entry.expiresAt) { delete(c.entries, sid) } } c.mu.Unlock()}
package auth
import (
"sync"
"time"
)
// sessionEntry stores auth binding with expiration.
type sessionEntry struct {
authID string
expiresAt time.Time
}
// SessionCache provides TTL-based session to auth mapping with automatic cleanup.
type SessionCache struct {
mu sync.RWMutex
entries map[string]sessionEntry
ttl time.Duration
stopCh chan struct{}
}
// NewSessionCache creates a cache with the specified TTL.
// A background goroutine periodically cleans expired entries.
func NewSessionCache(ttl time.Duration) *SessionCache {
if ttl <= 0 {
ttl = 30 * time.Minute
}
c := &SessionCache{
entries: make(map[string]sessionEntry),
ttl: ttl,
stopCh: make(chan struct{}),
}
go c.cleanupLoop()
return c
}
// Get retrieves the auth ID bound to a session, if still valid.
// Does NOT refresh the TTL on access.
func (c *SessionCache) Get(sessionID string) (string, bool) {
if sessionID == "" {
return "", false
}
c.mu.RLock()
entry, ok := c.entries[sessionID]
c.mu.RUnlock()
if !ok {
return "", false
}
if time.Now().After(entry.expiresAt) {
c.mu.Lock()
delete(c.entries, sessionID)
c.mu.Unlock()
return "", false
}
return entry.authID, true
}
// GetAndRefresh retrieves the auth ID bound to a session and refreshes TTL on hit.
// This extends the binding lifetime for active sessions.
func (c *SessionCache) GetAndRefresh(sessionID string) (string, bool) {
if sessionID == "" {
return "", false
}
now := time.Now()
c.mu.Lock()
entry, ok := c.entries[sessionID]
if !ok {
c.mu.Unlock()
return "", false
}
if now.After(entry.expiresAt) {
delete(c.entries, sessionID)
c.mu.Unlock()
return "", false
}
// Refresh TTL on successful access
entry.expiresAt = now.Add(c.ttl)
c.entries[sessionID] = entry
c.mu.Unlock()
return entry.authID, true
}
// Set binds a session to an auth ID with TTL refresh.
func (c *SessionCache) Set(sessionID, authID string) {
if sessionID == "" || authID == "" {
return
}
c.mu.Lock()
c.entries[sessionID] = sessionEntry{
authID: authID,
expiresAt: time.Now().Add(c.ttl),
}
c.mu.Unlock()
}
// Invalidate removes a specific session binding.
func (c *SessionCache) Invalidate(sessionID string) {
if sessionID == "" {
return
}
c.mu.Lock()
delete(c.entries, sessionID)
c.mu.Unlock()
}
// InvalidateAuth removes all sessions bound to a specific auth ID.
// Used when an auth becomes unavailable.
func (c *SessionCache) InvalidateAuth(authID string) {
if authID == "" {
return
}
c.mu.Lock()
for sid, entry := range c.entries {
if entry.authID == authID {
delete(c.entries, sid)
}
}
c.mu.Unlock()
}
// Stop terminates the background cleanup goroutine.
func (c *SessionCache) Stop() {
select {
case <-c.stopCh:
default:
close(c.stopCh)
}
}
func (c *SessionCache) cleanupLoop() {
ticker := time.NewTicker(c.ttl / 2)
defer ticker.Stop()
for {
select {
case <-c.stopCh:
return
case <-ticker.C:
c.cleanup()
}
}
}
func (c *SessionCache) cleanup() {
now := time.Now()
c.mu.Lock()
for sid, entry := range c.entries {
if now.After(entry.expiresAt) {
delete(c.entries, sid)
}
}
c.mu.Unlock()
}