Skip to content

Plataforma técnica · Orchestrator

Sessions

Orchestrator Command Sessions

SessionService provee visibilidad en tiempo real de quién está conectado y un kill switch para terminar sesiones forzosamente. Es la herramienta de respuesta a incidentes: token comprometido, usuario olvidó cerrar sesión en un kiosko público, sospecha de actividad anormal.

SessionService no autentica (eso es AuthService / routes/command/auth.ts). Solo lee y borra registros en auth.user_sessions. La creación de sesiones ocurre en el flujo de login (POST /api/auth/login emite la cookie sid y el INSERT de la sesión).

ActiveSession (lo que devuelve GET /api/sessions):

CampoOrigen SQLNotas
session_ids.idUUID, usado para el kill switch.
user_ids.user_id
usernameu.usernameLEFT JOIN — 'Unknown' si NULL.
roleu.roleLEFT JOIN — 'USER' si NULL.
tenant_idt.idVía u.tenant_id.
tenant_namet.business_name
ip_addresss.ip_addressCapturada al login.
user_agents.user_agent
login_times.created_at
last_activitys.created_at (fallback)No hay columna real de last_activity.
duration_minutesEXTRACT(EPOCH FROM (NOW() - s.created_at))/60Redondeado.

La query principal de SessionRepository.findActiveSessions:

findActiveSessions
SELECT
s.id AS session_id,
s.user_id,
s.created_at AS login_time,
s.expires_at,
s.ip_address,
s.user_agent,
u.username,
u.role,
t.id AS tenant_id,
t.business_name AS tenant_name,
EXTRACT(EPOCH FROM (NOW() - s.created_at))/60 AS duration_minutes
FROM auth.user_sessions s
LEFT JOIN auth.users u ON s.user_id = u.id
LEFT JOIN command.tenants t ON u.tenant_id = t.id
WHERE s.expires_at > NOW()
ORDER BY s.created_at DESC;

Filtro de actividad: el WHERE s.expires_at > NOW() excluye sesiones ya vencidas. No hay limpieza activa de filas vencidas — quedan en la tabla y son ignoradas por esta query (y por authenticateToken).

Lista todas las sesiones activas. Cualquier rol autorizado por authorizeRoute puede consultar (típicamente ADMIN y SUPER_ADMIN).

Terminal window
curl -b "sid=$JWT" http://localhost:8000/api/sessions

Respuesta (array, ejemplo abreviado):

Response
[
{
"session_id": "550e8400-e29b-41d4-a716-446655440000",
"user_id": "a1b2c3d4-...",
"username": "chris",
"role": "SUPER_ADMIN",
"tenant_id": "...",
"tenant_name": "Albornoz Contadores SpA",
"ip_address": "192.168.1.10",
"user_agent": "Mozilla/5.0 (...)",
"login_time": "2026-05-24T10:00:00.000Z",
"last_activity": "2026-05-24T10:00:00.000Z",
"duration_minutes": 47
}
]

Termina la sesión forzosamente (kill switch). El usuario perderá acceso en su próxima request porque authenticateToken no encontrará el registro en auth.user_sessions.

Terminal window
curl -X DELETE -b "sid=$JWT" \
"http://localhost:8000/api/sessions/550e8400-e29b-41d4-a716-446655440000"

Variantes aceptadas por el router (compatibilidad con clientes):

FormaEquivalencia
DELETE /api/sessions/:idPath param.
DELETE /api/sessions?id=<uuid>Query param (mismo handler).

Códigos:

StatusSignificado
200{ "success": true } — sesión terminada.
400Missing session id.
404Sesión no existe o ya estaba terminada.
sequenceDiagram
  autonumber
  participant Admin as Super Admin
  participant API as Orchestrator
  participant DB as auth.user_sessions
  participant User as Cliente del usuario

  Admin->>API: DELETE /api/sessions/{id}
  API->>DB: DELETE FROM auth.user_sessions WHERE id=$1
  DB-->>API: rowCount
  alt rowCount > 0
    API-->>Admin: 200 { success: true }
  else 0
    API-->>Admin: 404 not found
  end

  Note over User: Usuario sigue navegando con la cookie sid

  User->>API: GET /api/employees
  API->>DB: SELECT * FROM auth.user_sessions WHERE id=$sid
  DB-->>API: vacío
  API-->>User: 401 Unauthorized

SessionRepository devuelve un array vacío en lugar de lanzar si la query falla, para que el dashboard no quede en 500:

catch (e) {
console.error("Error fetching active sessions:", e);
return [];
}

Esto significa que un fallo de DB se manifiesta como “0 sesiones activas” en el dashboard. Monitorear console.error (o el audit log) para distinguir cero real vs error.

Las sesiones son inmutables en el modelo: o existen y son válidas, o se borran. No hay “extender expiración” como operación administrativa porque eso requeriría re-autenticar de todos modos. El logout normal del usuario también es un DELETE (desde routes/command/auth.ts), no un UPDATE expires_at = NOW().