src/EventListener/UserSessionTracker.php line 176

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. $this->writeLog(" - Found session for user: " . $sessionLog->getUsername() . "\n");
  209. // Check if session was auto-closed by timeout - reopen it
  210. if ($sessionLog->getLogoutAt() !== null) {
  211. if ($sessionLog->getLogoutType() === 'timeout') {
  212. $this->writeLog(" - Session was auto-closed, reopening\n");
  213. $sessionLog->setLogoutAt(null);
  214. $sessionLog->setLogoutType(null);
  215. $sessionLog->setDuration(null);
  216. } else {
  217. // Manual logout - don't reopen
  218. $this->writeLog(" - Session manually closed, not reopening\n");
  219. return;
  220. }
  221. }
  222. $this->writeLog(" - Updating lastActivityAt\n");
  223. $sessionLog->setLastActivityAt(new DateTime());
  224. $this->entityManager->flush();
  225. $this->writeLog(" - Updated successfully\n");
  226. // Update throttle timestamp
  227. $session->set('_session_log_last_update', $now);
  228. }
  229. // 🔹 Función helper para escribir logs portables
  230. private function writeLog(string $message): void
  231. {
  232. $logDir = $this->kernel->getProjectDir() . '/var/tmp';
  233. if (!is_dir($logDir)) {
  234. mkdir($logDir, 0777, true);
  235. }
  236. $logFile = $logDir . '/session_tracker.log';
  237. file_put_contents($logFile, date('Y-m-d H:i:s') . " - $message\n", FILE_APPEND);
  238. }
  239. }