// app/auth/cache.go // Кэш успешной валидации JWT-токенов (в памяти процесса). // Мотивация: Nubes-шлюз нестабильно отвечает на запросы с IP платформы // (то 200, то 401/403), из-за чего повторная валидация на каждый запрос // консоли роняла UI. Кэш: одна успешная проверка → TTL без обращений к шлюзу. // Безопасность: отозванный токен перестаёт работать максимум через TTL; // явный 401/403 сбрасывает запись немедленно. // Created: 2026-08-14 package auth import ( "context" "crypto/sha256" "encoding/hex" "errors" "sync" "time" ) // cacheTTL — срок жизни записи успешной валидации. const cacheTTL = 5 * time.Minute // cacheGraceTTL — продление при сетевой ошибке шлюза для ранее валидного токена. const cacheGraceTTL = 2 * time.Minute // cacheCleanupInterval — период фоновой очистки протухших записей. const cacheCleanupInterval = 5 * time.Minute type tokenCacheEntry struct { validUntil time.Time } var ( tokenCacheMu sync.RWMutex tokenCache = map[string]tokenCacheEntry{} ) // tokenCacheKey — компактный ключ кэша: hex(SHA256(token)). func tokenCacheKey(token string) string { h := sha256.Sum256([]byte(token)) return hex.EncodeToString(h[:]) } // PingNubesAPICached — валидация токена через шлюз с кэшем. // HIT (запись валидна) → nil без обращения к шлюзу. // MISS → свежая проверка: успех → запись с TTL; 401/403 → удаление записи и ошибка. // Сетевая ошибка: если запись была (пусть и протухшая) → grace-продление и nil, // иначе — ошибка (шлюз недоступен, кэша нет — справедливый отказ). func PingNubesAPICached(ctx context.Context, endpoint, token string) error { key := tokenCacheKey(token) now := time.Now() tokenCacheMu.RLock() entry, ok := tokenCache[key] tokenCacheMu.RUnlock() if ok && now.Before(entry.validUntil) { return nil // cache HIT } err := PingNubesAPI(ctx, endpoint, token) if errors.Is(err, ErrTokenRejected) { tokenCacheMu.Lock() delete(tokenCache, key) tokenCacheMu.Unlock() return err } if err != nil { // Сетевая ошибка шлюза: если токен ранее был валиден — даём grace. if ok { tokenCacheMu.Lock() tokenCache[key] = tokenCacheEntry{validUntil: time.Now().Add(cacheGraceTTL)} tokenCacheMu.Unlock() return nil } return err } tokenCacheMu.Lock() tokenCache[key] = tokenCacheEntry{validUntil: time.Now().Add(cacheTTL)} tokenCacheMu.Unlock() return nil } // WarmTokenCache — записывает токен в кэш как валидный без вызова шлюза. // Используется после успешной свежей проверки при логине (jwtAuth). func WarmTokenCache(token string) { tokenCacheMu.Lock() tokenCache[tokenCacheKey(token)] = tokenCacheEntry{validUntil: time.Now().Add(cacheTTL)} tokenCacheMu.Unlock() } var tokenCacheCleanupOnce sync.Once // StartTokenCacheCleanup — фоновая очистка протухших записей. Идемпотентна (sync.Once). func StartTokenCacheCleanup() { tokenCacheCleanupOnce.Do(func() { go func() { ticker := time.NewTicker(cacheCleanupInterval) defer ticker.Stop() for range ticker.C { now := time.Now() tokenCacheMu.Lock() for k, e := range tokenCache { if now.After(e.validUntil) { delete(tokenCache, k) } } tokenCacheMu.Unlock() } }() }) }