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 #}
+
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 %}