feat: implement user authentication and login modal, refactor backend
This commit is contained in:
219
web/backend/services/redis.js
Normal file
219
web/backend/services/redis.js
Normal file
@@ -0,0 +1,219 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user