import Redis from 'ioredis'; import { logger } from '../shared/utils/logger.js'; /** * Configuración de Redis para blacklist de tokens JWT */ const redisConfig = { host: process.env.REDIS_HOST || 'localhost', port: parseInt(process.env.REDIS_PORT || '6379', 10), password: process.env.REDIS_PASSWORD || undefined, // Configuración de reconexión retryStrategy(times: number) { const delay = Math.min(times * 50, 2000); return delay; }, // Timeouts connectTimeout: 10000, maxRetriesPerRequest: 3, // Logging de eventos lazyConnect: true, // No conectar automáticamente, esperar a connect() }; /** * Cliente Redis para blacklist de tokens */ export const redisClient = new Redis(redisConfig); // Event listeners redisClient.on('connect', () => { logger.info('Redis client connecting...', { host: redisConfig.host, port: redisConfig.port, }); }); redisClient.on('ready', () => { logger.info('Redis client ready'); }); redisClient.on('error', (error) => { logger.error('Redis client error', { error: error.message, stack: error.stack, }); }); redisClient.on('close', () => { logger.warn('Redis connection closed'); }); redisClient.on('reconnecting', () => { logger.info('Redis client reconnecting...'); }); /** * Inicializa la conexión a Redis * @returns Promise - true si la conexión fue exitosa */ export async function initializeRedis(): Promise { try { await redisClient.connect(); // Test de conexión await redisClient.ping(); logger.info('Redis connection successful', { host: redisConfig.host, port: redisConfig.port, }); return true; } catch (error) { logger.error('Failed to connect to Redis', { error: (error as Error).message, host: redisConfig.host, port: redisConfig.port, }); // Redis es opcional, no debe detener la app logger.warn('Application will continue without Redis (token blacklist disabled)'); return false; } } /** * Cierra la conexión a Redis */ export async function closeRedis(): Promise { try { await redisClient.quit(); logger.info('Redis connection closed gracefully'); } catch (error) { logger.error('Error closing Redis connection', { error: (error as Error).message, }); // Forzar desconexión si quit() falla redisClient.disconnect(); } } /** * Verifica si Redis está conectado */ export function isRedisConnected(): boolean { return redisClient.status === 'ready'; } // ===== Utilidades para Token Blacklist ===== /** * Agrega un token a la blacklist * @param token - Token JWT a invalidar * @param expiresIn - Tiempo de expiración en segundos */ export async function blacklistToken(token: string, expiresIn: number): Promise { if (!isRedisConnected()) { logger.warn('Cannot blacklist token: Redis not connected'); return; } try { const key = `blacklist:${token}`; await redisClient.setex(key, expiresIn, '1'); logger.debug('Token added to blacklist', { expiresIn }); } catch (error) { logger.error('Error blacklisting token', { error: (error as Error).message, }); } } /** * Verifica si un token está en la blacklist * @param token - Token JWT a verificar * @returns Promise - true si el token está en blacklist */ export async function isTokenBlacklisted(token: string): Promise { if (!isRedisConnected()) { logger.warn('Cannot check blacklist: Redis not connected'); return false; // Si Redis no está disponible, permitir el acceso } try { const key = `blacklist:${token}`; const result = await redisClient.get(key); return result !== null; } catch (error) { logger.error('Error checking token blacklist', { error: (error as Error).message, }); return false; // En caso de error, no bloquear el acceso } } /** * Limpia tokens expirados de la blacklist * Nota: Redis hace esto automáticamente con SETEX, esta función es para uso manual si es necesario */ export async function cleanupBlacklist(): Promise { if (!isRedisConnected()) { logger.warn('Cannot cleanup blacklist: Redis not connected'); return; } try { // Redis maneja automáticamente la expiración con SETEX // Esta función está disponible para limpieza manual si se necesita logger.info('Blacklist cleanup completed (handled by Redis TTL)'); } catch (error) { logger.error('Error during blacklist cleanup', { error: (error as Error).message, }); } }