import { WebSocketServer } from 'ws'; 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' }); wss.on('connection', async (ws, req) => { // Extraer token de los query parameters const url = new URL(req.url, `http://${req.headers.host}`); const token = url.searchParams.get('token'); if (!token) { console.log('Intento de conexión WebSocket sin token'); ws.close(1008, 'Token requerido'); return; } // Validar token try { const db = getDB(); if (!db) { console.error('MongoDB no disponible para validar WebSocket'); ws.close(1011, 'Servicio no disponible'); return; } // Verificar token en MongoDB const session = await getSession(token); if (!session) { console.log('Intento de conexión WebSocket con token inválido'); ws.close(1008, 'Token inválido'); return; } // Verificar que la sesión no haya expirado if (session.expiresAt && new Date(session.expiresAt) < new Date()) { await deleteSessionFromDB(token); console.log('Intento de conexión WebSocket con sesión expirada'); ws.close(1008, 'Sesión expirada'); return; } // Verificar que el usuario aún existe const user = await getUser(session.username); if (!user) { // Eliminar sesión si el usuario ya no existe await deleteSessionFromDB(token); console.log('Intento de conexión WebSocket con usuario inexistente'); ws.close(1008, 'Usuario no encontrado'); return; } // Actualizar expiración de la sesión (refresh) const sessionsCollection = db.collection('sessions'); const newExpiresAt = new Date(Date.now() + SESSION_DURATION); await sessionsCollection.updateOne( { token }, { $set: { expiresAt: newExpiresAt } } ); // Autenticación exitosa - almacenar información del usuario en el websocket ws.user = { username: session.username, 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; } // 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'); } }); ws.on('error', (error) => { console.error('Error WebSocket:', error); }); }); // 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; wss.clients.forEach((client) => { if (client.readyState === 1) { // WebSocket.OPEN client.send(JSON.stringify(data)); } }); } // Obtener instancia del WebSocket Server 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; } }