220 lines
6.5 KiB
JavaScript
220 lines
6.5 KiB
JavaScript
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();
|
||
}
|
||
}
|
||
|