src/EventListener/UserSessionTracker.php line 90

Open in your IDE?
  1. <?php
  2. namespace App\EventListener;
  3. use App\Entity\UserSessionLog;
  4. use App\Repository\UserSessionLogRepository;
  5. use DateTime;
  6. use Doctrine\ORM\EntityManagerInterface;
  7. use Exception;
  8. use Symfony\Component\EventDispatcher\EventSubscriberInterface;
  9. use Symfony\Component\HttpKernel\Event\RequestEvent;
  10. use Symfony\Component\HttpKernel\KernelEvents;
  11. use Symfony\Component\HttpKernel\KernelInterface;
  12. use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
  13. use Symfony\Component\Security\Http\Event\InteractiveLoginEvent;
  14. use Symfony\Component\Security\Http\Event\LogoutEvent;
  15. use Symfony\Component\Security\Http\SecurityEvents;
  16. class UserSessionTracker implements EventSubscriberInterface
  17. {
  18.     private const ACTIVITY_UPDATE_INTERVAL 10// Update activity every 10 seconds (for testing, use 60 in production)
  19.     public function __construct(
  20.         private EntityManagerInterface $entityManager,
  21.         private UserSessionLogRepository $sessionLogRepository,
  22.         private TokenStorageInterface $tokenStorage,
  23.         private KernelInterface $kernel
  24.     ) {
  25.     }
  26.     public static function getSubscribedEvents(): array
  27.     {
  28.         return [
  29.             SecurityEvents::INTERACTIVE_LOGIN => 'onLogin',
  30.             LogoutEvent::class => 'onLogout',
  31.             KernelEvents::REQUEST => 'onRequest',
  32.         ];
  33.     }
  34.     /**
  35.      * Handle user login event
  36.      */
  37.     public function onLogin(InteractiveLoginEvent $event): void
  38.     {
  39.         error_log("UserSessionTracker::onLogin() CALLED at " date('Y-m-d H:i:s'));
  40.         $this->writeLog(" - LOGIN: onLogin called\n");
  41.         $user $event->getAuthenticationToken()->getUser();
  42.         $request $event->getRequest();
  43.         if (!$user || !method_exists($user'getId')) {
  44.             $this->writeLog(" - LOGIN: No valid user\n");
  45.             return;
  46.         }
  47.         $sessionId $request->getSession()->getId();
  48.         $this->writeLog(" - LOGIN: Session ID: $sessionId, User: " $user->getUserIdentifier() . "\n");
  49.         try {
  50.             // Check if session already exists (avoid duplicates)
  51.             $existingSession $this->sessionLogRepository->findActiveBySessionId($sessionId);
  52.             if ($existingSession) {
  53.                 $this->writeLog(" - LOGIN: Session already exists, skipping\n");
  54.                 return;
  55.             }
  56.             $sessionLog = new UserSessionLog();
  57.             $sessionLog->setUser($user);
  58.             $sessionLog->setUsername($user->getUserIdentifier() ?? null);
  59.             $sessionLog->setEmail($user->getEmail() ?? null);
  60.             $sessionLog->setLoginAt(new DateTime());
  61.             $sessionLog->setLastActivityAt(new DateTime());
  62.             $sessionLog->setIpAddress($request->getClientIp());
  63.             $sessionLog->setUserAgent($request->headers->get('User-Agent'));
  64.             $sessionLog->setSessionId($sessionId);
  65.             $this->entityManager->persist($sessionLog);
  66.             $this->entityManager->flush();
  67.             $this->writeLog(" - LOGIN: Session created successfully\n");
  68.         } catch (Exception $e) {
  69.             $this->writeLog(" - LOGIN ERROR: " $e->getMessage() . "\n");
  70.             // Don't fail the login process if session logging fails
  71.             return;
  72.         }
  73.     }
  74.     /**
  75.      * Handle user logout event
  76.      */
  77.     public function onLogout(LogoutEvent $event): void
  78.     {
  79.         error_log("UserSessionTracker::onLogout() CALLED at " date('Y-m-d H:i:s'));
  80.         $request $event->getRequest();
  81.         $sessionId $request->getSession()->getId();
  82.         $this->writeLog(" - LOGOUT: onLogout called for session_id: $sessionId\n");
  83.         try {
  84.             $sessionLog $this->sessionLogRepository->findActiveBySessionId($sessionId);
  85.             if (!$sessionLog) {
  86.                 $this->writeLog(" - LOGOUT: No active session found by session_id\n");
  87.                 // Session ID might have been regenerated - try to find by user
  88.                 $token $this->tokenStorage->getToken();
  89.                 if ($token && $token->getUser() && method_exists($token->getUser(), 'getId')) {
  90.                     $user $token->getUser();
  91.                     $this->writeLog(" - LOGOUT: Searching by user_id: " $user->getId() . "\n");
  92.                     // Find the most recent active session for this user
  93.                     $sessionLog $this->sessionLogRepository->findOneBy(
  94.                         ['user' => $user'logoutAt' => null],
  95.                         ['loginAt' => 'DESC']
  96.                     );
  97.                     if (!$sessionLog) {
  98.                         $this->writeLog(" - LOGOUT: No active session found for user\n");
  99.                         return;
  100.                     }
  101.                     $this->writeLog(" - LOGOUT: Found session by user with session_id: " $sessionLog->getSessionId() . "\n");
  102.                 } else {
  103.                     $this->writeLog(" - LOGOUT: No authenticated user found\n");
  104.                     return;
  105.                 }
  106.             }
  107.             $this->writeLog(" - LOGOUT: Closing session for user: " $sessionLog->getUsername() . "\n");
  108.             $logoutTime = new DateTime();
  109.             $sessionLog->setLogoutAt($logoutTime);
  110.             $sessionLog->setLastActivityAt($logoutTime);
  111.             $sessionLog->setLogoutType('manual'); // User clicked logout
  112.             $this->entityManager->flush();
  113.             $this->writeLog(" - LOGOUT: Session closed successfully\n");
  114.         } catch (Exception $e) {
  115.             $this->writeLog(" - LOGOUT ERROR: " $e->getMessage() . "\n");
  116.             return;
  117.         }
  118.     }
  119.     /**
  120.      * Update last activity timestamp on each request
  121.      * ONLY if session hasn't been closed (no logoutAt)
  122.      */
  123.     public function onRequest(RequestEvent $event): void
  124.     {
  125.         error_log("UserSessionTracker::onRequest() CALLED at " date('Y-m-d H:i:s'));
  126.         $this->writeLog(" - onRequest called\n");
  127.         // Only track main requests, not sub-requests
  128.         if (!$event->isMainRequest()) {
  129.             $this->writeLog(" - Not main request, skipping\n");
  130.             return;
  131.         }
  132.         $request $event->getRequest();
  133.         // Skip if no session
  134.         if (!$request->hasSession()) {
  135.             $this->writeLog(" - No session\n");
  136.             return;
  137.         }
  138.         $session $request->getSession();
  139.         // Skip if session not started
  140.         if (!$session->isStarted()) {
  141.             $this->writeLog(" - Session not started\n");
  142.             return;
  143.         }
  144.         $sessionId $session->getId();
  145.         $this->writeLog(" - Session ID: $sessionId\n");
  146.         // Check if we should update activity (throttle updates)
  147.         $lastUpdate $session->get('_session_log_last_update');
  148.         $now time();
  149.         if ($lastUpdate && ($now $lastUpdate) < self::ACTIVITY_UPDATE_INTERVAL) {
  150.             $this->writeLog(" - Throttled (last update: $lastUpdate, now: $now)\n");
  151.             return;
  152.         }
  153.         $this->writeLog(" - Checking DB for session\n");
  154.         try {
  155.             $sessionLog $this->sessionLogRepository->findActiveBySessionId($sessionId);
  156.         } catch (Exception $e) {
  157.             // Skip if Doctrine is not ready yet (during cache warming, etc.)
  158.             $this->writeLog(" - Doctrine not ready: " $e->getMessage() . "\n");
  159.             return;
  160.         }
  161.         if (!$sessionLog) {
  162.             $this->writeLog(" - No session log found in DB\n");
  163.             // Session ID might have been regenerated after login - try to find by user
  164.             // Check if user is authenticated
  165.             $token $this->tokenStorage->getToken();
  166.             if ($token && $token->getUser() && method_exists($token->getUser(), 'getId')) {
  167.                 $user $token->getUser();
  168.                 $this->writeLog(" - User authenticated, searching by user_id: " $user->getId() . "\n");
  169.                 // Find the most recent session for this user (open OR closed by timeout)
  170.                 // First try to find an open session
  171.                 try {
  172.                     $sessionLog $this->sessionLogRepository->findOneBy(
  173.                         ['user' => $user'logoutAt' => null],
  174.                         ['loginAt' => 'DESC']
  175.                     );
  176.                 } catch (Exception $e) {                    
  177.                     $this->writeLog(" - Error finding session: " $e->getMessage() . "\n");
  178.                     return;
  179.                 }
  180.                 // If no open session, try to find the most recent timeout session
  181.                 if (!$sessionLog) {
  182.                     try {
  183.                         $sessionLog $this->sessionLogRepository->findOneBy(
  184.                             ['user' => $user'logoutType' => 'timeout'],
  185.                             ['loginAt' => 'DESC']
  186.                         );
  187.                     } catch (Exception $e) {
  188.                         $this->writeLog(" - Error finding timeout session: " $e->getMessage() . "\n");
  189.                         return;
  190.                     }
  191.                     if ($sessionLog) {
  192.                         $this->writeLog(" - Found timeout session by user, reopening with session_id: $sessionId\n");
  193.                     }
  194.                 }
  195.                 if ($sessionLog) {
  196.                     $this->writeLog(" - Found session by user, updating session_id from " $sessionLog->getSessionId() . " to $sessionId\n");
  197.                     // Update the session_id (Symfony regenerated it after login)
  198.                     $sessionLog->setSessionId($sessionId);
  199.                     $this->entityManager->flush();
  200.                 } else {
  201.                     $this->writeLog(" - No session found for user (neither open nor timeout)\n");
  202.                     return;
  203.                 }
  204.             } else {
  205.                 return;
  206.             }
  207.         }
  208.         
  209.         $this->writeLog(" - Found session for user: " $sessionLog->getUsername() . "\n");
  210.         // Check if session was auto-closed by timeout - reopen it
  211.         if ($sessionLog->getLogoutAt() !== null) {
  212.             if ($sessionLog->getLogoutType() === 'timeout') {
  213.                 $this->writeLog(" - Session was auto-closed, reopening\n");
  214.                 $sessionLog->setLogoutAt(null);
  215.                 $sessionLog->setLogoutType(null);
  216.                 $sessionLog->setDuration(null);
  217.             } else {
  218.                 // Manual logout - don't reopen
  219.                 $this->writeLog(" - Session manually closed, not reopening\n");
  220.                 return;
  221.             }
  222.         }
  223.         
  224.         $this->writeLog(" - Updating lastActivityAt\n");
  225.         $sessionLog->setLastActivityAt(new DateTime());
  226.         $this->entityManager->flush();
  227.         $this->writeLog(" - Updated successfully\n");
  228.         // Update throttle timestamp
  229.         $session->set('_session_log_last_update'$now);
  230.     }
  231.     // 🔹 Función helper para escribir logs portables
  232.     private function writeLog(string $message): void
  233.     {
  234.         $logDir $this->kernel->getProjectDir() . '/var/tmp';
  235.         if (!is_dir($logDir)) {
  236.             mkdir($logDir0777true);
  237.         }
  238.         $logFile $logDir '/session_tracker.log';
  239.         file_put_contents($logFiledate('Y-m-d H:i:s') . " - $message\n"FILE_APPEND);
  240.     }
  241. }