315 lines
8.8 KiB
JavaScript
315 lines
8.8 KiB
JavaScript
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;
|
|
}
|
|
}
|
|
|