Canales Privados & Difusión Reactiva
La arquitectura de difusión en tiempo real de Mañuti opera bajo un principio estricto de privacidad total: no existen canales públicos. Todo flujo de comunicación requiere autenticación previa contra la sesión del usuario o tokens de autorización firmados.
1. Definición y Autorización de Canales (routes/channels.php)
Los canales privados se protegen mediante clausuras que validan la sesión activa en MariaDB/Redis antes de permitir al cliente unirse al bus de mensajería:
// routes/channels.php
use Illuminate\Support\Facades\Broadcast;
// 1. Canal Personal de Usuario (Kill Switch & Notificaciones Específicas)
Broadcast::channel('user.{id}', function ($user, $id) {
return (int) $user->id === (int) $id;
});
// 2. Canal de Gestión de Sesiones Activas (Solo Administradores de Seguridad)
Broadcast::channel('admin.sessions', function ($user) {
return $user->can('sessions.manage') || $user->hasRole('Super Admin');
});
// 3. Canal de Auditoría Forense en Vivo (Auditores y CISO)
Broadcast::channel('admin.updates', function ($user) {
return $user->can('audit.search') || $user->hasRole('Super Admin');
});
2. Eventos de Difusión Clave
Cada evento implementa la interfaz Illuminate\Contracts\Broadcasting\ShouldBroadcast, lo que garantiza que Laravel no transmita el mensaje en el hilo HTTP principal, sino que lo encole automáticamente en Redis para su entrega asíncrona a través de Horizon.
A. Kill Switch Instantáneo (UserSessionTerminated)
Permite expulsar a un operador de forma inmediata ante un bloqueo administrativo o violación de la Política de Sesión Única:
// app/Events/UserSessionTerminated.php
namespace App\Events;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class UserSessionTerminated implements ShouldBroadcast
{
use Dispatchable, SerializesModels;
public function __construct(public int $userId) {}
public function broadcastOn(): array
{
return [new PrivateChannel('user.' . $this->userId)];
}
public function broadcastAs(): string
{
return 'session.terminated';
}
}
B. Notificación de Concurrencia (ActiveSessionsChanged)
Transmite entradas (login), salidas (logout) y expulsiones forzadas (kill) hacia la tabla de sesiones activas:
// app/Events/ActiveSessionsChanged.php
namespace App\Events;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class ActiveSessionsChanged implements ShouldBroadcast
{
use Dispatchable, SerializesModels;
public function __construct(
public string $type,
public string $message,
public ?int $userId = null
) {}
public function broadcastOn(): array
{
return [new PrivateChannel('admin.sessions')];
}
public function broadcastAs(): string
{
return 'sessions.updated';
}
}
3. Desacoplamiento y Despacho vía Colas
La emisión de eventos WebSocket sigue un modelo de desacoplamiento para no comprometer el tiempo de respuesta del controlador:
flowchart TD
CTRL["[Controlador / Acción]"]
EVENT["Event::dispatch()"]
QUEUE["[Horizon Queue: 'default']"]
WORKER["[Worker toma el Evento]"]
REDIS["[Redis PUBLISH 'reverb-scaling']"]
REVERB["[Reverb Daemon distribuye a Sockets]"]
CTRL --> EVENT
EVENT --> QUEUE
QUEUE --> WORKER
WORKER --> REDIS
REDIS --> REVERB
4. Receptores en Frontend & Animaciones Reactivas
Receptor Global del Kill Switch (resources/views/partials/auth-listener.blade.php)
Escucha en segundo plano en todas las vistas protegidas del backend:
window.Echo.private('user.' + userId)
.listen('.session.terminated', (e) => {
alert('Su sesión ha sido terminada remotamente por un administrador.');
// Forzar navegación al dashboard activa el middleware CheckActiveSession
window.location.href = '/administracion';
});
Animación Reactiva de Fila en Sesiones Activas
Cuando un usuario ingresa al sistema, la tabla en /sesiones-activas se actualiza sin recargar manualmente y aplica una animación de resalte verde con desvanecimiento (Yellow/Green Fade Technique):
<style>
@keyframes fadeGreenToWhite {
0% { background-color: #d1fae5; }
100% { background-color: transparent; }
}
.row-highlight {
animation: fadeGreenToWhite 3s ease-out forwards;
}
</style>
window.Echo.private('admin.sessions')
.listen('.sessions.updated', (e) => {
if (typeof KTToast !== 'undefined') {
KTToast.show({ message: e.message, variant: e.type === 'login' ? 'success' : 'info' });
}
setTimeout(() => {
const url = new URL(window.location.href);
if (e.userId && e.type === 'login') {
url.searchParams.set('highlight', e.userId);
}
window.location.href = url.toString();
}, 1000);
});