mongodb
Signed-off-by: Omar Sánchez Pizarro <omar.sanchez@pistacero.net>
This commit is contained in:
@@ -1,16 +1,50 @@
|
||||
import express from 'express';
|
||||
import { getNotifiedArticles } from '../services/redis.js';
|
||||
import { getNotifiedArticles } from '../services/mongodb.js';
|
||||
import { basicAuthMiddleware } from '../middlewares/auth.js';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// Obtener artículos notificados
|
||||
router.get('/', async (req, res) => {
|
||||
router.delete('/', async (req, res) => {
|
||||
try {
|
||||
const articles = await getNotifiedArticles();
|
||||
const count = await clearAllArticles();
|
||||
res.json({ success: true, message: `Todos los artículos eliminados: ${count} artículos borrados`, count });
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Obtener artículos notificados (requiere autenticación obligatoria)
|
||||
router.get('/', basicAuthMiddleware, async (req, res) => {
|
||||
try {
|
||||
// Obtener usuario autenticado (requerido)
|
||||
const user = req.user;
|
||||
const isAdmin = user.role === 'admin';
|
||||
|
||||
// Construir filtro
|
||||
const filter = {};
|
||||
|
||||
// Si no es admin, solo mostrar sus artículos
|
||||
if (!isAdmin) {
|
||||
filter.username = user.username;
|
||||
} else if (req.query.username) {
|
||||
// Admin puede filtrar por username
|
||||
filter.username = req.query.username;
|
||||
}
|
||||
|
||||
if (req.query.worker_name) filter.worker_name = req.query.worker_name;
|
||||
if (req.query.platform) filter.platform = req.query.platform;
|
||||
|
||||
const articles = await getNotifiedArticles(filter);
|
||||
|
||||
const limit = parseInt(req.query.limit) || 100;
|
||||
const offset = parseInt(req.query.offset) || 0;
|
||||
|
||||
const sorted = articles.sort((a, b) => b.notifiedAt - a.notifiedAt);
|
||||
// Los artículos ya vienen ordenados de MongoDB, pero asegurémonos
|
||||
const sorted = articles.sort((a, b) => {
|
||||
const aTime = typeof a.notifiedAt === 'number' ? a.notifiedAt : new Date(a.notifiedAt).getTime();
|
||||
const bTime = typeof b.notifiedAt === 'number' ? b.notifiedAt : new Date(b.notifiedAt).getTime();
|
||||
return bTime - aTime;
|
||||
});
|
||||
const paginated = sorted.slice(offset, offset + limit);
|
||||
|
||||
res.json({
|
||||
@@ -24,16 +58,34 @@ router.get('/', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// Buscar artículos en Redis
|
||||
router.get('/search', async (req, res) => {
|
||||
// Buscar artículos en MongoDB (requiere autenticación obligatoria)
|
||||
router.get('/search', basicAuthMiddleware, async (req, res) => {
|
||||
try {
|
||||
const query = req.query.q || '';
|
||||
if (!query.trim()) {
|
||||
return res.json({ articles: [], total: 0 });
|
||||
}
|
||||
|
||||
// Obtener usuario autenticado (requerido)
|
||||
const user = req.user;
|
||||
const isAdmin = user.role === 'admin';
|
||||
|
||||
// Construir filtro adicional si se proporciona
|
||||
const filter = {};
|
||||
|
||||
// Si no es admin, solo buscar sus artículos
|
||||
if (!isAdmin) {
|
||||
filter.username = user.username;
|
||||
} else if (req.query.username) {
|
||||
// Admin puede filtrar por username
|
||||
filter.username = req.query.username;
|
||||
}
|
||||
|
||||
if (req.query.worker_name) filter.worker_name = req.query.worker_name;
|
||||
if (req.query.platform) filter.platform = req.query.platform;
|
||||
|
||||
const searchTerm = query.toLowerCase().trim();
|
||||
const allArticles = await getNotifiedArticles();
|
||||
const allArticles = await getNotifiedArticles(filter);
|
||||
|
||||
// Filtrar artículos que coincidan con la búsqueda
|
||||
const filtered = allArticles.filter(article => {
|
||||
@@ -61,11 +113,23 @@ router.get('/search', async (req, res) => {
|
||||
const id = String(article.id || '').toLowerCase();
|
||||
if (id.includes(searchTerm)) return true;
|
||||
|
||||
// Buscar en username
|
||||
const username = (article.username || '').toLowerCase();
|
||||
if (username.includes(searchTerm)) return true;
|
||||
|
||||
// Buscar en worker_name
|
||||
const worker_name = (article.worker_name || '').toLowerCase();
|
||||
if (worker_name.includes(searchTerm)) return true;
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
// Ordenar por fecha de notificación (más recientes primero)
|
||||
const sorted = filtered.sort((a, b) => b.notifiedAt - a.notifiedAt);
|
||||
const sorted = filtered.sort((a, b) => {
|
||||
const aTime = typeof a.notifiedAt === 'number' ? a.notifiedAt : new Date(a.notifiedAt).getTime();
|
||||
const bTime = typeof b.notifiedAt === 'number' ? b.notifiedAt : new Date(b.notifiedAt).getTime();
|
||||
return bTime - aTime;
|
||||
});
|
||||
|
||||
res.json({
|
||||
articles: sorted,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import express from 'express';
|
||||
import { basicAuthMiddleware } from '../middlewares/auth.js';
|
||||
import { getConfig, reloadConfig } from '../services/redis.js';
|
||||
import { getConfig, reloadConfig } from '../services/mongodb.js';
|
||||
import { readFileSync } from 'fs';
|
||||
import yaml from 'yaml';
|
||||
import { PATHS } from '../config/constants.js';
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
import express from 'express';
|
||||
import { getFavorites, getRedisClient } from '../services/redis.js';
|
||||
import { getFavorites, getDB, updateArticleFavorite, getArticle } from '../services/mongodb.js';
|
||||
import { basicAuthMiddleware } from '../middlewares/auth.js';
|
||||
import { broadcast } from '../services/websocket.js';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// Obtener favoritos
|
||||
router.get('/', async (req, res) => {
|
||||
// Obtener favoritos (requiere autenticación)
|
||||
router.get('/', basicAuthMiddleware, async (req, res) => {
|
||||
try {
|
||||
const favorites = await getFavorites();
|
||||
// Obtener usuario autenticado (requerido)
|
||||
const user = req.user;
|
||||
|
||||
// Todos los usuarios (incluidos admins) ven solo sus propios favoritos
|
||||
const username = user.username;
|
||||
const favorites = await getFavorites(username);
|
||||
res.json(favorites);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
@@ -18,41 +23,40 @@ router.get('/', async (req, res) => {
|
||||
// Añadir favorito (requiere autenticación)
|
||||
router.post('/', basicAuthMiddleware, async (req, res) => {
|
||||
try {
|
||||
const redisClient = getRedisClient();
|
||||
if (!redisClient) {
|
||||
return res.status(500).json({ error: 'Redis no está disponible' });
|
||||
const db = getDB();
|
||||
if (!db) {
|
||||
return res.status(500).json({ error: 'MongoDB no está disponible' });
|
||||
}
|
||||
|
||||
// Verificar que el usuario está autenticado (middleware ya lo valida, pero doble verificación)
|
||||
if (!req.user || !req.user.username) {
|
||||
return res.status(401).json({ error: 'Se requiere autenticación', message: 'Se requiere autenticación para marcar favoritos' });
|
||||
}
|
||||
|
||||
const username = req.user.username;
|
||||
const { platform, id } = req.body;
|
||||
if (!platform || !id) {
|
||||
return res.status(400).json({ error: 'platform e id son requeridos' });
|
||||
}
|
||||
|
||||
const key = `notified:${platform}:${id}`;
|
||||
const value = await redisClient.get(key);
|
||||
// Convertir id a string para consistencia
|
||||
const idStr = String(id);
|
||||
|
||||
if (!value) {
|
||||
return res.status(404).json({ error: 'Artículo no encontrado' });
|
||||
// Verificar si el artículo existe
|
||||
const article = await getArticle(platform, idStr);
|
||||
if (!article) {
|
||||
return res.status(404).json({ error: `Artículo no encontrado en MongoDB: ${platform}:${idStr}` });
|
||||
}
|
||||
|
||||
try {
|
||||
const articleData = JSON.parse(value);
|
||||
articleData.is_favorite = true;
|
||||
// Mantener el TTL existente
|
||||
const ttl = await redisClient.ttl(key);
|
||||
if (ttl > 0) {
|
||||
await redisClient.setex(key, ttl, JSON.stringify(articleData));
|
||||
} else {
|
||||
await redisClient.set(key, JSON.stringify(articleData));
|
||||
}
|
||||
|
||||
const favorites = await getFavorites();
|
||||
broadcast({ type: 'favorites_updated', data: favorites });
|
||||
res.json({ success: true, favorites });
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: 'Error procesando artículo' });
|
||||
}
|
||||
// Actualizar favorito para el usuario autenticado
|
||||
await updateArticleFavorite(platform, idStr, true, username);
|
||||
|
||||
// Obtener favoritos del usuario autenticado (todos ven solo los suyos)
|
||||
const favorites = await getFavorites(username);
|
||||
broadcast({ type: 'favorites_updated', data: favorites, username });
|
||||
res.json({ success: true, favorites });
|
||||
} catch (error) {
|
||||
console.error('Error en POST /favorites:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
@@ -60,37 +64,35 @@ router.post('/', basicAuthMiddleware, async (req, res) => {
|
||||
// Eliminar favorito (requiere autenticación)
|
||||
router.delete('/:platform/:id', basicAuthMiddleware, async (req, res) => {
|
||||
try {
|
||||
const redisClient = getRedisClient();
|
||||
if (!redisClient) {
|
||||
return res.status(500).json({ error: 'Redis no está disponible' });
|
||||
const db = getDB();
|
||||
if (!db) {
|
||||
return res.status(500).json({ error: 'MongoDB no está disponible' });
|
||||
}
|
||||
|
||||
const { platform, id } = req.params;
|
||||
const key = `notified:${platform}:${id}`;
|
||||
const value = await redisClient.get(key);
|
||||
// Verificar que el usuario está autenticado (middleware ya lo valida, pero doble verificación)
|
||||
if (!req.user || !req.user.username) {
|
||||
return res.status(401).json({ error: 'Se requiere autenticación', message: 'Se requiere autenticación para eliminar favoritos' });
|
||||
}
|
||||
|
||||
if (!value) {
|
||||
const username = req.user.username;
|
||||
const { platform, id } = req.params;
|
||||
const idStr = String(id);
|
||||
|
||||
// Verificar si el artículo existe
|
||||
const article = await getArticle(platform, idStr);
|
||||
if (!article) {
|
||||
return res.status(404).json({ error: 'Artículo no encontrado' });
|
||||
}
|
||||
|
||||
try {
|
||||
const articleData = JSON.parse(value);
|
||||
articleData.is_favorite = false;
|
||||
// Mantener el TTL existente
|
||||
const ttl = await redisClient.ttl(key);
|
||||
if (ttl > 0) {
|
||||
await redisClient.setex(key, ttl, JSON.stringify(articleData));
|
||||
} else {
|
||||
await redisClient.set(key, JSON.stringify(articleData));
|
||||
}
|
||||
|
||||
const favorites = await getFavorites();
|
||||
broadcast({ type: 'favorites_updated', data: favorites });
|
||||
res.json({ success: true, favorites });
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: 'Error procesando artículo' });
|
||||
}
|
||||
// Actualizar favorito para el usuario autenticado
|
||||
await updateArticleFavorite(platform, idStr, false, username);
|
||||
|
||||
// Obtener favoritos del usuario autenticado (todos ven solo los suyos)
|
||||
const favorites = await getFavorites(username);
|
||||
broadcast({ type: 'favorites_updated', data: favorites, username });
|
||||
res.json({ success: true, favorites });
|
||||
} catch (error) {
|
||||
console.error('Error en DELETE /favorites:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,22 +1,57 @@
|
||||
import express from 'express';
|
||||
import { readJSON } from '../utils/fileUtils.js';
|
||||
import { PATHS } from '../config/constants.js';
|
||||
import { getFavorites, getNotifiedArticles, getRedisClient } from '../services/redis.js';
|
||||
import { getFavorites, getNotifiedArticles, getDB, getWorkers, clearAllArticles } from '../services/mongodb.js';
|
||||
import { basicAuthMiddleware } from '../middlewares/auth.js';
|
||||
import { adminAuthMiddleware } from '../middlewares/adminAuth.js';
|
||||
import { broadcast } from '../services/websocket.js';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// Obtener estadísticas
|
||||
router.get('/stats', async (req, res) => {
|
||||
// Obtener estadísticas (requiere autenticación obligatoria)
|
||||
router.get('/stats', basicAuthMiddleware, async (req, res) => {
|
||||
try {
|
||||
const workers = readJSON(PATHS.WORKERS, { items: [] });
|
||||
const favorites = await getFavorites();
|
||||
const notifiedArticles = await getNotifiedArticles();
|
||||
const db = getDB();
|
||||
if (!db) {
|
||||
return res.status(500).json({ error: 'MongoDB no está disponible' });
|
||||
}
|
||||
|
||||
// Obtener usuario autenticado (requerido)
|
||||
const user = req.user;
|
||||
const isAdmin = user.role === 'admin';
|
||||
|
||||
let totalWorkers = 0;
|
||||
let activeWorkers = 0;
|
||||
let favorites = [];
|
||||
let notifiedArticles = [];
|
||||
|
||||
if (isAdmin) {
|
||||
// Admin: estadísticas globales de todos los usuarios
|
||||
const workersCollection = db.collection('workers');
|
||||
const allWorkers = await workersCollection.find({}).toArray();
|
||||
|
||||
for (const userWorkers of allWorkers) {
|
||||
const items = userWorkers.items || [];
|
||||
const disabled = userWorkers.disabled || [];
|
||||
totalWorkers += items.length;
|
||||
activeWorkers += items.filter(w => !disabled.includes(w.name) && !disabled.includes(w.id)).length;
|
||||
}
|
||||
|
||||
favorites = await getFavorites(null); // Todos los favoritos
|
||||
notifiedArticles = await getNotifiedArticles(); // Todos los artículos
|
||||
} else {
|
||||
// Usuario normal: solo sus estadísticas
|
||||
const workers = await getWorkers(user.username);
|
||||
const items = workers.items || [];
|
||||
const disabled = workers.disabled || [];
|
||||
totalWorkers = items.length;
|
||||
activeWorkers = items.filter(w => !disabled.includes(w.name) && !disabled.includes(w.id)).length;
|
||||
|
||||
favorites = await getFavorites(user.username); // Solo sus favoritos
|
||||
notifiedArticles = await getNotifiedArticles({ username: user.username }); // Solo sus artículos
|
||||
}
|
||||
|
||||
const stats = {
|
||||
totalWorkers: workers.items?.length || 0,
|
||||
activeWorkers: (workers.items || []).filter(w => !workers.disabled?.includes(w.name)).length,
|
||||
totalWorkers,
|
||||
activeWorkers,
|
||||
totalFavorites: favorites.length,
|
||||
totalNotified: notifiedArticles.length,
|
||||
platforms: {
|
||||
@@ -27,34 +62,21 @@ router.get('/stats', async (req, res) => {
|
||||
|
||||
res.json(stats);
|
||||
} catch (error) {
|
||||
console.error('Error obteniendo estadísticas:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Limpiar toda la caché de Redis (requiere autenticación)
|
||||
router.delete('/cache', basicAuthMiddleware, async (req, res) => {
|
||||
// Limpiar toda la caché de MongoDB (requiere autenticación de administrador)
|
||||
router.delete('/cache', basicAuthMiddleware, adminAuthMiddleware, async (req, res) => {
|
||||
try {
|
||||
const redisClient = getRedisClient();
|
||||
if (!redisClient) {
|
||||
return res.status(500).json({ error: 'Redis no está disponible' });
|
||||
const db = getDB();
|
||||
if (!db) {
|
||||
return res.status(500).json({ error: 'MongoDB no está disponible' });
|
||||
}
|
||||
|
||||
// Obtener todas las claves que empiezan con 'notified:'
|
||||
const keys = await redisClient.keys('notified:*');
|
||||
|
||||
if (!keys || keys.length === 0) {
|
||||
return res.json({
|
||||
success: true,
|
||||
message: 'Cache ya está vacío',
|
||||
count: 0
|
||||
});
|
||||
}
|
||||
|
||||
// Eliminar todas las claves
|
||||
const count = keys.length;
|
||||
for (const key of keys) {
|
||||
await redisClient.del(key);
|
||||
}
|
||||
// Eliminar todos los artículos
|
||||
const count = await clearAllArticles();
|
||||
|
||||
// Notificar a los clientes WebSocket
|
||||
broadcast({
|
||||
@@ -62,17 +84,55 @@ router.delete('/cache', basicAuthMiddleware, async (req, res) => {
|
||||
data: { count, timestamp: Date.now() }
|
||||
});
|
||||
|
||||
// También notificar actualización de artículos (ahora está vacío)
|
||||
broadcast({ type: 'articles_updated', data: [] });
|
||||
|
||||
// También actualizar favoritos (debería estar vacío ahora)
|
||||
const favorites = await getFavorites();
|
||||
broadcast({ type: 'favorites_updated', data: favorites });
|
||||
const favorites = await getFavorites(null);
|
||||
broadcast({ type: 'favorites_updated', data: favorites, username: null });
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: `Cache limpiado: ${count} artículos eliminados`,
|
||||
message: `Todos los artículos eliminados: ${count} artículos borrados`,
|
||||
count
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error limpiando cache de Redis:', error);
|
||||
console.error('Error limpiando cache de MongoDB:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Endpoint específico para borrar artículos (alias de /cache para claridad)
|
||||
router.delete('/articles', basicAuthMiddleware, adminAuthMiddleware, async (req, res) => {
|
||||
try {
|
||||
const db = getDB();
|
||||
if (!db) {
|
||||
return res.status(500).json({ error: 'MongoDB no está disponible' });
|
||||
}
|
||||
|
||||
// Eliminar todos los artículos
|
||||
const count = await clearAllArticles();
|
||||
|
||||
// Notificar a los clientes WebSocket
|
||||
broadcast({
|
||||
type: 'articles_cleared',
|
||||
data: { count, timestamp: Date.now() }
|
||||
});
|
||||
|
||||
// También notificar actualización de artículos (ahora está vacío)
|
||||
broadcast({ type: 'articles_updated', data: [] });
|
||||
|
||||
// También actualizar favoritos (debería estar vacío ahora)
|
||||
const favorites = await getFavorites(null);
|
||||
broadcast({ type: 'favorites_updated', data: favorites, username: null });
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: `Todos los artículos eliminados: ${count} artículos borrados`,
|
||||
count
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error borrando artículos:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import express from 'express';
|
||||
import { basicAuthMiddleware } from '../middlewares/auth.js';
|
||||
import { adminAuthMiddleware } from '../middlewares/adminAuth.js';
|
||||
import { getLogPath, readLogs } from '../utils/fileUtils.js';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// Obtener logs (últimas líneas o nuevas líneas desde un número de línea)
|
||||
router.get('/', basicAuthMiddleware, (req, res) => {
|
||||
// Obtener logs (requiere autenticación de administrador obligatoria)
|
||||
router.get('/', basicAuthMiddleware, adminAuthMiddleware, (req, res) => {
|
||||
try {
|
||||
const logPath = getLogPath();
|
||||
const sinceLine = parseInt(req.query.since) || 0;
|
||||
|
||||
@@ -1,27 +1,66 @@
|
||||
import express from 'express';
|
||||
import { basicAuthMiddleware } from '../middlewares/auth.js';
|
||||
import { getConfig, reloadConfig } from '../services/redis.js';
|
||||
import { readFileSync } from 'fs';
|
||||
import yaml from 'yaml';
|
||||
import { PATHS } from '../config/constants.js';
|
||||
import { getTelegramConfig, setTelegramConfig } from '../services/mongodb.js';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// Obtener configuración de Telegram del usuario autenticado
|
||||
router.get('/config', basicAuthMiddleware, async (req, res) => {
|
||||
try {
|
||||
const username = req.user.username;
|
||||
const config = await getTelegramConfig(username);
|
||||
|
||||
if (!config) {
|
||||
return res.json({
|
||||
token: '',
|
||||
channel: '',
|
||||
enable_polling: false
|
||||
});
|
||||
}
|
||||
|
||||
res.json(config);
|
||||
} catch (error) {
|
||||
console.error('Error obteniendo configuración de Telegram:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Guardar configuración de Telegram del usuario autenticado
|
||||
router.put('/config', basicAuthMiddleware, async (req, res) => {
|
||||
try {
|
||||
const username = req.user.username;
|
||||
const { token, channel, enable_polling } = req.body;
|
||||
|
||||
if (!token || !channel) {
|
||||
return res.status(400).json({ error: 'Token y channel son requeridos' });
|
||||
}
|
||||
|
||||
await setTelegramConfig(username, {
|
||||
token,
|
||||
channel,
|
||||
enable_polling: enable_polling || false
|
||||
});
|
||||
|
||||
res.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error('Error guardando configuración de Telegram:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Obtener threads/topics de Telegram
|
||||
router.get('/threads', basicAuthMiddleware, async (req, res) => {
|
||||
try {
|
||||
let config = getConfig();
|
||||
if (!config) {
|
||||
config = yaml.parse(readFileSync(PATHS.CONFIG, 'utf8'));
|
||||
}
|
||||
const username = req.user.username;
|
||||
const config = await getTelegramConfig(username);
|
||||
|
||||
const token = config?.telegram_token;
|
||||
const channel = config?.telegram_channel;
|
||||
|
||||
if (!token || !channel) {
|
||||
if (!config || !config.token || !config.channel) {
|
||||
return res.status(400).json({ error: 'Token o canal de Telegram no configurados' });
|
||||
}
|
||||
|
||||
const token = config.token;
|
||||
const channel = config.channel;
|
||||
|
||||
// Convertir el canal a chat_id si es necesario
|
||||
let chatId = channel;
|
||||
if (channel.startsWith('@')) {
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
import express from 'express';
|
||||
import bcrypt from 'bcrypt';
|
||||
import { getRedisClient } from '../services/redis.js';
|
||||
import { getDB, getUser, createUser, deleteUser as deleteUserFromDB, getAllUsers, updateUserPassword } from '../services/mongodb.js';
|
||||
import { basicAuthMiddleware, createSession, invalidateSession, invalidateUserSessions } from '../middlewares/auth.js';
|
||||
import { adminAuthMiddleware } from '../middlewares/adminAuth.js';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// Endpoint de login (público)
|
||||
router.post('/login', async (req, res) => {
|
||||
try {
|
||||
const redisClient = getRedisClient();
|
||||
if (!redisClient) {
|
||||
return res.status(500).json({ error: 'Redis no está disponible' });
|
||||
const db = getDB();
|
||||
if (!db) {
|
||||
return res.status(500).json({ error: 'MongoDB no está disponible' });
|
||||
}
|
||||
|
||||
const { username, password } = req.body;
|
||||
@@ -19,17 +20,15 @@ router.post('/login', async (req, res) => {
|
||||
return res.status(400).json({ error: 'username y password son requeridos' });
|
||||
}
|
||||
|
||||
// Buscar usuario en Redis
|
||||
const userKey = `user:${username}`;
|
||||
const userExists = await redisClient.exists(userKey);
|
||||
// Buscar usuario en MongoDB
|
||||
const user = await getUser(username);
|
||||
|
||||
if (!userExists) {
|
||||
if (!user) {
|
||||
return res.status(401).json({ error: 'Invalid credentials', message: 'Usuario o contraseña incorrectos' });
|
||||
}
|
||||
|
||||
// Obtener hash de la contraseña
|
||||
const userData = await redisClient.hGetAll(userKey);
|
||||
const passwordHash = userData.passwordHash;
|
||||
const passwordHash = user.passwordHash;
|
||||
|
||||
if (!passwordHash) {
|
||||
return res.status(401).json({ error: 'Invalid credentials', message: 'Usuario o contraseña incorrectos' });
|
||||
@@ -45,11 +44,15 @@ router.post('/login', async (req, res) => {
|
||||
// Crear sesión/token
|
||||
const token = await createSession(username);
|
||||
|
||||
console.log(`✅ Login exitoso: ${username}`);
|
||||
// Obtener rol del usuario
|
||||
const userRole = user.role || 'user';
|
||||
|
||||
console.log(`✅ Login exitoso: ${username} (${userRole})`);
|
||||
res.json({
|
||||
success: true,
|
||||
token,
|
||||
username,
|
||||
role: userRole,
|
||||
message: 'Login exitoso'
|
||||
});
|
||||
} catch (error) {
|
||||
@@ -78,9 +81,11 @@ router.post('/logout', basicAuthMiddleware, async (req, res) => {
|
||||
// Verificar token (para validar si la sesión sigue activa)
|
||||
router.get('/me', basicAuthMiddleware, async (req, res) => {
|
||||
try {
|
||||
const user = await getUser(req.user.username);
|
||||
res.json({
|
||||
success: true,
|
||||
username: req.user.username,
|
||||
role: user?.role || 'user',
|
||||
authenticated: true
|
||||
});
|
||||
} catch (error) {
|
||||
@@ -91,9 +96,9 @@ router.get('/me', basicAuthMiddleware, async (req, res) => {
|
||||
// Cambiar contraseña de usuario
|
||||
router.post('/change-password', basicAuthMiddleware, async (req, res) => {
|
||||
try {
|
||||
const redisClient = getRedisClient();
|
||||
if (!redisClient) {
|
||||
return res.status(500).json({ error: 'Redis no está disponible' });
|
||||
const db = getDB();
|
||||
if (!db) {
|
||||
return res.status(500).json({ error: 'MongoDB no está disponible' });
|
||||
}
|
||||
|
||||
const { currentPassword, newPassword } = req.body;
|
||||
@@ -107,26 +112,21 @@ router.post('/change-password', basicAuthMiddleware, async (req, res) => {
|
||||
return res.status(400).json({ error: 'La nueva contraseña debe tener al menos 6 caracteres' });
|
||||
}
|
||||
|
||||
const userKey = `user:${username}`;
|
||||
const userData = await redisClient.hGetAll(userKey);
|
||||
const user = await getUser(username);
|
||||
|
||||
if (!userData || !userData.passwordHash) {
|
||||
if (!user || !user.passwordHash) {
|
||||
return res.status(404).json({ error: 'Usuario no encontrado' });
|
||||
}
|
||||
|
||||
// Verificar contraseña actual
|
||||
const match = await bcrypt.compare(currentPassword, userData.passwordHash);
|
||||
const match = await bcrypt.compare(currentPassword, user.passwordHash);
|
||||
if (!match) {
|
||||
return res.status(401).json({ error: 'Contraseña actual incorrecta' });
|
||||
}
|
||||
|
||||
// Hashear nueva contraseña y actualizar
|
||||
const newPasswordHash = await bcrypt.hash(newPassword, 10);
|
||||
await redisClient.hSet(userKey, {
|
||||
...userData,
|
||||
passwordHash: newPasswordHash,
|
||||
updatedAt: new Date().toISOString(),
|
||||
});
|
||||
await updateUserPassword(username, newPasswordHash);
|
||||
|
||||
// Invalidar todas las sesiones del usuario (requiere nuevo login)
|
||||
await invalidateUserSessions(username);
|
||||
@@ -139,40 +139,37 @@ router.post('/change-password', basicAuthMiddleware, async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// Obtener lista de usuarios (requiere autenticación admin)
|
||||
// Obtener lista de usuarios (requiere autenticación, admin ve todos, user ve solo suyo)
|
||||
router.get('/', basicAuthMiddleware, async (req, res) => {
|
||||
try {
|
||||
const redisClient = getRedisClient();
|
||||
if (!redisClient) {
|
||||
return res.status(500).json({ error: 'Redis no está disponible' });
|
||||
const db = getDB();
|
||||
if (!db) {
|
||||
return res.status(500).json({ error: 'MongoDB no está disponible' });
|
||||
}
|
||||
|
||||
// Obtener todas las claves de usuarios
|
||||
const userKeys = await redisClient.keys('user:*');
|
||||
const users = [];
|
||||
const users = await getAllUsers(req.user.username);
|
||||
|
||||
for (const key of userKeys) {
|
||||
const username = key.replace('user:', '');
|
||||
const userData = await redisClient.hGetAll(key);
|
||||
|
||||
if (userData && userData.username) {
|
||||
users.push({
|
||||
username: userData.username,
|
||||
createdAt: userData.createdAt || null,
|
||||
updatedAt: userData.updatedAt || null,
|
||||
createdBy: userData.createdBy || null,
|
||||
});
|
||||
// Convertir ObjectId a string y formatear fechas
|
||||
const formattedUsers = users.map(user => {
|
||||
const formatted = { ...user };
|
||||
formatted._id = user._id?.toString();
|
||||
if (user.createdAt && typeof user.createdAt === 'object') {
|
||||
formatted.createdAt = user.createdAt.toISOString();
|
||||
}
|
||||
}
|
||||
if (user.updatedAt && typeof user.updatedAt === 'object') {
|
||||
formatted.updatedAt = user.updatedAt.toISOString();
|
||||
}
|
||||
return formatted;
|
||||
});
|
||||
|
||||
// Ordenar por fecha de creación (más recientes primero)
|
||||
users.sort((a, b) => {
|
||||
formattedUsers.sort((a, b) => {
|
||||
const dateA = a.createdAt ? new Date(a.createdAt).getTime() : 0;
|
||||
const dateB = b.createdAt ? new Date(b.createdAt).getTime() : 0;
|
||||
return dateB - dateA;
|
||||
});
|
||||
|
||||
res.json({ users, total: users.length });
|
||||
res.json({ users: formattedUsers, total: formattedUsers.length });
|
||||
} catch (error) {
|
||||
console.error('Error obteniendo usuarios:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
@@ -180,11 +177,11 @@ router.get('/', basicAuthMiddleware, async (req, res) => {
|
||||
});
|
||||
|
||||
// Crear nuevo usuario (requiere autenticación admin)
|
||||
router.post('/', basicAuthMiddleware, async (req, res) => {
|
||||
router.post('/', basicAuthMiddleware, adminAuthMiddleware, async (req, res) => {
|
||||
try {
|
||||
const redisClient = getRedisClient();
|
||||
if (!redisClient) {
|
||||
return res.status(500).json({ error: 'Redis no está disponible' });
|
||||
const db = getDB();
|
||||
if (!db) {
|
||||
return res.status(500).json({ error: 'MongoDB no está disponible' });
|
||||
}
|
||||
|
||||
const { username, password } = req.body;
|
||||
@@ -201,19 +198,17 @@ router.post('/', basicAuthMiddleware, async (req, res) => {
|
||||
return res.status(400).json({ error: 'La contraseña debe tener al menos 6 caracteres' });
|
||||
}
|
||||
|
||||
const userKey = `user:${username}`;
|
||||
const userExists = await redisClient.exists(userKey);
|
||||
|
||||
if (userExists) {
|
||||
// Verificar si el usuario ya existe
|
||||
const existingUser = await getUser(username);
|
||||
if (existingUser) {
|
||||
return res.status(409).json({ error: 'El usuario ya existe' });
|
||||
}
|
||||
|
||||
// Hashear contraseña y crear usuario
|
||||
const passwordHash = await bcrypt.hash(password, 10);
|
||||
await redisClient.hSet(userKey, {
|
||||
await createUser({
|
||||
username,
|
||||
passwordHash,
|
||||
createdAt: new Date().toISOString(),
|
||||
createdBy: req.user.username,
|
||||
});
|
||||
|
||||
@@ -221,16 +216,20 @@ router.post('/', basicAuthMiddleware, async (req, res) => {
|
||||
res.json({ success: true, message: 'Usuario creado correctamente', username });
|
||||
} catch (error) {
|
||||
console.error('Error creando usuario:', error);
|
||||
// Manejar error de duplicado
|
||||
if (error.code === 11000) {
|
||||
return res.status(409).json({ error: 'El usuario ya existe' });
|
||||
}
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Eliminar usuario (requiere autenticación admin)
|
||||
router.delete('/:username', basicAuthMiddleware, async (req, res) => {
|
||||
router.delete('/:username', basicAuthMiddleware, adminAuthMiddleware, async (req, res) => {
|
||||
try {
|
||||
const redisClient = getRedisClient();
|
||||
if (!redisClient) {
|
||||
return res.status(500).json({ error: 'Redis no está disponible' });
|
||||
const db = getDB();
|
||||
if (!db) {
|
||||
return res.status(500).json({ error: 'MongoDB no está disponible' });
|
||||
}
|
||||
|
||||
const { username } = req.params;
|
||||
@@ -241,15 +240,19 @@ router.delete('/:username', basicAuthMiddleware, async (req, res) => {
|
||||
return res.status(400).json({ error: 'No puedes eliminar tu propio usuario' });
|
||||
}
|
||||
|
||||
const userKey = `user:${username}`;
|
||||
const userExists = await redisClient.exists(userKey);
|
||||
|
||||
if (!userExists) {
|
||||
// Verificar si el usuario existe
|
||||
const user = await getUser(username);
|
||||
if (!user) {
|
||||
return res.status(404).json({ error: 'Usuario no encontrado' });
|
||||
}
|
||||
|
||||
// Eliminar usuario
|
||||
await redisClient.del(userKey);
|
||||
// Eliminar usuario y sus sesiones
|
||||
await deleteUserFromDB(username);
|
||||
await invalidateUserSessions(username);
|
||||
|
||||
// También eliminar sus workers
|
||||
const workersCollection = db.collection('workers');
|
||||
await workersCollection.deleteOne({ username });
|
||||
|
||||
console.log(`✅ Usuario eliminado: ${username} por ${currentUser}`);
|
||||
res.json({ success: true, message: `Usuario ${username} eliminado correctamente` });
|
||||
|
||||
@@ -1,32 +1,65 @@
|
||||
import express from 'express';
|
||||
import { readJSON, writeJSON } from '../utils/fileUtils.js';
|
||||
import { PATHS } from '../config/constants.js';
|
||||
import { basicAuthMiddleware } from '../middlewares/auth.js';
|
||||
import { broadcast } from '../services/websocket.js';
|
||||
import { getWorkers, setWorkers, getDB } from '../services/mongodb.js';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// Obtener workers (requiere autenticación - solo administradores)
|
||||
router.get('/', basicAuthMiddleware, (req, res) => {
|
||||
// Obtener workers del usuario autenticado (requiere autenticación)
|
||||
router.get('/', basicAuthMiddleware, async (req, res) => {
|
||||
try {
|
||||
const workers = readJSON(PATHS.WORKERS, { items: [], general: {}, disabled: [] });
|
||||
const db = getDB();
|
||||
if (!db) {
|
||||
return res.status(500).json({ error: 'MongoDB no está disponible' });
|
||||
}
|
||||
|
||||
const username = req.user.username;
|
||||
const workers = await getWorkers(username);
|
||||
res.json(workers);
|
||||
} catch (error) {
|
||||
console.error('Error obteniendo workers:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Actualizar workers (requiere autenticación)
|
||||
router.put('/', basicAuthMiddleware, (req, res) => {
|
||||
// Actualizar workers del usuario autenticado (requiere autenticación)
|
||||
router.put('/', basicAuthMiddleware, async (req, res) => {
|
||||
try {
|
||||
const workers = req.body;
|
||||
if (writeJSON(PATHS.WORKERS, workers)) {
|
||||
broadcast({ type: 'workers_updated', data: workers });
|
||||
res.json({ success: true });
|
||||
} else {
|
||||
res.status(500).json({ error: 'Error guardando workers' });
|
||||
const db = getDB();
|
||||
if (!db) {
|
||||
return res.status(500).json({ error: 'MongoDB no está disponible' });
|
||||
}
|
||||
|
||||
const username = req.user.username;
|
||||
const workers = req.body;
|
||||
|
||||
// Validar estructura básica
|
||||
if (!workers || typeof workers !== 'object') {
|
||||
return res.status(400).json({ error: 'Formato de workers inválido' });
|
||||
}
|
||||
|
||||
// Asegurar estructura mínima
|
||||
const workersData = {
|
||||
general: workers.general || {
|
||||
title_exclude: [],
|
||||
description_exclude: []
|
||||
},
|
||||
items: workers.items || [],
|
||||
disabled: workers.disabled || []
|
||||
};
|
||||
|
||||
await setWorkers(username, workersData);
|
||||
|
||||
// Notificar a todos los clientes WebSocket del usuario
|
||||
broadcast({
|
||||
type: 'workers_updated',
|
||||
data: workersData,
|
||||
username // Incluir username para que los clientes sepan de quién son los workers
|
||||
});
|
||||
|
||||
res.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error('Error guardando workers:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user