diff --git a/web/backend/routes/admin.js b/web/backend/routes/admin.js index 0e31d44..2564b38 100644 --- a/web/backend/routes/admin.js +++ b/web/backend/routes/admin.js @@ -30,6 +30,8 @@ router.get('/sessions', basicAuthMiddleware, adminAuthMiddleware, async (req, re const sessionsByUser = {}; let activeSessions = 0; let expiredSessions = 0; + let connectedSessions = 0; // Sesiones con actividad reciente + let inactiveSessions = 0; // Sesiones sin actividad pero no expiradas sessions.forEach(session => { if (!sessionsByUser[session.username]) { @@ -41,6 +43,13 @@ router.get('/sessions', basicAuthMiddleware, adminAuthMiddleware, async (req, re expiredSessions++; } else { activeSessions++; + + // Contar sesiones conectadas (con actividad reciente) + if (session.isActive) { + connectedSessions++; + } else { + inactiveSessions++; + } } }); @@ -50,6 +59,8 @@ router.get('/sessions', basicAuthMiddleware, adminAuthMiddleware, async (req, re total: sessions.length, active: activeSessions, expired: expiredSessions, + connected: connectedSessions, + inactive: inactiveSessions, byUser: sessionsByUser, }, }); diff --git a/web/backend/routes/users.js b/web/backend/routes/users.js index d8b0196..29a090a 100644 --- a/web/backend/routes/users.js +++ b/web/backend/routes/users.js @@ -1,9 +1,10 @@ import express from 'express'; import bcrypt from 'bcrypt'; -import { getDB, getUser, createUser, deleteUser as deleteUserFromDB, getAllUsers, updateUserPassword } from '../services/mongodb.js'; +import { getDB, getUser, createUser, deleteUser as deleteUserFromDB, getAllUsers, updateUserPassword, getActiveSessions } from '../services/mongodb.js'; import { basicAuthMiddleware, createSession, invalidateSession, invalidateUserSessions } from '../middlewares/auth.js'; import { adminAuthMiddleware } from '../middlewares/adminAuth.js'; import { combineFingerprint } from '../utils/fingerprint.js'; +import { getActiveUsers } from '../services/websocket.js'; const router = express.Router(); @@ -375,5 +376,71 @@ router.delete('/:username', basicAuthMiddleware, adminAuthMiddleware, async (req } }); +// Obtener usuarios activos/conectados (requiere autenticación) +router.get('/active', basicAuthMiddleware, async (req, res) => { + try { + const db = getDB(); + if (!db) { + return res.status(500).json({ error: 'MongoDB no está disponible' }); + } + + // Obtener usuarios activos del WebSocket + const activeUsersWS = getActiveUsers(); + + // Obtener sesiones activas de la base de datos + const activeSessions = await getActiveSessions(); + + // Combinar información y eliminar duplicados + const userMap = new Map(); + + // Añadir usuarios del WebSocket + for (const user of activeUsersWS) { + if (!userMap.has(user.username)) { + userMap.set(user.username, { + username: user.username, + role: user.role, + status: user.status, + lastActivity: user.lastActivity, + connectedViaWebSocket: true + }); + } + } + + // Añadir usuarios de sesiones activas (que podrían no estar en WebSocket) + for (const session of activeSessions) { + if (!userMap.has(session.username)) { + const user = await getUser(session.username); + userMap.set(session.username, { + username: session.username, + role: user?.role || 'user', + status: 'active', + lastActivity: session.lastActivity?.toISOString() || null, + connectedViaWebSocket: false, + deviceInfo: session.deviceInfo + }); + } + } + + // Convertir a array + const activeUsers = Array.from(userMap.values()); + + // Ordenar por última actividad (más recientes primero) + activeUsers.sort((a, b) => { + const dateA = a.lastActivity ? new Date(a.lastActivity).getTime() : 0; + const dateB = b.lastActivity ? new Date(b.lastActivity).getTime() : 0; + return dateB - dateA; + }); + + res.json({ + activeUsers, + total: activeUsers.length, + timestamp: new Date().toISOString() + }); + } catch (error) { + console.error('Error obteniendo usuarios activos:', error); + res.status(500).json({ error: error.message }); + } +}); + export default router; diff --git a/web/backend/services/mongodb.js b/web/backend/services/mongodb.js index 192d3ec..f3a4be9 100644 --- a/web/backend/services/mongodb.js +++ b/web/backend/services/mongodb.js @@ -1101,6 +1101,30 @@ export async function getSession(token) { } } +// Actualizar actividad de una sesión +export async function updateSessionActivity(token, isActive = true) { + if (!db) { + return false; + } + + try { + const sessionsCollection = db.collection('sessions'); + await sessionsCollection.updateOne( + { token }, + { + $set: { + lastActivity: new Date(), + isActive: isActive, + }, + } + ); + return true; + } catch (error) { + console.error('Error actualizando actividad de sesión:', error.message); + return false; + } +} + export async function deleteSession(token) { if (!db) { return false; @@ -1139,21 +1163,63 @@ export async function getAllSessions() { try { const sessionsCollection = db.collection('sessions'); const sessions = await sessionsCollection.find({}).toArray(); - return sessions.map(session => ({ - token: session.token, - username: session.username, - fingerprint: session.fingerprint || null, - deviceInfo: session.deviceInfo || null, - createdAt: session.createdAt, - expiresAt: session.expiresAt, - isExpired: session.expiresAt ? new Date(session.expiresAt) < new Date() : false, - })); + const now = new Date(); + const INACTIVE_TIMEOUT = 5 * 60 * 1000; // 5 minutos + + return sessions.map(session => { + const isExpired = session.expiresAt ? new Date(session.expiresAt) < now : false; + const timeSinceActivity = session.lastActivity ? now - new Date(session.lastActivity) : null; + const isActive = session.isActive && timeSinceActivity && timeSinceActivity < INACTIVE_TIMEOUT; + + return { + token: session.token, + username: session.username, + fingerprint: session.fingerprint || null, + deviceInfo: session.deviceInfo || null, + createdAt: session.createdAt, + expiresAt: session.expiresAt, + lastActivity: session.lastActivity || null, + isActive: isActive || false, + isExpired: isExpired, + }; + }); } catch (error) { console.error('Error obteniendo todas las sesiones:', error.message); return []; } } +// Obtener sesiones activas (usuarios conectados) +export async function getActiveSessions() { + if (!db) { + return []; + } + + try { + const sessionsCollection = db.collection('sessions'); + const now = new Date(); + const INACTIVE_TIMEOUT = 5 * 60 * 1000; // 5 minutos + const recentTime = new Date(now - INACTIVE_TIMEOUT); + + // Buscar sesiones activas (con actividad reciente) + const sessions = await sessionsCollection.find({ + isActive: true, + lastActivity: { $gte: recentTime }, + expiresAt: { $gt: now } + }).toArray(); + + return sessions.map(session => ({ + username: session.username, + lastActivity: session.lastActivity, + deviceInfo: session.deviceInfo || null, + fingerprint: session.fingerprint || null, + })); + } catch (error) { + console.error('Error obteniendo sesiones activas:', error.message); + return []; + } +} + // Funciones para artículos export async function getArticle(platform, id, currentUsername = null) { if (!db) { diff --git a/web/backend/services/websocket.js b/web/backend/services/websocket.js index d053521..625f5a0 100644 --- a/web/backend/services/websocket.js +++ b/web/backend/services/websocket.js @@ -1,11 +1,17 @@ import { WebSocketServer } from 'ws'; -import { getDB, getSession, getUser, deleteSession as deleteSessionFromDB } from './mongodb.js'; +import { getDB, getSession, getUser, deleteSession as deleteSessionFromDB, updateSessionActivity } from './mongodb.js'; let wss = null; // Duración de la sesión en milisegundos (24 horas) const SESSION_DURATION = 24 * 60 * 60 * 1000; +// Tiempo de inactividad para marcar como inactivo (5 minutos) +const INACTIVE_TIMEOUT = 5 * 60 * 1000; + +// Intervalo para limpiar conexiones inactivas (1 minuto) +const CLEANUP_INTERVAL = 60 * 1000; + // Inicializar WebSocket Server export function initWebSocket(server) { wss = new WebSocketServer({ server, path: '/ws' }); @@ -73,18 +79,90 @@ export function initWebSocket(server) { role: user.role || 'user', token: token }; + ws.isAlive = true; + ws.lastActivity = new Date(); + + // Actualizar estado de conexión en la base de datos + await updateSessionActivity(token, true); console.log(`Cliente WebSocket conectado: ${session.username} (${user.role || 'user'})`); + // Enviar confirmación de conexión + ws.send(JSON.stringify({ + type: 'connection', + status: 'connected', + timestamp: new Date().toISOString() + })); + + // Broadcast a otros clientes que este usuario se conectó + broadcastUserStatus(session.username, 'online'); + } catch (error) { console.error('Error validando token WebSocket:', error); ws.close(1011, 'Error de autenticación'); return; } - ws.on('close', () => { + // Manejar mensajes del cliente + ws.on('message', async (data) => { + try { + const message = JSON.parse(data.toString()); + + // Manejar diferentes tipos de mensajes + switch (message.type) { + case 'ping': + case 'heartbeat': + // Actualizar actividad + ws.isAlive = true; + ws.lastActivity = new Date(); + + // Actualizar en base de datos + if (ws.user && ws.user.token) { + await updateSessionActivity(ws.user.token, true); + } + + // Responder con pong + ws.send(JSON.stringify({ + type: 'pong', + timestamp: new Date().toISOString() + })); + break; + + case 'activity': + // El usuario realizó alguna actividad (click, scroll, etc.) + ws.isAlive = true; + ws.lastActivity = new Date(); + + if (ws.user && ws.user.token) { + await updateSessionActivity(ws.user.token, true); + } + break; + + default: + console.log(`Mensaje WebSocket no manejado: ${message.type}`); + } + } catch (error) { + console.error('Error procesando mensaje WebSocket:', error); + } + }); + + // Manejar pong nativo de WebSocket + ws.on('pong', () => { + ws.isAlive = true; + ws.lastActivity = new Date(); + }); + + ws.on('close', async () => { if (ws.user) { console.log(`Cliente WebSocket desconectado: ${ws.user.username}`); + + // Actualizar estado en base de datos + if (ws.user.token) { + await updateSessionActivity(ws.user.token, false); + } + + // Broadcast a otros clientes que este usuario se desconectó + broadcastUserStatus(ws.user.username, 'offline'); } else { console.log('Cliente WebSocket desconectado'); } @@ -95,9 +173,81 @@ export function initWebSocket(server) { }); }); + // Iniciar limpieza periódica de conexiones inactivas + startCleanupInterval(); + return wss; } +// Limpiar conexiones inactivas periódicamente +let cleanupIntervalId = null; + +function startCleanupInterval() { + if (cleanupIntervalId) { + clearInterval(cleanupIntervalId); + } + + cleanupIntervalId = setInterval(() => { + if (!wss) return; + + const now = new Date(); + + wss.clients.forEach(async (ws) => { + // Si el cliente no ha respondido, marcarlo como inactivo + if (ws.isAlive === false) { + console.log(`Cerrando conexión inactiva: ${ws.user?.username || 'desconocido'}`); + + // Actualizar estado en base de datos antes de cerrar + if (ws.user && ws.user.token) { + await updateSessionActivity(ws.user.token, false); + } + + return ws.terminate(); + } + + // Verificar si ha pasado el timeout de inactividad + if (ws.lastActivity && (now - ws.lastActivity) > INACTIVE_TIMEOUT) { + console.log(`Usuario inactivo detectado: ${ws.user?.username || 'desconocido'}`); + + // Actualizar estado en base de datos pero mantener conexión + if (ws.user && ws.user.token) { + await updateSessionActivity(ws.user.token, false); + } + } + + // Marcar como no vivo y enviar ping + ws.isAlive = false; + ws.ping(); + }); + }, CLEANUP_INTERVAL); +} + +// Detener limpieza +function stopCleanupInterval() { + if (cleanupIntervalId) { + clearInterval(cleanupIntervalId); + cleanupIntervalId = null; + } +} + +// Broadcast del estado de un usuario específico +function broadcastUserStatus(username, status) { + if (!wss) return; + + const message = JSON.stringify({ + type: 'user_status', + username: username, + status: status, // 'online' | 'offline' | 'inactive' + timestamp: new Date().toISOString() + }); + + wss.clients.forEach((client) => { + if (client.readyState === 1) { // WebSocket.OPEN + client.send(message); + } + }); +} + // Broadcast a todos los clientes WebSocket export function broadcast(data) { if (!wss) return; @@ -114,3 +264,51 @@ export function getWebSocketServer() { return wss; } +// Obtener usuarios activos conectados +export function getActiveUsers() { + if (!wss) return []; + + const activeUsers = []; + const now = new Date(); + + wss.clients.forEach((ws) => { + if (ws.user && ws.readyState === 1) { // WebSocket.OPEN + const isActive = ws.lastActivity && (now - ws.lastActivity) <= INACTIVE_TIMEOUT; + + activeUsers.push({ + username: ws.user.username, + role: ws.user.role, + status: isActive ? 'active' : 'inactive', + lastActivity: ws.lastActivity?.toISOString() || null, + connectedAt: ws.lastActivity?.toISOString() || null + }); + } + }); + + // Eliminar duplicados (un usuario puede tener múltiples conexiones) + const uniqueUsers = []; + const seenUsernames = new Set(); + + for (const user of activeUsers) { + if (!seenUsernames.has(user.username)) { + seenUsernames.add(user.username); + uniqueUsers.push(user); + } + } + + return uniqueUsers; +} + +// Cerrar todas las conexiones y limpiar +export function closeWebSocket() { + stopCleanupInterval(); + + if (wss) { + wss.clients.forEach((ws) => { + ws.close(1001, 'Servidor cerrando'); + }); + wss.close(); + wss = null; + } +} + diff --git a/web/dashboard/src/App.vue b/web/dashboard/src/App.vue index b9dd644..0e84e03 100644 --- a/web/dashboard/src/App.vue +++ b/web/dashboard/src/App.vue @@ -206,6 +206,8 @@ const pushEnabled = ref(false); const currentUser = ref(authService.getUsername() || null); const isAdmin = ref(false); let ws = null; +let heartbeatInterval = null; +let activityThrottleTimeout = null; const isDark = computed(() => darkMode.value); const isAuthenticated = computed(() => authService.hasCredentials()); @@ -335,6 +337,13 @@ onMounted(async () => { window.addEventListener('auth-login', handleAuthChange); window.addEventListener('auth-logout', handleAuthChange); + // Escuchar eventos de actividad del usuario para enviar al servidor + // Estos eventos ayudan a mantener la sesión activa + window.addEventListener('click', sendUserActivity); + window.addEventListener('keydown', sendUserActivity); + window.addEventListener('scroll', sendUserActivity, { passive: true }); + window.addEventListener('mousemove', sendUserActivity, { passive: true }); + // Si hay credenciales, validar y conectar websocket if (authService.hasCredentials()) { // Validar si el token sigue siendo válido @@ -358,6 +367,15 @@ onUnmounted(() => { window.removeEventListener('auth-login', handleAuthChange); window.removeEventListener('auth-logout', handleAuthChange); + // Limpiar listeners de actividad + window.removeEventListener('click', sendUserActivity); + window.removeEventListener('keydown', sendUserActivity); + window.removeEventListener('scroll', sendUserActivity); + window.removeEventListener('mousemove', sendUserActivity); + + // Detener heartbeat + stopHeartbeat(); + if (ws) { ws.close(); } @@ -370,6 +388,12 @@ function connectWebSocket() { ws = null; } + // Limpiar heartbeat interval previo + if (heartbeatInterval) { + clearInterval(heartbeatInterval); + heartbeatInterval = null; + } + // Verificar si hay token de autenticación const token = authService.getToken(); if (!token) { @@ -404,11 +428,17 @@ function connectWebSocket() { ws.onopen = () => { wsConnected.value = true; console.log('WebSocket conectado'); + + // Iniciar heartbeat cada 30 segundos + startHeartbeat(); }; ws.onclose = (event) => { wsConnected.value = false; + // Detener heartbeat + stopHeartbeat(); + // Si el cierre fue por autenticación fallida (código 1008), no reintentar if (event.code === 1008) { console.log('WebSocket cerrado: autenticación fallida'); @@ -434,9 +464,73 @@ function connectWebSocket() { ws.onmessage = (event) => { const data = JSON.parse(event.data); + // Manejar respuesta de pong + if (data.type === 'pong') { + // El servidor respondió al heartbeat + return; + } + + // Manejar confirmación de conexión + if (data.type === 'connection' && data.status === 'connected') { + console.log('Conexión WebSocket confirmada'); + return; + } + + // Manejar cambios de estado de usuarios + if (data.type === 'user_status') { + console.log(`Usuario ${data.username} cambió a estado: ${data.status}`); + // Emitir evento para que otros componentes lo manejen + window.dispatchEvent(new CustomEvent('user-status-change', { detail: data })); + return; + } + // Los componentes individuales manejarán los mensajes (incluyendo ToastContainer) window.dispatchEvent(new CustomEvent('ws-message', { detail: data })); }; } + +// Enviar heartbeat al servidor periódicamente +function startHeartbeat() { + // Detener heartbeat previo si existe + stopHeartbeat(); + + // Enviar heartbeat cada 30 segundos + heartbeatInterval = setInterval(() => { + if (ws && ws.readyState === WebSocket.OPEN) { + ws.send(JSON.stringify({ + type: 'heartbeat', + timestamp: new Date().toISOString() + })); + } + }, 30000); // 30 segundos +} + +function stopHeartbeat() { + if (heartbeatInterval) { + clearInterval(heartbeatInterval); + heartbeatInterval = null; + } +} + +// Enviar actividad del usuario al servidor (throttled) +function sendUserActivity() { + // Si ya hay un timeout pendiente, no hacer nada + if (activityThrottleTimeout) { + return; + } + + // Enviar actividad y crear throttle de 10 segundos + if (ws && ws.readyState === WebSocket.OPEN) { + ws.send(JSON.stringify({ + type: 'activity', + timestamp: new Date().toISOString() + })); + } + + // Throttle: no enviar más actividad por 10 segundos + activityThrottleTimeout = setTimeout(() => { + activityThrottleTimeout = null; + }, 10000); +} diff --git a/web/dashboard/src/components/ActiveUsers.vue b/web/dashboard/src/components/ActiveUsers.vue new file mode 100644 index 0000000..8577b02 --- /dev/null +++ b/web/dashboard/src/components/ActiveUsers.vue @@ -0,0 +1,188 @@ + + + + diff --git a/web/dashboard/src/services/api.js b/web/dashboard/src/services/api.js index 4a5d5ee..d13e387 100644 --- a/web/dashboard/src/services/api.js +++ b/web/dashboard/src/services/api.js @@ -155,6 +155,11 @@ export default { return response.data; }, + async getActiveUsers() { + const response = await api.get('/users/active'); + return response.data; + }, + // Admin - Rate Limiter async getRateLimiterInfo() { const response = await api.get('/admin/rate-limiter'); diff --git a/web/dashboard/src/views/Dashboard.vue b/web/dashboard/src/views/Dashboard.vue index 884002c..62cc5d8 100644 --- a/web/dashboard/src/views/Dashboard.vue +++ b/web/dashboard/src/views/Dashboard.vue @@ -106,6 +106,11 @@ + +
+ +
+
@@ -229,6 +234,7 @@ import { ref, onMounted, onUnmounted } from 'vue'; import api from '../services/api'; import authService from '../services/auth'; +import ActiveUsers from '../components/ActiveUsers.vue'; import { Cog6ToothIcon, HeartIcon, diff --git a/web/dashboard/src/views/Sessions.vue b/web/dashboard/src/views/Sessions.vue index 195a705..65f5c46 100644 --- a/web/dashboard/src/views/Sessions.vue +++ b/web/dashboard/src/views/Sessions.vue @@ -22,7 +22,7 @@
-
+
Total Sesiones
@@ -30,13 +30,25 @@
-
Sesiones Activas
+
Activas (Válidas)
{{ sessionsData.stats.active || 0 }}
-
Sesiones Expiradas
+
🟢 Conectadas
+
+ {{ sessionsData.stats.connected || 0 }} +
+
+
+
🟡 Inactivas
+
+ {{ sessionsData.stats.inactive || 0 }} +
+
+
+
🔴 Expiradas
{{ sessionsData.stats.expired || 0 }}
@@ -79,6 +91,9 @@ Dispositivo + + Última Actividad + Token @@ -118,6 +133,19 @@ Sin información
+ +
+
+ {{ formatRelativeTime(session.lastActivity) }} +
+
+ {{ formatDate(session.lastActivity) }} +
+
+
+ Sin actividad +
+ {{ session.token.substring(0, 16) }}... @@ -130,13 +158,26 @@ {{ formatDate(session.expiresAt) }} - - {{ session.isExpired ? 'Expirada' : 'Activa' }} - +
+ + + {{ session.isExpired ? '🔴 Expirada' : '✅ Válida' }} + + + + + {{ session.isActive ? '🟢 Conectado' : '🟡 Inactivo' }} + +
+ + +
+ +
+
+

Información

+
+
+ Creado: + {{ formatDate(user.createdAt) }} +
+
+ Por: + {{ user.createdBy }} +
+
+ Actualizado: + {{ formatDate(user.updatedAt) }} +
+
+ Sesiones activas: + {{ getUserSessionCount(user.username) }} +
+
+
+
+ + +
+

Suscripción

+
+
+
+

Plan

+

+ {{ userSubscriptions[user.username].subscription?.plan?.name || 'Gratis' }} +

+
+ + {{ userSubscriptions[user.username].subscription?.status === 'active' ? 'Activo' : 'Inactivo' }} + +
+
+
+ Uso de búsquedas + + {{ userSubscriptions[user.username].usage.workers }} / {{ userSubscriptions[user.username].usage.maxWorkers === 'Ilimitado' ? '∞' : userSubscriptions[user.username].usage.maxWorkers }} + +
+ +
+
+
+
+
+
+ Sin información de suscripción +
+
+
@@ -419,6 +476,8 @@ const availablePlans = ref([]); const subscriptionError = ref(''); const subscriptionSuccess = ref(''); const addError = ref(''); +const activeUsers = ref([]); +const userSessions = ref({}); const userForm = ref({ username: '', @@ -458,6 +517,80 @@ function formatDate(dateString) { } } +function formatRelativeTime(timestamp) { + if (!timestamp) return 'Nunca'; + + const date = new Date(timestamp); + const now = new Date(); + const diffInSeconds = Math.floor((now - date) / 1000); + + if (diffInSeconds < 60) { + return 'Hace unos segundos'; + } else if (diffInSeconds < 3600) { + const minutes = Math.floor(diffInSeconds / 60); + return `Hace ${minutes} ${minutes === 1 ? 'minuto' : 'minutos'}`; + } else if (diffInSeconds < 86400) { + const hours = Math.floor(diffInSeconds / 3600); + return `Hace ${hours} ${hours === 1 ? 'hora' : 'horas'}`; + } else { + const days = Math.floor(diffInSeconds / 86400); + return `Hace ${days} ${days === 1 ? 'día' : 'días'}`; + } +} + +// Obtener el estado de conexión de un usuario +function getUserConnectionStatus(username) { + // Buscar si el usuario tiene alguna sesión activa + const activeSessions = userSessions.value[username] || []; + + if (activeSessions.length === 0) { + return 'offline'; + } + + // Verificar si alguna sesión está activamente conectada + const hasActiveSession = activeSessions.some(session => session.isActive); + if (hasActiveSession) { + return 'active'; + } + + // Si tiene sesiones válidas pero ninguna activa + const hasValidSession = activeSessions.some(session => !session.isExpired); + if (hasValidSession) { + return 'inactive'; + } + + return 'offline'; +} + +// Obtener la última actividad de un usuario (de todas sus sesiones) +function getUserLastActivity(username) { + const sessions = userSessions.value[username] || []; + + if (sessions.length === 0) { + return null; + } + + // Encontrar la sesión con la actividad más reciente + let latestActivity = null; + + for (const session of sessions) { + if (session.lastActivity) { + const activityDate = new Date(session.lastActivity); + if (!latestActivity || activityDate > latestActivity) { + latestActivity = activityDate; + } + } + } + + return latestActivity ? latestActivity.toISOString() : null; +} + +// Obtener el número de sesiones activas de un usuario +function getUserSessionCount(username) { + const sessions = userSessions.value[username] || []; + return sessions.filter(s => !s.isExpired).length; +} + async function loadUsers() { loading.value = true; try { @@ -466,7 +599,10 @@ async function loadUsers() { // Cargar información de suscripción para todos los usuarios (solo si es admin) if (isAdmin.value) { - await loadAllUserSubscriptions(); + await Promise.all([ + loadAllUserSubscriptions(), + loadUserSessions(), + ]); } } catch (error) { console.error('Error cargando usuarios:', error); @@ -476,6 +612,29 @@ async function loadUsers() { } } +async function loadUserSessions() { + if (!isAdmin.value) return; + + try { + // Obtener todas las sesiones + const data = await api.getSessions(); + const sessions = data.sessions || []; + + // Agrupar sesiones por usuario + const sessionsByUser = {}; + for (const session of sessions) { + if (!sessionsByUser[session.username]) { + sessionsByUser[session.username] = []; + } + sessionsByUser[session.username].push(session); + } + + userSessions.value = sessionsByUser; + } catch (error) { + console.error('Error cargando sesiones de usuarios:', error); + } +} + async function loadAllUserSubscriptions() { // Cargar suscripciones de todos los usuarios en paralelo const subscriptionPromises = users.value.map(async (user) => { @@ -489,10 +648,31 @@ async function loadAllUserSubscriptions() { }); if (response.ok) { const data = await response.json(); + console.log(`Suscripción de ${user.username}:`, data); userSubscriptions.value[user.username] = data; + } else { + console.warn(`No se pudo cargar suscripción de ${user.username}, usando valores por defecto`); + // Valores por defecto si no se puede cargar + userSubscriptions.value[user.username] = { + subscription: { + planId: 'free', + status: 'active', + plan: { name: 'Gratis', description: 'Plan gratuito' } + }, + usage: { workers: 0, maxWorkers: 2 } + }; } } catch (error) { console.error(`Error cargando suscripción de ${user.username}:`, error); + // Valores por defecto en caso de error + userSubscriptions.value[user.username] = { + subscription: { + planId: 'free', + status: 'active', + plan: { name: 'Gratis', description: 'Plan gratuito' } + }, + usage: { workers: 0, maxWorkers: 2 } + }; } }); await Promise.all(subscriptionPromises); @@ -687,8 +867,22 @@ function handleAuthLogout() { showAddModal.value = false; userToDelete.value = null; addError.value = ''; + activeUsers.value = []; + userSessions.value = {}; } +// Manejar cambios de estado de usuarios vía WebSocket +function handleUserStatusChange(event) { + const { username, status } = event.detail; + + // Recargar sesiones para actualizar el estado + if (isAdmin.value) { + loadUserSessions(); + } +} + +let refreshInterval = null; + onMounted(() => { loadUsers(); window.addEventListener('auth-logout', handleAuthLogout); @@ -696,11 +890,26 @@ onMounted(() => { window.addEventListener('auth-login', () => { loadUsers(); }); + + // Escuchar cambios de estado de usuarios + window.addEventListener('user-status-change', handleUserStatusChange); + + // Actualizar sesiones periódicamente (cada 30 segundos) + if (isAdmin.value) { + refreshInterval = setInterval(() => { + loadUserSessions(); + }, 30000); + } }); onUnmounted(() => { window.removeEventListener('auth-logout', handleAuthLogout); window.removeEventListener('auth-login', loadUsers); + window.removeEventListener('user-status-change', handleUserStatusChange); + + if (refreshInterval) { + clearInterval(refreshInterval); + } });