Files
radiusNucleo/node-api/index.js
josedario87 06707df581
All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 30s
Feature: Sistema de tracking de sesiones RADIUS en tiempo real
- Nueva tabla `sesiones` para historial persistente de conexiones
- Job de detección de stale cada 2 min (10 min idle = desconectado)
- Inicialización resiliente desde BD al arrancar el servidor
- Nuevos endpoints: /api/sessions, /api/sessions/history, /api/sessions.csv
- Nueva vista "Sesiones" en el dashboard con estadísticas
- Historial integrado en UserCard y DispositivoCard
- Estadísticas de bytes in/out y duración por sesión
- Retención configurable de historial (90 días por defecto)
2025-11-25 00:49:14 -06:00

133 lines
4.3 KiB
JavaScript

import { createApp } from './src/app.js';
import {
ensureSchema,
disableGuestsFromYesterday,
getActiveSessions,
markStaleSessions,
syncConnectedDevicesFromSessions,
cleanOldSessions
} from './src/services/db.js';
import { activeSessions } from './src/services/radius.js';
import { broadcastStatus } from './src/sse.js';
const app = createApp();
const port = process.env.PORT || 3000;
// Configuration from environment
const STALE_CHECK_INTERVAL = parseInt(process.env.STALE_CHECK_INTERVAL || '120000', 10); // 2 min
const MAX_IDLE_MINUTES = parseInt(process.env.MAX_IDLE_MINUTES || '10', 10);
const SESSION_HISTORY_RETENTION_DAYS = parseInt(process.env.SESSION_HISTORY_RETENTION_DAYS || '90', 10);
try {
await ensureSchema();
} catch (e) {
console.error('Database schema ensure failed:', e?.message || e);
}
// Initialize sessions from database on startup
async function initializeSessionsFromDb() {
try {
// Sync dispositivos_conectados from active sessions
await syncConnectedDevicesFromSessions();
// Load active sessions into memory Map for CoA
const active = await getActiveSessions();
for (const sess of active) {
activeSessions.set(sess.session_id, {
username: sess.username,
sessionId: sess.session_id,
nasIp: sess.nas_ip,
nasId: sess.nas_id,
callingStationId: sess.calling_station_id,
calledStationId: sess.called_station_id,
updatedAt: new Date(sess.last_update).getTime(),
});
}
console.log(`[init] Loaded ${active.length} active sessions from database`);
} catch (e) {
console.error('[init] Failed to load sessions:', e?.message || e);
}
}
// Job to detect and mark stale sessions
function scheduleStaleSessionsJob() {
setInterval(async () => {
try {
const result = await markStaleSessions(MAX_IDLE_MINUTES);
if (result.count > 0) {
console.log(`[stale-sessions] Marked ${result.count} sessions as stale`);
// Remove stale sessions from memory Map
for (const sess of result.sessions) {
activeSessions.delete(sess.session_id);
}
// Notify SSE clients
broadcastStatus({ type: 'sessions-updated', staleCount: result.count });
}
} catch (e) {
console.error('[stale-sessions] Error:', e?.message || e);
}
}, STALE_CHECK_INTERVAL);
}
// Job to clean old sessions (runs daily at 3:00 AM local)
function scheduleSessionCleanupJob() {
function schedule() {
const now = new Date();
const next = new Date(now);
next.setUTCHours(9, 0, 0, 0); // 9:00 UTC = 3:00 AM Honduras (UTC-6)
if (next <= now) next.setUTCDate(next.getUTCDate() + 1);
const delay = next - now;
setTimeout(async () => {
try {
const deleted = await cleanOldSessions(SESSION_HISTORY_RETENTION_DAYS);
if (deleted > 0) {
console.log(`[session-cleanup] Deleted ${deleted} old sessions (>${SESSION_HISTORY_RETENTION_DAYS} days)`);
}
} catch (e) {
console.error('[session-cleanup] Error:', e?.message || e);
} finally {
schedule(); // Re-schedule for tomorrow
}
}, delay);
}
schedule();
}
// Initialize sessions from database
try {
await initializeSessionsFromDb();
} catch (e) {
console.error('Session initialization failed:', e?.message || e);
}
// Start maintenance jobs
scheduleStaleSessionsJob();
scheduleSessionCleanupJob();
app.listen(port, () => {
console.log(`Node RADIUS REST API listening on :${port}`);
console.log(`[config] Stale check: every ${STALE_CHECK_INTERVAL/1000}s, idle timeout: ${MAX_IDLE_MINUTES} min, retention: ${SESSION_HISTORY_RETENTION_DAYS} days`);
});
// Schedule daily guest disable at 4:00 AM America/Tegucigalpa (UTC-6 -> 10:00 UTC)
function scheduleGuestJob() {
const now = new Date();
const next = new Date(now);
next.setUTCHours(10, 0, 0, 0); // 10:00 UTC == 4:00 local (UTC-6)
if (next <= now) next.setUTCDate(next.getUTCDate() + 1);
const delay = next - now;
setTimeout(async function run() {
try {
const { count } = await disableGuestsFromYesterday();
if (count) console.log(`[guest-cron] Disabled ${count} invitado users from yesterday`);
} catch (e) {
console.error('[guest-cron] Error:', e?.message || e);
} finally {
scheduleGuestJob();
}
}, delay);
}
scheduleGuestJob();