activity
Signed-off-by: Omar Sánchez Pizarro <omar.sanchez@pistacero.net>
This commit is contained in:
@@ -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,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user