'use strict'; /** * Redis Cache Helper for Partner Authentication * Provides a distributed cache that can be shared across different processes */ const Redis = require('ioredis'); const env = require('./env'); const logger = require('./logger'); const pino = logger.child('redis_cache'); class RedisCache { constructor() { this.redis = null; this.isConnected = false; this.fallbackCache = new Map(); // In-memory fallback this.initialize(); } initialize() { try { this.redis = new Redis({ host: env.REDIS_HOST || 'localhost', port: env.REDIS_PORT || 6379, password: env.REDIS_PWD, retryDelayOnFailover: 100, enableReadyCheck: false, lazyConnect: true, maxRetriesPerRequest: 3 }); this.redis.on('connect', () => { this.isConnected = true; pino.info('Redis cache connected'); }); this.redis.on('error', (err) => { this.isConnected = false; pino.error({ err }, 'Redis cache error'); }); this.redis.on('close', () => { this.isConnected = false; pino.warn('Redis cache connection closed'); }); } catch (error) { pino.error({ err: error }, 'Failed to initialize Redis cache'); } } /** * Generate cache key for partner authentication * @param {string} partnerCode - Partner code (e.g., 'SATLOC') * @param {string} customerId - Customer ID * @returns {string} Cache key */ getAuthCacheKey(partnerCode, customerId) { return `partner:auth:${partnerCode}:${customerId}`; } /** * Set authentication data in cache with expiration * @param {string} partnerCode - Partner code * @param {string} customerId - Customer ID * @param {object} authData - Authentication data to cache * @param {number} ttlSeconds - Time to live in seconds (default: 1 hour) * @returns {Promise} Success status */ async setAuth(partnerCode, customerId, authData, ttlSeconds = 3600) { const key = this.getAuthCacheKey(partnerCode, customerId); const dataToCache = { ...authData, cachedAt: Date.now(), expiresAt: Date.now() + (ttlSeconds * 1000) }; if (this.isConnected) { try { const serializedData = JSON.stringify(dataToCache); await this.redis.setex(key, ttlSeconds, serializedData); pino.debug(`Cached auth data in Redis for ${partnerCode}:${customerId}`); return true; } catch (error) { pino.error({ err: error }, 'Failed to cache auth data in Redis'); } } // Fallback to in-memory cache this.fallbackCache.set(key, dataToCache); pino.debug(`Cached auth data in memory for ${partnerCode}:${customerId}`); return true; } /** * Get authentication data from cache * @param {string} partnerCode - Partner code * @param {string} customerId - Customer ID * @returns {Promise} Cached auth data or null */ async getAuth(partnerCode, customerId) { const key = this.getAuthCacheKey(partnerCode, customerId); if (this.isConnected) { try { const cachedData = await this.redis.get(key); if (cachedData) { const authData = JSON.parse(cachedData); pino.debug(`Retrieved cached auth data from Redis for ${partnerCode}:${customerId}`); return authData; } } catch (error) { pino.error({ err: error }, 'Failed to retrieve cached auth data from Redis'); } } // Fallback to in-memory cache const memoryData = this.fallbackCache.get(key); if (memoryData) { // Check if in-memory data is expired if (memoryData.expiresAt && memoryData.expiresAt > Date.now()) { pino.debug(`Retrieved cached auth data from memory for ${partnerCode}:${customerId}`); return memoryData; } else { // Remove expired data this.fallbackCache.delete(key); } } return null; } /** * Delete authentication data from cache * @param {string} partnerCode - Partner code * @param {string} customerId - Customer ID (optional, if not provided clears all for partner) * @returns {Promise} Success status */ async deleteAuth(partnerCode, customerId = null) { let deleted = false; if (this.isConnected) { try { if (customerId) { const key = this.getAuthCacheKey(partnerCode, customerId); await this.redis.del(key); pino.debug(`Deleted cached auth data from Redis for ${partnerCode}:${customerId}`); } else { // Delete all auth data for the partner const pattern = this.getAuthCacheKey(partnerCode, '*'); const keys = await this.redis.keys(pattern); if (keys.length > 0) { await this.redis.del(...keys); pino.debug(`Deleted ${keys.length} cached auth entries from Redis for ${partnerCode}`); } } deleted = true; } catch (error) { pino.error({ err: error }, 'Failed to delete cached auth data from Redis'); } } // Also clean from fallback cache if (customerId) { const key = this.getAuthCacheKey(partnerCode, customerId); this.fallbackCache.delete(key); } else { // Delete all entries for the partner from memory const keysToDelete = []; for (const key of this.fallbackCache.keys()) { if (key.startsWith(`partner:auth:${partnerCode}:`)) { keysToDelete.push(key); } } keysToDelete.forEach(key => this.fallbackCache.delete(key)); pino.debug(`Deleted ${keysToDelete.length} cached auth entries from memory for ${partnerCode}`); } return true; } /** * Check if auth data is still valid based on expiration time * @param {object} authData - Cached auth data * @param {number} healthCheckInterval - Health check interval in ms * @returns {boolean} Whether auth data is still valid */ isAuthValid(authData, healthCheckInterval = 30000) { if (!authData) return false; const now = Date.now(); return authData.expiresAt > now && authData.lastHealthCheck && (now - authData.lastHealthCheck) < healthCheckInterval; } /** * Close Redis connection */ async disconnect() { if (this.redis) { await this.redis.disconnect(); this.isConnected = false; pino.info('Redis cache disconnected'); } } } // Export singleton instance module.exports = new RedisCache();