Files
wallabicher/web/backend/services/redis.js

220 lines
6.5 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import redis from 'redis';
import yaml from 'yaml';
import { readFileSync, existsSync } from 'fs';
import bcrypt from 'bcrypt';
import { RateLimiterRedis } from 'rate-limiter-flexible';
import { PATHS } from '../config/constants.js';
import { RATE_LIMIT } from '../config/constants.js';
let redisClient = null;
let rateLimiter = null;
let config = null;
// Inicializar Redis si está configurado
export async function initRedis() {
try {
config = yaml.parse(readFileSync(PATHS.CONFIG, 'utf8'));
const cacheConfig = config?.cache;
if (cacheConfig?.type === 'redis') {
const redisConfig = cacheConfig.redis;
// En Docker, usar el nombre del servicio si no se especifica host
const redisHost = process.env.REDIS_HOST || redisConfig.host || 'localhost';
redisClient = redis.createClient({
socket: {
host: redisHost,
port: redisConfig.port || 6379,
},
password: redisConfig.password || undefined,
database: redisConfig.db || 0,
});
redisClient.on('error', (err) => console.error('Redis Client Error', err));
await redisClient.connect();
console.log('✅ Conectado a Redis');
// Inicializar rate limiter con Redis
try {
// Crear un cliente Redis adicional para el rate limiter
const rateLimiterClient = redis.createClient({
socket: {
host: redisHost,
port: redisConfig.port || 6379,
},
password: redisConfig.password || undefined,
database: redisConfig.db || 0,
});
await rateLimiterClient.connect();
rateLimiter = new RateLimiterRedis({
storeClient: rateLimiterClient,
keyPrefix: 'rl:',
points: RATE_LIMIT.POINTS,
duration: RATE_LIMIT.DURATION,
blockDuration: RATE_LIMIT.BLOCK_DURATION,
});
console.log('✅ Rate limiter inicializado con Redis');
} catch (error) {
console.error('Error inicializando rate limiter:', error.message);
}
// Inicializar usuario admin por defecto si no existe
await initDefaultAdmin();
} else {
console.log(' Redis no configurado, usando modo memoria');
console.log('⚠️ Rate limiting y autenticación requieren Redis');
}
} catch (error) {
console.error('Error inicializando Redis:', error.message);
}
}
// Inicializar usuario admin por defecto
async function initDefaultAdmin() {
if (!redisClient) return;
try {
const adminExists = await redisClient.exists('user:admin');
if (!adminExists) {
// Crear usuario admin por defecto con contraseña "admin"
// En producción, esto debería cambiarse
const defaultPassword = 'admin';
const hashedPassword = await bcrypt.hash(defaultPassword, 10);
await redisClient.hSet('user:admin', {
username: 'admin',
passwordHash: hashedPassword,
createdAt: new Date().toISOString(),
});
console.log('✅ Usuario admin creado por defecto (usuario: admin, contraseña: admin)');
console.log('⚠️ IMPORTANTE: Cambia la contraseña por defecto en producción');
}
} catch (error) {
console.error('Error inicializando usuario admin:', error.message);
}
}
// Getters
export function getRedisClient() {
return redisClient;
}
export function getRateLimiter() {
return rateLimiter;
}
export function getConfig() {
return config;
}
export function reloadConfig() {
try {
config = yaml.parse(readFileSync(PATHS.CONFIG, 'utf8'));
return config;
} catch (error) {
console.error('Error recargando configuración:', error.message);
return config;
}
}
// Funciones de utilidad para artículos
export async function getNotifiedArticles() {
if (!redisClient) {
return [];
}
try {
const keys = await redisClient.keys('notified:*');
const articles = [];
for (const key of keys) {
const parts = key.split(':');
if (parts.length >= 3) {
const platform = parts[1];
const id = parts.slice(2).join(':');
const ttl = await redisClient.ttl(key);
const value = await redisClient.get(key);
// Intentar parsear como JSON (nuevo formato con toda la info)
let articleData = {};
try {
if (value && value !== '1') {
articleData = JSON.parse(value);
}
} catch (e) {
// Si no es JSON válido, usar valor por defecto
}
articles.push({
platform: articleData.platform || platform,
id: articleData.id || id,
title: articleData.title || null,
description: articleData.description || null,
price: articleData.price || null,
currency: articleData.currency || null,
location: articleData.location || null,
allows_shipping: articleData.allows_shipping !== undefined ? articleData.allows_shipping : null,
url: articleData.url || null,
images: articleData.images || [],
modified_at: articleData.modified_at || null,
notifiedAt: Date.now() - (7 * 24 * 60 * 60 - ttl) * 1000,
expiresAt: Date.now() + ttl * 1000,
});
}
}
return articles;
} catch (error) {
console.error('Error obteniendo artículos de Redis:', error.message);
return [];
}
}
export async function getFavorites() {
if (!redisClient) {
return [];
}
try {
const keys = await redisClient.keys('notified:*');
const favorites = [];
for (const key of keys) {
const value = await redisClient.get(key);
if (value) {
try {
const articleData = JSON.parse(value);
if (articleData.is_favorite === true) {
favorites.push(articleData);
}
} catch (e) {
// Si no es JSON válido, ignorar
}
}
}
return favorites;
} catch (error) {
console.error('Error obteniendo favoritos de Redis:', error.message);
return [];
}
}
// Inicializar claves conocidas para evitar notificar artículos existentes
export async function initNotifiedArticleKeys() {
if (!redisClient) {
return new Set();
}
try {
const initialKeys = await redisClient.keys('notified:*');
const keysSet = new Set(initialKeys);
console.log(`📋 ${keysSet.size} artículos ya notificados detectados`);
return keysSet;
} catch (error) {
console.error('Error inicializando claves de artículos:', error.message);
return new Set();
}
}