0

Lately, I've been working on a notification system. I made it possible to create them, and to the user that receive them to read them, and now I want for the user to be able to dismiss and delete them. I want to handle there deletion asynchronously so the user doesn't have to refresh the page everytime he dismisses a notification.

The least important notifications only remain during one request, so I handled them with the session flashbag, and, on click, just remove them from the DOM in javascript.

But some more important messages remain until the user click them. So in javascript, when the user clicks them, I send an Ajax request to a controller that should delete the notification. The problem is : the controller can't access to the currently connected user ! I tried with $this->getUser() and $tokenInterace->getToken(), but both return null.

Having access to the user would permit me to check if he really owns the notification and did not just type a random url trying to suppress someone's notification. It would also permit to clear all the notifications of the user (as another feature).

Here is all the configuration and code I have that I think can relate to this issue.

Symfony

# config/packages/framework.yaml
framework:
    session:
        handler_id: ~

SecurityBundle

# config/packages/security.yaml
security:
    # ...
    firewalls:
        dev:
            pattern: ^/(_(profiler|wdt))/
            security: false

        login:
            pattern: ^/login$
            anonymous: true

        main:
            pattern: ^/
            anonymous: false
            form_login:
                # ...
            remember_me:
                secret: '%env(APP_SECRET)%'
                lifetime: 604800

    access_control:
        # - { path: ^/api, role: ROLE_USER }
        # If I uncomment this line, Ajax request get redirected to login page 
        # ...

FOSUserBundle

# config/packages/fos_user.yaml
fos_user:
    db_driver: orm
    firewall_name: main
    user_class: App\Entity\User\User
    from_email:
        address:        '%env(MAILER_URL)%'
        sender_name:    '%env(MAILER_URL)%'

The controller

// The controller is imported with options prefix="/api", name_prefix="api_" and expose=true
// (name_prefix is new from Symfony 4.1, it permits to automatically prefix all imported route names)

/**
 * @Route("/notification", name="notification_")
 */
 class NotificationApiController extends BaseController
 {
     /**
      * @Route("/{id}", name="delete", requirements={"id"="\d+"})
      * @Method({"DELETE"})
      */
     public function delete(Notification $notification, TranslatorInterface $translator)
     {
         if ($notification->getUser() !== $this->getUser()) {
             return $this->createJsonResponse([
                 'error' => $translator->trans('error.notification.not_owner'),
             ], 403);
         }

         $em = $this->getDoctrine()->getEntityManager();
         $em->remove($notification);
         $em->flush();

         return $this->createJsonResponse();
     }

     /**
      * @Route("/clear", name="clear")
      * @Method({"DELETE"})
      */
     public function clear(NotificationRepository $notificationRepository, TranslatorInterface $translator)
     {
         $user = $this->getUser();

         if ($user === null) {
             return $this->createJsonResponse([
                 'error' => $translator->trans('error.notification.not_user'),
             ]);
         }

         $notificationRepository->clearForUser($user);

         return $this->createJsonResponse();
     }
 }

The Javascript

// Notifications are in the header
const $header = document.querySelector('header');
$header.addEventListener('click', (e) => {
  const $notif = e.target.closest('.notif');
  if ($notif !== null) {
    // Parse the notification id
    const id = +$notif.getAttribute('data-id');
    if (id !== null) {
      const route = Routing.generate('api_notification_delete', { id });

      // I don't care about browser support
      fetch(route, { method: 'DELETE' })
        .then((response) => {
          // check for errors
          return response.json();
        })
        .then((response) => {
          // remove notification from DOM;
        })
    }
  }
});

I'm almost sure the problem isn't about session expiring. Or maybe session works differently for asynchronous request ? (That would be very weird)

Anyone ever encountered the problem, or has an idea on how to fix it ? If you need anything more, feel free to ask !

EDIT

I found the origin of the problem. It's the 'fetch' fonction. The Fetch API doesn't use cookies by default. To use cookies, you must pass { credentials: 'same-origin' } as parameter to fetch each time you use it (as I found here).

Hope this can help someone one day !

Enzo Baldisserri
  • 285
  • 2
  • 10
  • Is the security.yml file you posted complete? I’ve normally encountered this issue when user is authenticated on one firewall but the request goes through another firewall. – gvf Jun 04 '18 at 21:55
  • Those are all the firewalls I have, so, as all the requests begin with /api, the firewall should be 'main'. I can't test it right now, but I'll test it soon and post the result – Enzo Baldisserri Jun 05 '18 at 07:11
  • I went into the Symfony profiler, which gave me `security.firewall.map.context.main` as `_firewall_context`, so I guess I'm on the right firewall ! Also, I have an `AuthenticationCredentialsNotFoundException` in the exceptions panel. – Enzo Baldisserri Jun 05 '18 at 11:20
  • Ok, next step is checking that you are making the ajax call to the correct url, for example if your user authenticates on _http://dev.test.com:8080/app_dev.php/api_ but the api call is _http://dev.test.com:8080/api_ that is probably the cause. Make sure both use the same protocol: _http_ or _https_. Also check that both either use _www_ or don't. One last thing to check, but is not likely to be the cause, would be to make sure session is being saved: If your user logs in, and then you refresh the page, is the user still loged in? – gvf Jun 05 '18 at 19:09

1 Answers1

1

I found the origin of the problem. It's the 'fetch' fonction. The Fetch API doesn't use cookies by default. To use cookies, you must pass { credentials: 'same-origin' } as parameter to fetch each time you use it (as I found here).

Hope this can help someone one day !

Enzo Baldisserri
  • 285
  • 2
  • 10