3

The admin user in my Symfony 4.2 application should be able to log out another (non-admin) user. I created a user login system depending on the Symfony security-bundle (https://symfony.com/doc/current/security/form_login_setup.html).

Now I am building an admin dashboard where all user have to be listed with their online status (last activity).

Is there a recommended way to list active users and kill their session if needed?

I've read some posts like this: Symfony how to return all logged in Active Users. But the answers are a little bit older and are just about listing the active users.

Hugues D
  • 322
  • 2
  • 10
igi
  • 125
  • 1
  • 15
  • What session handler are you using? – atymic Jul 25 '19 at 08:03
  • Good question. I did not change or install something for the session handling. So I would say "the default one", if there is a default one. I installed the user login handling by the make:user command. – igi Jul 29 '19 at 10:03

2 Answers2

2

Here's a good way to kill user sessions: use an EventListener with an onKernelRequest event. In your main code: public function onKernelRequest(KernelEvent $event)

$request = $event->getRequest();
$token = $this->container->get('security.token_storage')->getToken();

if ($token === null) { // somehow
        return;
}

if ($token->getUser()->isLocked() === true) {
        // you must implement a boolean flag on your user Entities, which the admins can set to false
        $this->container->get('security.token_storage')->setToken(); // default is null, therefore null
        $request->getSession()->invalidate(); // these lines will invalidate user session on next request
        return;
 }

Now, on to your other question: How to list users with their online status? Easy, your user Entities should implement another boolean flag, such as isOnline (with a getter and setter).

Next, you should create a LoginListener (no need to implement any interface). And in your main code:

public function onSecurityInteractiveLogin(InteractiveLoginEvent $event) {
       $user = $event->getAuthenticationToken()->getUser();
       if ($user instanceof UserInterface) {
             // set isOnline flag === true
             // you will need to fetch the $user with the EntityManager ($this->em)
             // make sure it exists, set the flag and then
             $this->em->flush();
       }
}

Your third event should be a LogoutListener, where you will set the isOnline flag === false

Symfony calls a LogoutListener (as a handler) when a user requests logout. But you can write your own:

class LogoutListener implements LogoutHandlerInterface {

 public function logout(Request $request, Response $response, TokenInterface $token): void
    {
        $user = $token->getUser();
        if (!$user instanceof UserInterface) { /** return if user is somehow anonymous
                * this should not happen here, unless... reasons */
                return;
        }

       // else
      $username = $user->getUsername(); // each user class must implement getUsername()
      // get the entity Manager ($this->em, injected in your constructor)
      // get your User repository
      $repository = $this->em->getRepository(MyUser::class);
      $user = $repository->findOneBy(['username' => $username]); // find one by username
      $user->setIsOnline(false);
      $this->em->flush(); // done, you've recorded a logout

    }
}

Hope this helps. With a bit of luck, it will. Cheers! :-)

Hugues D
  • 322
  • 2
  • 10
  • Note that if you just just set a DateTimeInterface in your Entities (such as $lastLogin), it won't help. You really need two booleans, such as: 1/ $locked (is account locked on administrator request) and 2/ $online (is user currently online - so that we know if we need to log him / her out on administrator request). Good luck – Hugues D Nov 11 '19 at 04:04
1

The correct way is to store the user session in the database.

https://symfony.com/doc/current/doctrine/pdo_session_storage.html (in here is the create syntax of the database table. Also add a user_id to the table)

in framework.yml add the Pdo Session Handler.

session:    
    handler_id: Symfony\Component\HttpFoundation\Session\Storage\Handler\PdoSessionHandler
    cookie_secure: auto
    cookie_samesite: lax

In service.yml add a listener and register the session handler

 # Handlers
    Symfony\Component\HttpFoundation\Session\Storage\Handler\PdoSessionHandler:
        arguments:
            - !service { class: PDO, factory: 'database_connection:getWrappedConnection' }
            - { lock_mode: 1 }

    # Listeners
    App\Listener\SessionListener:
        tags:
            - {name: kernel.event_listener, event: kernel.request, method: onRequestListener}

create a new listener in

class SessionListener
{

    /**
     * @var TokenStorageInterface
     */
    private $tokenStorage;

    /**
     * @var EntityManagerInterface
     */
    private $em;

    /**
     * @var SessionInterface
     */
    private $session;

    public function __construct(
        TokenStorageInterface $tokenStorage,
        EntityManagerInterface $em,
        SessionInterface $session
    ) {
        $this->tokenStorage = $tokenStorage;
        $this->em = $em;     
        $this->session = $session;
       }

    public function onRequestListener(GetResponseEvent $event): void
    {

        // If its not te master request or token is null
        if (!$event->isMasterRequest() || $this->tokenStorage->getToken() === null) {
            return;
        }

        /** @var User $user */
        $user = $this->tokenStorage->getToken()->getUser();

        // Check if user is logged in
        if (!$user instanceof User) {
            return;
        }

        $connection = $this->em->getConnection();

        try {
            $stmt = $connection->prepare('UPDATE `sessions` SET `user_id` = :userId WHERE `sess_id` = :sessionId');
            $stmt->execute([
                'userId' => $user->getId(),
                'sessionId' => $this->session->getId(),
            ]);
        } catch (DBALException $e) {
        }
     }
 }

Now just delete the sessions from this user.

 /**
     * @var EntityManagerInterface
     */
    private $em;

    public function __construct(EntityManagerInterface $em)
    {
        $this->em = $em;
    }

    public function delete(User $user): void
    {
        $sessions = $this->em->getRepository(Session::class)->findBy([
            'user' => $user,
        ]);

        foreach ($sessions as $session) {
            $this->em->remove($session);
        }

        $this->em->flush();
    }
EJTJ3
  • 13
  • 5