diff --git a/public/js/activity.js b/public/js/activity.js index 437636d..77a73e7 100644 --- a/public/js/activity.js +++ b/public/js/activity.js @@ -1,23 +1,25 @@ const activityContainer = document.querySelector('#activity') -fetch(`/api/activity`) +fetch(window.location) .then(async response => { const bearer = response.headers.get('x-mercure-token') - - if (!bearer) return - - const hubUrl = response.headers.get('Link').match(/<([^>]+)>;\s+rel=(?:mercure|"[^"]*mercure[^"]*")/)[1] + const topic = response.headers.get('x-mercure-topic') + const hubUrl = response.headers.get('Link') + .match(/<([^>]+)>;\s+rel=(?:mercure|"[^"]*mercure[^"]*")/)[1] const hub = new URL(hubUrl, window.origin); - const body = await response.json() - hub.searchParams.append('topic', body.topic) + hub.searchParams.append('topic', topic) + + const options = !bearer + ? {} + : { + headers: { + 'Authorization': bearer, + } + } - const es = new EventSourcePolyfill(hub, { - headers: { - 'Authorization': bearer, - } - }) + const es = new EventSourcePolyfill(hub, options) es.onmessage = event => { const data = JSON.parse(event.data) @@ -31,4 +33,4 @@ fetch(`/api/activity`) activityContainer.append(item) } - }); + }) diff --git a/public/js/dinosaur.js b/public/js/dinosaur.js index b2e41a3..a96b789 100644 --- a/public/js/dinosaur.js +++ b/public/js/dinosaur.js @@ -1,31 +1,25 @@ const alertContainer = document.querySelector('#alert-container') -const template = document.querySelector('#dinosaur-item-template') -const dinosaurList = document.querySelector('#dinosaur-list') -const dinosaurSection = document.querySelector('#dinosaur-section') -const dinosaurId = dinosaurSection.dataset.id -fetch(`/api/dinosaurs/${dinosaurId}`) +fetch(window.location) .then(async response => { - const bearer = response.headers.get('x-mercure-token') + const topic = response.headers.get('x-mercure-topic') + const hubUrl = response.headers.get('Link').match(/<([^>]+)>;\s+rel=(?:mercure|"[^"]*mercure[^"]*")/)[1] - /* - * If no JWT is provided, it means that the user won't - * be able to subscribe to the updates. - */ - if (!bearer) return + const hub = new URL(hubUrl); - const hubUrl = response.headers.get('Link').match(/<([^>]+)>;\s+rel=(?:mercure|"[^"]*mercure[^"]*")/)[1] + hub.searchParams.append('topic', topic) + + const es = new EventSource(hub) - const hub = new URL(hubUrl, window.origin); - const body = await response.json() + es.onmessage = e => { + const item = document.createElement('div') - hub.searchParams.append('topic', body.topic) + item.setAttribute('class', 'alert alert-danger') + item.setAttribute('role', 'alert') - const es = new EventSourcePolyfill(hub, { - headers: { - 'Authorization': bearer, - } - }) + item.innerHTML = 'Dinosaur has changed !' - es.onmessage = event => console.log(event) - }); + alertContainer.innerHTML = '' + alertContainer.append(item) + } + }) diff --git a/public/js/dinosaurs.js b/public/js/dinosaurs.js new file mode 100644 index 0000000..f10d9c5 --- /dev/null +++ b/public/js/dinosaurs.js @@ -0,0 +1,35 @@ +const alertContainer = document.querySelector('#alert-container') +const template = document.querySelector('#dinosaur-item-template') +const dinosaurList = document.querySelector('#dinosaur-list') + +fetch(window.location) + .then(async response => { + const topic = response.headers.get('x-mercure-topic') + const hubUrl = response.headers.get('Link').match(/<([^>]+)>;\s+rel=(?:mercure|"[^"]*mercure[^"]*")/)[1] + + const hub = new URL(hubUrl); + + hub.searchParams.append('topic', topic) + + const es = new EventSource(hub) + + es.onmessage = e => { + const dinosaur = JSON.parse(e.data) + + const clone = template.content.cloneNode(true) + const dinosaurTemplateNameContainer = clone.querySelector('#dinosaur-item-template-name') + const dinosaurTemplateLinkContainer = clone.querySelector('#dinosaur-item-template-link') + + dinosaurTemplateNameContainer.innerHTML = dinosaur.name + dinosaurTemplateLinkContainer.href = dinosaur.link + + dinosaurList.append(clone) + + alertContainer.innerHTML =`
Welcome to ${dinosaur.name}!
` + + window.setTimeout(() => { + const alert = document.querySelector('.alert') + alert.parentNode.removeChild(alert) + }, 5000); + } + }) diff --git a/public/js/realtime.js b/public/js/realtime.js deleted file mode 100644 index 41b2ade..0000000 --- a/public/js/realtime.js +++ /dev/null @@ -1,28 +0,0 @@ -const alertContainer = document.querySelector('#alert-container') -const template = document.querySelector('#dinosaur-item-template') -const dinosaurList = document.querySelector('#dinosaur-list') - -const url = new URL("http://localhost:81/.well-known/mercure") -url.searchParams.append('topic', 'http://dinosaur-app/dinosaurs/create') - -const eventSource = new EventSource(url) - -eventSource.onmessage = e => { - const dinosaur = JSON.parse(e.data) - - const clone = template.content.cloneNode(true) - const dinosaurTemplateNameContainer = clone.querySelector('#dinosaur-item-template-name') - const dinosaurTemplateLinkContainer = clone.querySelector('#dinosaur-item-template-link') - - dinosaurTemplateNameContainer.innerHTML = dinosaur.name - dinosaurTemplateLinkContainer.href = dinosaur.link - - dinosaurList.append(clone) - - alertContainer.innerHTML =`
Welcome to ${dinosaur.name}!
` - - window.setTimeout(() => { - const alert = document.querySelector('.alert') - alert.parentNode.removeChild(alert) - }, 5000); -} diff --git a/src/Controller/ActivityController.php b/src/Controller/ActivityController.php index 897d868..f4efd17 100644 --- a/src/Controller/ActivityController.php +++ b/src/Controller/ActivityController.php @@ -5,10 +5,7 @@ namespace App\Controller; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; -use Symfony\Component\HttpFoundation\JsonResponse; -use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\Mercure\Discovery; use Symfony\Component\Routing\Annotation\Route; class ActivityController extends AbstractController @@ -18,14 +15,4 @@ public function activity(): Response { return $this->render('activity.html.twig'); } - - #[Route('/api/activity', name: 'api_activity')] - public function apiActivity(Request $request, Discovery $discovery): Response - { - $discovery->addLink($request); - - return new JsonResponse([ - 'topic' => 'https://dinosaur-app/activity' - ]); - } } diff --git a/src/Controller/DinosaursController.php b/src/Controller/DinosaursController.php index e1271f7..006502b 100644 --- a/src/Controller/DinosaursController.php +++ b/src/Controller/DinosaursController.php @@ -5,12 +5,12 @@ use App\Entity\Dinosaur; use App\Form\Type\DinosaurType; use App\Form\Type\SearchType; +use App\Repository\DinosaurRepository; use Doctrine\Persistence\ManagerRegistry; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\Mercure\Discovery; use Symfony\Component\Mercure\HubInterface; use Symfony\Component\Mercure\Update; use Symfony\Component\Routing\Annotation\Route; @@ -18,8 +18,14 @@ class DinosaursController extends AbstractController { + public function __construct( + private HubInterface $hub, + private RouterInterface $router + ) { + } + #[Route('/dinosaurs', name: 'app_list_dinosaurs')] - public function list(Request $request, ManagerRegistry $doctrine): Response + public function list(Request $request, ManagerRegistry $doctrine, DinosaurRepository $dinosaurRepository): Response { $q = null; $form = $this->createForm(SearchType::class); @@ -32,10 +38,7 @@ public function list(Request $request, ManagerRegistry $doctrine): Response $q = $search['q']; } - $dinosaurs = $doctrine - ->getRepository(Dinosaur::class) - ->search($q) - ; + $dinosaurs = $dinosaurRepository->search($q); return $this->render('dinosaurs-list.html.twig', [ 'dinosaurs' => $dinosaurs, @@ -51,8 +54,7 @@ public function list(Request $request, ManagerRegistry $doctrine): Response public function single( string $id, ManagerRegistry $doctrine, - Request $request, - Discovery $discovery + Request $request ): Response { $dinosaur = $doctrine @@ -60,8 +62,6 @@ public function single( ->find($id) ; - $discovery->addLink($request); - if ($dinosaur === false) { throw $this->createNotFoundException( 'The dinosaur you are looking for does not exists.' @@ -81,8 +81,7 @@ public function single( public function apiSingle( string $id, ManagerRegistry $doctrine, - Request $request, - Discovery $discovery + Request $request ): Response { $dinosaur = $doctrine @@ -90,8 +89,6 @@ public function apiSingle( ->find($id) ; - $discovery->addLink($request); - if ($dinosaur === false) { throw $this->createNotFoundException( 'The dinosaur you are looking for does not exists.' @@ -104,16 +101,14 @@ public function apiSingle( 'gender' => $dinosaur->getGender(), 'age' => $dinosaur->getAge(), 'eyeColor' => $dinosaur->getEyesColor(), - 'topic' => "http://dinosaur-app/api/dinosaurs/{$dinosaur->getId()}" + 'topic' => "https://dinosaur-app/api/dinosaurs/{$dinosaur->getId()}" ]); } #[Route('/dinosaurs/create', name: 'app_create_dinosaur')] public function create( Request $request, - ManagerRegistry $doctrine, - RouterInterface $router, - HubInterface $hub + ManagerRegistry $doctrine ): Response { $form = $this->createForm(DinosaurType::class); @@ -128,14 +123,18 @@ public function create( $em->flush(); $update = new Update( - 'http://localhost/dinosaurs/create', + [ + 'https://dinosaur-app/dinosaurs', + 'https://dinosaur-app/activity' + ], json_encode([ + 'link' => $this->router->generate('app_single_dinosaur', ['id' => $dinosaur->getId()]), 'name' => $dinosaur->getName(), - 'link' => $router->generate('app_single_dinosaur', ['id' => $dinosaur->getId()]) + 'message' => "{$dinosaur->getName()} has been created!" ]) ); - $hub->publish($update); + $this->hub->publish($update); $this->addFlash('success', 'The dinosaur has been created!'); @@ -175,6 +174,19 @@ public function edit(Request $request, int $id, ManagerRegistry $doctrine): Resp $em->flush(); + $update = new Update( + [ + sprintf('https://dinosaur-app/dinosaurs/edit/%d', $id), + 'https://dinosaur-app/activity' + ], + json_encode([ + 'link' => $this->router->generate('app_single_dinosaur', ['id' => $dinosaur->getId()]), + 'message' => "{$dinosaur->getName()} has been edited!" + ]) + ); + + $this->hub->publish($update); + $this->addFlash('success', 'The dinosaur has been edited!'); return $this->redirectToRoute('app_list_dinosaurs'); @@ -207,6 +219,19 @@ public function remove(int $id, ManagerRegistry $doctrine): Response $em->remove($dinosaur); $em->flush(); + $update = new Update( + [ + sprintf('https://dinosaur-app/dinosaurs/remove/%d', $id), + 'https://dinosaur-app/activity' + ], + json_encode([ + 'link' => $this->router->generate('app_single_dinosaur', ['id' => $id]), + 'message' => "{$dinosaur->getName()} has been removed!" + ]) + ); + + $this->hub->publish($update); + $this->addFlash('success', 'The dinosaur has been removed!'); return $this->redirectToRoute('app_list_dinosaurs'); diff --git a/src/Controller/SpeciesController.php b/src/Controller/SpeciesController.php index 91206c6..fc517b8 100644 --- a/src/Controller/SpeciesController.php +++ b/src/Controller/SpeciesController.php @@ -8,10 +8,19 @@ use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Mercure\HubInterface; +use Symfony\Component\Mercure\Update; use Symfony\Component\Routing\Annotation\Route; +use Symfony\Component\Routing\RouterInterface; class SpeciesController extends AbstractController { + public function __construct( + private HubInterface $hub, + private RouterInterface $router + ) { + } + #[Route('/species', name: 'app_list_species')] public function list(ManagerRegistry $doctrine): Response { @@ -39,6 +48,19 @@ public function create(Request $request, ManagerRegistry $doctrine): Response $em->persist($species); $em->flush(); + $update = new Update( + [ + 'https://dinosaur-app/species/create', + 'https://dinosaur-app/activity' + ], + json_encode([ + 'link' => $this->router->generate('app_single_species', ['id' => $species->getId()]), + 'message' => "{$species->getName()} species has been created!" + ]) + ); + + $this->hub->publish($update); + $this->addFlash('success', 'The species has been created!'); return $this->redirectToRoute('app_list_species'); @@ -54,7 +76,7 @@ public function create(Request $request, ManagerRegistry $doctrine): Response name: 'app_edit_species', requirements: ['id' => '\d+'] )] - public function edit(Request $request, int $id, ManagerRegistry $doctrine): Response + public function edit(int $id, ManagerRegistry $doctrine): Response { $species = $doctrine ->getRepository(Species::class) @@ -75,6 +97,19 @@ public function edit(Request $request, int $id, ManagerRegistry $doctrine): Resp $em->flush(); + $update = new Update( + [ + "https://dinosaur-app/species/edit/{$id}", + 'https://dinosaur-app/activity' + ], + json_encode([ + 'link' => $this->router->generate('app_single_species', ['id' => $species->getId()]), + 'message' => "{$species->getName()} species has been edited!" + ]) + ); + + $this->hub->publish($update); + $this->addFlash('success', 'The species has been edited!'); return $this->redirectToRoute('app_list_species'); @@ -107,6 +142,18 @@ public function remove(int $id, ManagerRegistry $doctrine): Response $em->remove($species); $em->flush(); + $update = new Update( + [ + "https://dinosaur-app/species/remove/{$id}", + 'https://dinosaur-app/activity' + ], + json_encode([ + 'message' => "{$species->getName()} species has been created!" + ]) + ); + + $this->hub->publish($update); + $this->addFlash('success', 'The species has been removed!'); return $this->redirectToRoute('app_list_species'); diff --git a/src/JwtFactory.php b/src/JwtFactory.php index 8648570..0f0880b 100644 --- a/src/JwtFactory.php +++ b/src/JwtFactory.php @@ -6,44 +6,20 @@ use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Mercure\Authorization; -use Symfony\Component\Security\Core\Security; class JwtFactory { public function __construct( - private Authorization $authorization, - private Security $security + private Authorization $authorization ) { } public function generateJwt(Request $request): string { - $isAdmin = $this->security->isGranted('ROLE_ADMIN'); - - $subscriptions = $isAdmin - ? $this->getAdminSubscriptions() - : $this->getUserSubscriptions() - ; - /* * Using mercure authorization service to generate jwt token allows you to create * the token using the existing mercure configuration. */ - return $this->authorization->createCookie($request, $subscriptions)->getValue(); - } - - private function getAdminSubscriptions() - { - return [ - 'https://dinosaur-app/dinosaurs/{id}', - 'https://dinosaur-app/activity' - ]; - } - - private function getUserSubscriptions() - { - return [ - 'https://dinosaur-app/dinosaurs/{id}' - ]; + return $this->authorization->createCookie($request, ["*"])->getValue(); } } diff --git a/src/Listener/MercureAuthorizationListener.php b/src/Listener/MercureAuthorizationListener.php index 514315d..2d33560 100644 --- a/src/Listener/MercureAuthorizationListener.php +++ b/src/Listener/MercureAuthorizationListener.php @@ -4,18 +4,15 @@ namespace App\Listener; -use App\Entity\User; use App\JwtFactory; use Symfony\Component\EventDispatcher\Attribute\AsEventListener; use Symfony\Component\HttpKernel\Event\ResponseEvent; -use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; use Symfony\Component\Security\Core\Security; #[AsEventListener(event: ResponseEvent::class)] class MercureAuthorizationListener { public function __construct( - private TokenStorageInterface $tokenStorage, private Security $security, private JwtFactory $jwtFactory ) { @@ -23,22 +20,13 @@ public function __construct( public function onKernelResponse(ResponseEvent $event) { - $token = $this->tokenStorage->getToken(); + $isAdmin = $this->security->isGranted('ROLE_ADMIN'); - if (null === $token) { + if (!$isAdmin) { return; } - $user = $token->getUser(); - - if (!$user instanceof User) { - return; - } - - $token = $this - ->jwtFactory - ->generateJwt($event->getRequest()) - ; + $token = $this->jwtFactory->generateJwt($event->getRequest()); $response = $event->getResponse(); diff --git a/src/Listener/MercureTopicListener.php b/src/Listener/MercureTopicListener.php new file mode 100644 index 0000000..0685276 --- /dev/null +++ b/src/Listener/MercureTopicListener.php @@ -0,0 +1,37 @@ +getRequest(); + $response = $event->getResponse(); + + /** + * Here we could eventually read a config file to + * determine which routes should correspond to which topics. + */ + $path = $request->getPathInfo(); + + $this->discovery->addLink($request); + + $response->headers->set('x-mercure-topic', sprintf( + 'https://dinosaur-app%s', + $path + )); + } +} diff --git a/templates/activity.html.twig b/templates/activity.html.twig index b31a38b..74ed3cb 100644 --- a/templates/activity.html.twig +++ b/templates/activity.html.twig @@ -15,5 +15,9 @@
{# Here will go the realtime activity messages #}
+ +
+ {{ mercure('https://example.com/books/1')|json_encode(constant('JSON_UNESCAPED_SLASHES') b-or constant('JSON_HEX_TAG'))|raw }} + {% endblock %} \ No newline at end of file diff --git a/templates/dinosaur.html.twig b/templates/dinosaur.html.twig index 1bfc3da..d238638 100644 --- a/templates/dinosaur.html.twig +++ b/templates/dinosaur.html.twig @@ -23,6 +23,9 @@

{{ dinosaur.name }}

+
+ {# Here will go alert messages #} +
twbs
diff --git a/templates/dinosaurs-list.html.twig b/templates/dinosaurs-list.html.twig index b1e9685..770e2a0 100644 --- a/templates/dinosaurs-list.html.twig +++ b/templates/dinosaurs-list.html.twig @@ -9,7 +9,7 @@ {% block javascripts %} - + {% endblock %} {% block body %}