erp-core-backend-v2/src/config/redis.ts
rckrdmrd 3ce5c6ad17 Migración desde erp-core/backend - Estándar multi-repo v2
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-16 08:10:37 -06:00

179 lines
4.5 KiB
TypeScript

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<boolean> - true si la conexión fue exitosa
*/
export async function initializeRedis(): Promise<boolean> {
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<void> {
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<void> {
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<boolean> - true si el token está en blacklist
*/
export async function isTokenBlacklisted(token: string): Promise<boolean> {
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<void> {
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,
});
}
}