<?php
namespace App\EventListener;
use App\Entity\UserSessionLog;
use App\Repository\UserSessionLogRepository;
use DateTime;
use Doctrine\ORM\EntityManagerInterface;
use Exception;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\HttpKernel\KernelInterface;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Http\Event\InteractiveLoginEvent;
use Symfony\Component\Security\Http\Event\LogoutEvent;
use Symfony\Component\Security\Http\SecurityEvents;
class UserSessionTracker implements EventSubscriberInterface
{
private const ACTIVITY_UPDATE_INTERVAL = 10; // Update activity every 10 seconds (for testing, use 60 in production)
public function __construct(
private EntityManagerInterface $entityManager,
private UserSessionLogRepository $sessionLogRepository,
private TokenStorageInterface $tokenStorage,
private KernelInterface $kernel
) {
}
public static function getSubscribedEvents(): array
{
return [
SecurityEvents::INTERACTIVE_LOGIN => 'onLogin',
LogoutEvent::class => 'onLogout',
KernelEvents::REQUEST => 'onRequest',
];
}
/**
* Handle user login event
*/
public function onLogin(InteractiveLoginEvent $event): void
{
error_log("UserSessionTracker::onLogin() CALLED at " . date('Y-m-d H:i:s'));
$this->writeLog(" - LOGIN: onLogin called\n");
$user = $event->getAuthenticationToken()->getUser();
$request = $event->getRequest();
if (!$user || !method_exists($user, 'getId')) {
$this->writeLog(" - LOGIN: No valid user\n");
return;
}
$sessionId = $request->getSession()->getId();
$this->writeLog(" - LOGIN: Session ID: $sessionId, User: " . $user->getUserIdentifier() . "\n");
try {
// Check if session already exists (avoid duplicates)
$existingSession = $this->sessionLogRepository->findActiveBySessionId($sessionId);
if ($existingSession) {
$this->writeLog(" - LOGIN: Session already exists, skipping\n");
return;
}
$sessionLog = new UserSessionLog();
$sessionLog->setUser($user);
$sessionLog->setUsername($user->getUserIdentifier() ?? null);
$sessionLog->setEmail($user->getEmail() ?? null);
$sessionLog->setLoginAt(new DateTime());
$sessionLog->setLastActivityAt(new DateTime());
$sessionLog->setIpAddress($request->getClientIp());
$sessionLog->setUserAgent($request->headers->get('User-Agent'));
$sessionLog->setSessionId($sessionId);
$this->entityManager->persist($sessionLog);
$this->entityManager->flush();
$this->writeLog(" - LOGIN: Session created successfully\n");
} catch (Exception $e) {
$this->writeLog(" - LOGIN ERROR: " . $e->getMessage() . "\n");
// Don't fail the login process if session logging fails
return;
}
}
/**
* Handle user logout event
*/
public function onLogout(LogoutEvent $event): void
{
error_log("UserSessionTracker::onLogout() CALLED at " . date('Y-m-d H:i:s'));
$request = $event->getRequest();
$sessionId = $request->getSession()->getId();
$this->writeLog(" - LOGOUT: onLogout called for session_id: $sessionId\n");
try {
$sessionLog = $this->sessionLogRepository->findActiveBySessionId($sessionId);
if (!$sessionLog) {
$this->writeLog(" - LOGOUT: No active session found by session_id\n");
// Session ID might have been regenerated - try to find by user
$token = $this->tokenStorage->getToken();
if ($token && $token->getUser() && method_exists($token->getUser(), 'getId')) {
$user = $token->getUser();
$this->writeLog(" - LOGOUT: Searching by user_id: " . $user->getId() . "\n");
// Find the most recent active session for this user
$sessionLog = $this->sessionLogRepository->findOneBy(
['user' => $user, 'logoutAt' => null],
['loginAt' => 'DESC']
);
if (!$sessionLog) {
$this->writeLog(" - LOGOUT: No active session found for user\n");
return;
}
$this->writeLog(" - LOGOUT: Found session by user with session_id: " . $sessionLog->getSessionId() . "\n");
} else {
$this->writeLog(" - LOGOUT: No authenticated user found\n");
return;
}
}
$this->writeLog(" - LOGOUT: Closing session for user: " . $sessionLog->getUsername() . "\n");
$logoutTime = new DateTime();
$sessionLog->setLogoutAt($logoutTime);
$sessionLog->setLastActivityAt($logoutTime);
$sessionLog->setLogoutType('manual'); // User clicked logout
$this->entityManager->flush();
$this->writeLog(" - LOGOUT: Session closed successfully\n");
} catch (Exception $e) {
$this->writeLog(" - LOGOUT ERROR: " . $e->getMessage() . "\n");
return;
}
}
/**
* Update last activity timestamp on each request
* ONLY if session hasn't been closed (no logoutAt)
*/
public function onRequest(RequestEvent $event): void
{
error_log("UserSessionTracker::onRequest() CALLED at " . date('Y-m-d H:i:s'));
$this->writeLog(" - onRequest called\n");
// Only track main requests, not sub-requests
if (!$event->isMainRequest()) {
$this->writeLog(" - Not main request, skipping\n");
return;
}
$request = $event->getRequest();
// Skip if no session
if (!$request->hasSession()) {
$this->writeLog(" - No session\n");
return;
}
$session = $request->getSession();
// Skip if session not started
if (!$session->isStarted()) {
$this->writeLog(" - Session not started\n");
return;
}
$sessionId = $session->getId();
$this->writeLog(" - Session ID: $sessionId\n");
// Check if we should update activity (throttle updates)
$lastUpdate = $session->get('_session_log_last_update');
$now = time();
if ($lastUpdate && ($now - $lastUpdate) < self::ACTIVITY_UPDATE_INTERVAL) {
$this->writeLog(" - Throttled (last update: $lastUpdate, now: $now)\n");
return;
}
$this->writeLog(" - Checking DB for session\n");
try {
$sessionLog = $this->sessionLogRepository->findActiveBySessionId($sessionId);
} catch (Exception $e) {
// Skip if Doctrine is not ready yet (during cache warming, etc.)
$this->writeLog(" - Doctrine not ready: " . $e->getMessage() . "\n");
return;
}
if (!$sessionLog) {
$this->writeLog(" - No session log found in DB\n");
// Session ID might have been regenerated after login - try to find by user
// Check if user is authenticated
$token = $this->tokenStorage->getToken();
if ($token && $token->getUser() && method_exists($token->getUser(), 'getId')) {
$user = $token->getUser();
$this->writeLog(" - User authenticated, searching by user_id: " . $user->getId() . "\n");
// Find the most recent session for this user (open OR closed by timeout)
// First try to find an open session
try {
$sessionLog = $this->sessionLogRepository->findOneBy(
['user' => $user, 'logoutAt' => null],
['loginAt' => 'DESC']
);
} catch (Exception $e) {
$this->writeLog(" - Error finding session: " . $e->getMessage() . "\n");
return;
}
// If no open session, try to find the most recent timeout session
if (!$sessionLog) {
try {
$sessionLog = $this->sessionLogRepository->findOneBy(
['user' => $user, 'logoutType' => 'timeout'],
['loginAt' => 'DESC']
);
} catch (Exception $e) {
$this->writeLog(" - Error finding timeout session: " . $e->getMessage() . "\n");
return;
}
if ($sessionLog) {
$this->writeLog(" - Found timeout session by user, reopening with session_id: $sessionId\n");
}
}
if ($sessionLog) {
$this->writeLog(" - Found session by user, updating session_id from " . $sessionLog->getSessionId() . " to $sessionId\n");
// Update the session_id (Symfony regenerated it after login)
$sessionLog->setSessionId($sessionId);
$this->entityManager->flush();
} else {
$this->writeLog(" - No session found for user (neither open nor timeout)\n");
return;
}
} else {
return;
}
}
$this->writeLog(" - Found session for user: " . $sessionLog->getUsername() . "\n");
// Check if session was auto-closed by timeout - reopen it
if ($sessionLog->getLogoutAt() !== null) {
if ($sessionLog->getLogoutType() === 'timeout') {
$this->writeLog(" - Session was auto-closed, reopening\n");
$sessionLog->setLogoutAt(null);
$sessionLog->setLogoutType(null);
$sessionLog->setDuration(null);
} else {
// Manual logout - don't reopen
$this->writeLog(" - Session manually closed, not reopening\n");
return;
}
}
$this->writeLog(" - Updating lastActivityAt\n");
$sessionLog->setLastActivityAt(new DateTime());
$this->entityManager->flush();
$this->writeLog(" - Updated successfully\n");
// Update throttle timestamp
$session->set('_session_log_last_update', $now);
}
// 🔹 Función helper para escribir logs portables
private function writeLog(string $message): void
{
$logDir = $this->kernel->getProjectDir() . '/var/tmp';
if (!is_dir($logDir)) {
mkdir($logDir, 0777, true);
}
$logFile = $logDir . '/session_tracker.log';
file_put_contents($logFile, date('Y-m-d H:i:s') . " - $message\n", FILE_APPEND);
}
}