From 805230361eca3b3d04f2217dcd022384950d9647 Mon Sep 17 00:00:00 2001
From: AntoineGonzalez <45924026+AntoineGonzalez@users.noreply.github.com>
Date: Fri, 3 May 2024 14:01:45 +0200
Subject: [PATCH 1/6] Practice 1: Publish and receive my first message
---
.env.dist | 7 +
composer.json | 1 +
composer.lock | 242 ++++++++++++++++++++++++++++++++++-
config/bundles.php | 1 +
config/packages/mercure.yaml | 8 ++
docker-compose.yml | 12 ++
symfony.lock | 12 ++
7 files changed, 282 insertions(+), 1 deletion(-)
create mode 100644 config/packages/mercure.yaml
diff --git a/.env.dist b/.env.dist
index eeafa62..dccee71 100644
--- a/.env.dist
+++ b/.env.dist
@@ -7,3 +7,10 @@ MYSQL_VERSION=8.0.27
MYSQL_DATABASE=databasorus
MYSQL_USER=user
MYSQL_PASSWORD=password
+
+MERCURE_URL=http://mercure/.well-known/mercure
+MERCURE_PUBLIC_URL=http://localhost:81/.well-known/mercure
+MERCURE_SERVER_NAME=:80
+MERCURE_CORS_ALLOWED_ORIGINS="http://localhost"
+MERCURE_PUBLISHER_JWT_KEY=!ChangeThisMercureHubJWTSecretKey!
+MERCURE_SUBSCRIBER_JWT_KEY=!ChangeThisMercureHubJWTSecretKey!
diff --git a/composer.json b/composer.json
index f760f5c..94a5088 100644
--- a/composer.json
+++ b/composer.json
@@ -22,6 +22,7 @@
"symfony/http-client": "^7.0",
"symfony/intl": "^7.0",
"symfony/mailer": "^7.0",
+ "symfony/mercure-bundle": "^0.3.8",
"symfony/mime": "^7.0",
"symfony/monolog-bundle": "^3.10",
"symfony/notifier": "^7.0",
diff --git a/composer.lock b/composer.lock
index a3c72ad..44cc58d 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "e1d68fda857d3a6a99a9c9d4e0c1c4dd",
+ "content-hash": "c8a29d7d9743605bbbbbff21d5cf0908",
"packages": [
{
"name": "composer/package-versions-deprecated",
@@ -1505,6 +1505,79 @@
],
"time": "2023-10-18T10:00:55+00:00"
},
+ {
+ "name": "lcobucci/jwt",
+ "version": "5.3.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/lcobucci/jwt.git",
+ "reference": "08071d8d2c7f4b00222cc4b1fb6aa46990a80f83"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/lcobucci/jwt/zipball/08071d8d2c7f4b00222cc4b1fb6aa46990a80f83",
+ "reference": "08071d8d2c7f4b00222cc4b1fb6aa46990a80f83",
+ "shasum": ""
+ },
+ "require": {
+ "ext-openssl": "*",
+ "ext-sodium": "*",
+ "php": "~8.1.0 || ~8.2.0 || ~8.3.0",
+ "psr/clock": "^1.0"
+ },
+ "require-dev": {
+ "infection/infection": "^0.27.0",
+ "lcobucci/clock": "^3.0",
+ "lcobucci/coding-standard": "^11.0",
+ "phpbench/phpbench": "^1.2.9",
+ "phpstan/extension-installer": "^1.2",
+ "phpstan/phpstan": "^1.10.7",
+ "phpstan/phpstan-deprecation-rules": "^1.1.3",
+ "phpstan/phpstan-phpunit": "^1.3.10",
+ "phpstan/phpstan-strict-rules": "^1.5.0",
+ "phpunit/phpunit": "^10.2.6"
+ },
+ "suggest": {
+ "lcobucci/clock": ">= 3.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Lcobucci\\JWT\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Luís Cobucci",
+ "email": "lcobucci@gmail.com",
+ "role": "Developer"
+ }
+ ],
+ "description": "A simple library to work with JSON Web Token and JSON Web Signature",
+ "keywords": [
+ "JWS",
+ "jwt"
+ ],
+ "support": {
+ "issues": "https://github.com/lcobucci/jwt/issues",
+ "source": "https://github.com/lcobucci/jwt/tree/5.3.0"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/lcobucci",
+ "type": "github"
+ },
+ {
+ "url": "https://www.patreon.com/lcobucci",
+ "type": "patreon"
+ }
+ ],
+ "time": "2024-04-11T23:07:54+00:00"
+ },
{
"name": "monolog/monolog",
"version": "3.6.0",
@@ -4193,6 +4266,173 @@
],
"time": "2024-03-28T09:20:36+00:00"
},
+ {
+ "name": "symfony/mercure",
+ "version": "v0.6.5",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/mercure.git",
+ "reference": "304cf84609ef645d63adc65fc6250292909a461b"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/mercure/zipball/304cf84609ef645d63adc65fc6250292909a461b",
+ "reference": "304cf84609ef645d63adc65fc6250292909a461b",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.1.3",
+ "symfony/deprecation-contracts": "^2.0|^3.0|^4.0",
+ "symfony/http-client": "^4.4|^5.0|^6.0|^7.0",
+ "symfony/http-foundation": "^4.4|^5.0|^6.0|^7.0",
+ "symfony/polyfill-php80": "^1.22",
+ "symfony/web-link": "^4.4|^5.0|^6.0|^7.0"
+ },
+ "require-dev": {
+ "lcobucci/jwt": "^3.4|^4.0|^5.0",
+ "symfony/event-dispatcher": "^4.4|^5.0|^6.0|^7.0",
+ "symfony/http-kernel": "^4.4|^5.0|^6.0|^7.0",
+ "symfony/phpunit-bridge": "^5.2|^6.0|^7.0",
+ "symfony/stopwatch": "^4.4|^5.0|^6.0|^7.0",
+ "twig/twig": "^2.0|^3.0|^4.0"
+ },
+ "suggest": {
+ "symfony/stopwatch": "Integration with the profiler performances"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "0.6.x-dev"
+ },
+ "thanks": {
+ "name": "dunglas/mercure",
+ "url": "https://github.com/dunglas/mercure"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\Mercure\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Kévin Dunglas",
+ "email": "dunglas@gmail.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony Mercure Component",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "mercure",
+ "push",
+ "sse",
+ "updates"
+ ],
+ "support": {
+ "issues": "https://github.com/symfony/mercure/issues",
+ "source": "https://github.com/symfony/mercure/tree/v0.6.5"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/dunglas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/mercure",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2024-04-08T12:51:34+00:00"
+ },
+ {
+ "name": "symfony/mercure-bundle",
+ "version": "v0.3.8",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/mercure-bundle.git",
+ "reference": "e21ad84694b84c9a3c94bedf4edd82b66728abfc"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/mercure-bundle/zipball/e21ad84694b84c9a3c94bedf4edd82b66728abfc",
+ "reference": "e21ad84694b84c9a3c94bedf4edd82b66728abfc",
+ "shasum": ""
+ },
+ "require": {
+ "lcobucci/jwt": "^3.4|^4.0|^5.0",
+ "php": ">=7.1.3",
+ "symfony/config": "^4.4|^5.0|^6.0|^7.0",
+ "symfony/dependency-injection": "^4.4|^5.4|^6.0|^7.0",
+ "symfony/http-kernel": "^4.4|^5.0|^6.0|^7.0",
+ "symfony/mercure": "^0.6.1",
+ "symfony/web-link": "^4.4|^5.0|^6.0|^7.0"
+ },
+ "require-dev": {
+ "symfony/phpunit-bridge": "^4.3.7|^5.0|^6.0|^7.0",
+ "symfony/stopwatch": "^4.3.7|^5.0|^6.0|^7.0",
+ "symfony/ux-turbo": "*",
+ "symfony/var-dumper": "^4.3.7|^5.0|^6.0|^7.0"
+ },
+ "suggest": {
+ "symfony/messenger": "To use the Messenger integration"
+ },
+ "type": "symfony-bundle",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "0.3.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Bundle\\MercureBundle\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Kévin Dunglas",
+ "email": "dunglas@gmail.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony MercureBundle",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "mercure",
+ "push",
+ "sse",
+ "updates"
+ ],
+ "support": {
+ "issues": "https://github.com/symfony/mercure-bundle/issues",
+ "source": "https://github.com/symfony/mercure-bundle/tree/v0.3.8"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/dunglas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/mercure-bundle",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2023-12-03T22:26:24+00:00"
+ },
{
"name": "symfony/mime",
"version": "v7.0.6",
diff --git a/config/bundles.php b/config/bundles.php
index 3106eee..74d202e 100644
--- a/config/bundles.php
+++ b/config/bundles.php
@@ -12,4 +12,5 @@
Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true],
Twig\Extra\TwigExtraBundle\TwigExtraBundle::class => ['all' => true],
Doctrine\Bundle\FixturesBundle\DoctrineFixturesBundle::class => ['dev' => true, 'test' => true],
+ Symfony\Bundle\MercureBundle\MercureBundle::class => ['all' => true],
];
diff --git a/config/packages/mercure.yaml b/config/packages/mercure.yaml
new file mode 100644
index 0000000..859f91f
--- /dev/null
+++ b/config/packages/mercure.yaml
@@ -0,0 +1,8 @@
+mercure:
+ hubs:
+ default:
+ url: '%env(MERCURE_URL)%'
+ public_url: '%env(MERCURE_PUBLIC_URL)%'
+ jwt:
+ secret: '%env(MERCURE_PUBLISHER_JWT_KEY)%'
+ publish: '*'
diff --git a/docker-compose.yml b/docker-compose.yml
index 6f9f8ba..a15ed93 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -41,6 +41,18 @@ services:
volumes:
- db-data:/var/lib/mysql:rw
restart: unless-stopped
+ mercure:
+ image: dunglas/mercure
+ restart: unless-stopped
+ command: /usr/bin/caddy run --config /etc/caddy/Caddyfile.dev
+ environment:
+ SERVER_NAME: ${MERCURE_SERVER_NAME}
+ MERCURE_PUBLISHER_JWT_KEY: ${MERCURE_PUBLISHER_JWT_KEY}
+ MERCURE_SUBSCRIBER_JWT_KEY: ${MERCURE_SUBSCRIBER_JWT_KEY}
+ MERCURE_EXTRA_DIRECTIVES: |
+ cors_origin ${MERCURE_CORS_ALLOW_ORIGIN}
+ ports:
+ - 81:80
volumes:
db-data: ~
diff --git a/symfony.lock b/symfony.lock
index e355d56..49abc32 100644
--- a/symfony.lock
+++ b/symfony.lock
@@ -353,6 +353,18 @@
"ref": "fadbfe33303a76e25cb63401050439aa9b1a9c7f"
}
},
+ "symfony/mercure-bundle": {
+ "version": "0.3",
+ "recipe": {
+ "repo": "github.com/symfony/recipes",
+ "branch": "main",
+ "version": "0.3",
+ "ref": "d097c114aae82c5bc88d48ac164fe523f1003292"
+ },
+ "files": [
+ "config/packages/mercure.yaml"
+ ]
+ },
"symfony/mime": {
"version": "v5.3.8"
},
From 26b52b0492d63c0f033f4d9af9c169062251b4e6 Mon Sep 17 00:00:00 2001
From: AntoineGonzalez <45924026+AntoineGonzalez@users.noreply.github.com>
Date: Fri, 3 May 2024 15:25:13 +0200
Subject: [PATCH 2/6] Practice 2: basic usages
---
public/js/dinosaurs.js | 79 +++++++++++++++++++++++++
src/Controller/DinosaursController.php | 63 +++++++++++++++++---
templates/_dinosaur-list-item.html.twig | 16 +++++
templates/dinosaurs-list.html.twig | 40 ++++++-------
4 files changed, 167 insertions(+), 31 deletions(-)
create mode 100644 public/js/dinosaurs.js
create mode 100644 templates/_dinosaur-list-item.html.twig
diff --git a/public/js/dinosaurs.js b/public/js/dinosaurs.js
new file mode 100644
index 0000000..32f5b52
--- /dev/null
+++ b/public/js/dinosaurs.js
@@ -0,0 +1,79 @@
+const alertContainer = document.querySelector('#alert-container')
+const template = document.querySelector('#dinosaur-item-template')
+const dinosaurList = document.querySelector('#dinosaurs-list')
+
+const hub = new URL('http://localhost:81/.well-known/mercure')
+hub.searchParams.append('topic', 'http://localhost/dinosaurs')
+
+const es = new EventSource(hub);
+
+const displayToast = (message) => {
+ alertContainer.innerHTML = `
${message}
`
+
+ window.setTimeout(() => {
+ const alert = document.querySelector('.alert')
+ alert.parentNode.removeChild(alert)
+ }, 5000)
+}
+
+const addDinosaur = (id, name, link, message) => {
+ const clone = template.content.cloneNode(true)
+ const dinosaurTemplateNameContainer = clone.querySelector('.dinosaur-name')
+ const dinosaurTemplateLinkContainer = clone.querySelector('.dinosaur-link')
+
+ dinosaurTemplateNameContainer.innerHTML = name
+ dinosaurTemplateLinkContainer.href = link
+ dinosaurTemplateLinkContainer['data-id'] = id
+
+ dinosaurList.append(clone)
+
+ displayToast(message)
+}
+
+const updateDinosaur = (id, name, message) => {
+ const dinosaur = document.querySelector(`[data-id='${id}']`)
+
+ if(dinosaur) {
+ dinosaur.querySelector('.dinosaur-name').innerHTML = name
+
+ displayToast(message)
+ }
+}
+
+const removeDinosaur = (id) => {
+ const dinosaur = document.querySelector(`[data-id='${id}']`)
+
+ if(dinosaur) {
+ dinosaur.parentNode.removeChild(dinosaur)
+
+ displayToast(message)
+ }
+}
+
+es.onmessage = e => {
+ const message = JSON.parse(e.data)
+
+ switch(message.type) {
+ case 'created':
+ addDinosaur(
+ message.id,
+ message.name,
+ message.link,
+ message.message
+ )
+ break
+ case 'updated':
+ updateDinosaur(
+ message.id,
+ message.name,
+ message.message
+ )
+ break
+ case 'deleted':
+ removeDinosaur(
+ message.id,
+ message.message
+ )
+ break
+ }
+}
diff --git a/src/Controller/DinosaursController.php b/src/Controller/DinosaursController.php
index 4ced07c..a1216fa 100644
--- a/src/Controller/DinosaursController.php
+++ b/src/Controller/DinosaursController.php
@@ -10,12 +10,16 @@
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
+use Symfony\Component\Mercure\HubInterface;
+use Symfony\Component\Mercure\Update;
final class DinosaursController extends AbstractController
{
#[Route('/dinosaurs', name: 'app_list_dinosaurs')]
- public function list(Request $request, ManagerRegistry $doctrine): Response
- {
+ public function list(
+ Request $request,
+ ManagerRegistry $doctrine
+ ): Response {
$q = null;
$form = $this->createForm(SearchType::class);
@@ -58,8 +62,11 @@ public function single(string $id, ManagerRegistry $doctrine): Response
}
#[Route('/dinosaurs/create', name: 'app_create_dinosaur')]
- public function create(Request $request, ManagerRegistry $doctrine): Response
- {
+ public function create(
+ Request $request,
+ ManagerRegistry $doctrine,
+ HubInterface $hub
+ ): Response {
$form = $this->createForm(DinosaurType::class);
$form->handleRequest($request);
@@ -73,6 +80,17 @@ public function create(Request $request, ManagerRegistry $doctrine): Response
$this->addFlash('success', 'The dinosaur has been created!');
+ $hub->publish(new Update(
+ 'http://localhost/dinosaurs',
+ json_encode([
+ 'type' => 'created',
+ 'id' => $dinosaur->getId(),
+ 'name' => $dinosaur->getName(),
+ 'link' => $this->generateUrl('app_single_dinosaur', ['id' => $dinosaur->getId()]),
+ 'message' => "The dinosaur {$dinosaur->getName()} has been created !"
+ ])
+ ));
+
return $this->redirectToRoute('app_list_dinosaurs');
}
@@ -86,12 +104,18 @@ public function create(Request $request, ManagerRegistry $doctrine): Response
name: 'app_edit_dinosaur',
requirements: ['id' => '\d+']
)]
- public function edit(Request $request, int $id, ManagerRegistry $doctrine): Response
- {
+ public function edit(
+ Request $request,
+ int $id,
+ ManagerRegistry $doctrine,
+ HubInterface $hub
+ ): Response {
$dinosaur = $doctrine
->getRepository(Dinosaur::class)
->find($id);
+ $oldName = $dinosaur->getName();
+
if (false === $dinosaur) {
throw $this->createNotFoundException('The dinosaur you are looking for does not exists.');
}
@@ -108,6 +132,17 @@ public function edit(Request $request, int $id, ManagerRegistry $doctrine): Resp
$this->addFlash('success', 'The dinosaur has been edited!');
+ $hub->publish(new Update(
+ 'http://localhost/dinosaurs',
+ json_encode([
+ 'type' => 'updated',
+ 'id' => $dinosaur->getId(),
+ 'name' => $dinosaur->getName(),
+ 'link' => $this->generateUrl('app_single_dinosaur', ['id' => $dinosaur->getId()]),
+ 'message' => "The dinosaur {$oldName} has been edited !"
+ ]),
+ ));
+
return $this->redirectToRoute('app_list_dinosaurs');
}
@@ -121,8 +156,11 @@ public function edit(Request $request, int $id, ManagerRegistry $doctrine): Resp
name: 'app_remove_dinosaur',
requirements: ['id' => '\d+']
)]
- public function remove(int $id, ManagerRegistry $doctrine): Response
- {
+ public function remove(
+ int $id,
+ ManagerRegistry $doctrine,
+ HubInterface $hub
+ ): Response {
$dinosaur = $doctrine
->getRepository(Dinosaur::class)
->find($id);
@@ -137,6 +175,15 @@ public function remove(int $id, ManagerRegistry $doctrine): Response
$this->addFlash('success', 'The dinosaur has been removed!');
+ $hub->publish(new Update(
+ 'http://localhost/dinosaurs',
+ json_encode([
+ 'type' => 'deleted',
+ 'id' => $id,
+ 'message' => "The dinosaur {$dinosaur->getName()} has been removed !"
+ ])
+ ));
+
return $this->redirectToRoute('app_list_dinosaurs');
}
}
diff --git a/templates/_dinosaur-list-item.html.twig b/templates/_dinosaur-list-item.html.twig
new file mode 100644
index 0000000..04eea41
--- /dev/null
+++ b/templates/_dinosaur-list-item.html.twig
@@ -0,0 +1,16 @@
+
+
+
+ {{ dinosaur|default(null) is not same as null ? dinosaur.name : '' }}
+
+
diff --git a/templates/dinosaurs-list.html.twig b/templates/dinosaurs-list.html.twig
index e5588f9..3da7009 100644
--- a/templates/dinosaurs-list.html.twig
+++ b/templates/dinosaurs-list.html.twig
@@ -3,33 +3,27 @@
{% block title %}Dinosaurs{% endblock %}
{% block stylesheets %}
-{{ parent() }}
-
+ {{ parent() }}
+
{% endblock %}
{% block javascripts %}
-
+
+
{% endblock %}
{% block body %}
-
-
-
+
+
+
+
+
+ {% for dinosaur in dinosaurs %}
+ {{ include('_dinosaur-list-item.html.twig', {dinosaur: dinosaur}) }}
+ {% endfor %}
+
+
+
+ {{ include('_dinosaur-list-item.html.twig') }}
+
{% endblock %}
From 9b4f68d730f9bce66159bdf5bb59998059c04f95 Mon Sep 17 00:00:00 2001
From: AntoineGonzalez <45924026+AntoineGonzalez@users.noreply.github.com>
Date: Mon, 6 May 2024 12:03:25 +0200
Subject: [PATCH 3/6] Practice 2: basic usages (improvements)
---
public/js/dinosaurs.js | 54 ++++++++++++------------
src/Controller/DinosaursController.php | 52 +++++++++--------------
src/Realtime/Trigger.php | 26 ++++++++++++
src/Realtime/Trigger/DinosaurCreated.php | 27 ++++++++++++
src/Realtime/Trigger/DinosaurDeleted.php | 23 ++++++++++
src/Realtime/Trigger/DinosaurUpdated.php | 28 ++++++++++++
src/Service/Realtime/Publisher.php | 26 ++++++++++++
7 files changed, 179 insertions(+), 57 deletions(-)
create mode 100644 src/Realtime/Trigger.php
create mode 100644 src/Realtime/Trigger/DinosaurCreated.php
create mode 100644 src/Realtime/Trigger/DinosaurDeleted.php
create mode 100644 src/Realtime/Trigger/DinosaurUpdated.php
create mode 100644 src/Service/Realtime/Publisher.php
diff --git a/public/js/dinosaurs.js b/public/js/dinosaurs.js
index 32f5b52..68138be 100644
--- a/public/js/dinosaurs.js
+++ b/public/js/dinosaurs.js
@@ -40,7 +40,7 @@ const updateDinosaur = (id, name, message) => {
}
}
-const removeDinosaur = (id) => {
+const removeDinosaur = (id, message) => {
const dinosaur = document.querySelector(`[data-id='${id}']`)
if(dinosaur) {
@@ -50,30 +50,32 @@ const removeDinosaur = (id) => {
}
}
-es.onmessage = e => {
+es.addEventListener('created', e => {
const message = JSON.parse(e.data)
- switch(message.type) {
- case 'created':
- addDinosaur(
- message.id,
- message.name,
- message.link,
- message.message
- )
- break
- case 'updated':
- updateDinosaur(
- message.id,
- message.name,
- message.message
- )
- break
- case 'deleted':
- removeDinosaur(
- message.id,
- message.message
- )
- break
- }
-}
+ addDinosaur(
+ message.id,
+ message.name,
+ message.link,
+ message.message
+ )
+})
+
+es.addEventListener('updated', e => {
+ const message = JSON.parse(e.data)
+
+ updateDinosaur(
+ message.id,
+ message.name,
+ message.message
+ )
+})
+
+es.addEventListener('deleted', e => {
+ const message = JSON.parse(e.data)
+
+ removeDinosaur(
+ message.id,
+ message.message
+ )
+})
diff --git a/src/Controller/DinosaursController.php b/src/Controller/DinosaursController.php
index a1216fa..7966659 100644
--- a/src/Controller/DinosaursController.php
+++ b/src/Controller/DinosaursController.php
@@ -5,13 +5,15 @@
use App\Entity\Dinosaur;
use App\Form\Type\DinosaurType;
use App\Form\Type\SearchType;
+use App\Service\Realtime\Publisher;
+use App\Realtime\Trigger\DinosaurCreated;
+use App\Realtime\Trigger\DinosaurDeleted;
+use App\Realtime\Trigger\DinosaurUpdated;
use Doctrine\Persistence\ManagerRegistry;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
-use Symfony\Component\Mercure\HubInterface;
-use Symfony\Component\Mercure\Update;
final class DinosaursController extends AbstractController
{
@@ -65,7 +67,7 @@ public function single(string $id, ManagerRegistry $doctrine): Response
public function create(
Request $request,
ManagerRegistry $doctrine,
- HubInterface $hub
+ Publisher $publisher
): Response {
$form = $this->createForm(DinosaurType::class);
@@ -80,15 +82,12 @@ public function create(
$this->addFlash('success', 'The dinosaur has been created!');
- $hub->publish(new Update(
- 'http://localhost/dinosaurs',
- json_encode([
- 'type' => 'created',
- 'id' => $dinosaur->getId(),
- 'name' => $dinosaur->getName(),
- 'link' => $this->generateUrl('app_single_dinosaur', ['id' => $dinosaur->getId()]),
- 'message' => "The dinosaur {$dinosaur->getName()} has been created !"
- ])
+ $publisher->publish(new DinosaurCreated(
+ $dinosaur,
+ $this->generateUrl(
+ 'app_single_dinosaur',
+ ['id' => $dinosaur->getId()]
+ )
));
return $this->redirectToRoute('app_list_dinosaurs');
@@ -108,7 +107,7 @@ public function edit(
Request $request,
int $id,
ManagerRegistry $doctrine,
- HubInterface $hub
+ Publisher $publisher
): Response {
$dinosaur = $doctrine
->getRepository(Dinosaur::class)
@@ -132,15 +131,13 @@ public function edit(
$this->addFlash('success', 'The dinosaur has been edited!');
- $hub->publish(new Update(
- 'http://localhost/dinosaurs',
- json_encode([
- 'type' => 'updated',
- 'id' => $dinosaur->getId(),
- 'name' => $dinosaur->getName(),
- 'link' => $this->generateUrl('app_single_dinosaur', ['id' => $dinosaur->getId()]),
- 'message' => "The dinosaur {$oldName} has been edited !"
- ]),
+ $publisher->publish(new DinosaurUpdated(
+ $dinosaur,
+ $oldName,
+ $this->generateUrl(
+ 'app_single_dinosaur',
+ ['id' => $dinosaur->getId()]
+ )
));
return $this->redirectToRoute('app_list_dinosaurs');
@@ -159,7 +156,7 @@ public function edit(
public function remove(
int $id,
ManagerRegistry $doctrine,
- HubInterface $hub
+ Publisher $publisher
): Response {
$dinosaur = $doctrine
->getRepository(Dinosaur::class)
@@ -175,14 +172,7 @@ public function remove(
$this->addFlash('success', 'The dinosaur has been removed!');
- $hub->publish(new Update(
- 'http://localhost/dinosaurs',
- json_encode([
- 'type' => 'deleted',
- 'id' => $id,
- 'message' => "The dinosaur {$dinosaur->getName()} has been removed !"
- ])
- ));
+ $publisher->publish(new DinosaurDeleted($id, $dinosaur));
return $this->redirectToRoute('app_list_dinosaurs');
}
diff --git a/src/Realtime/Trigger.php b/src/Realtime/Trigger.php
new file mode 100644
index 0000000..235e460
--- /dev/null
+++ b/src/Realtime/Trigger.php
@@ -0,0 +1,26 @@
+ $topics
+ * @param array $data
+ */
+ public function __construct(
+ public string $type,
+ public array $topics,
+ private array $data
+ ) {
+ }
+
+ public function jsonSerialize(): array
+ {
+ return $this->data;
+ }
+}
diff --git a/src/Realtime/Trigger/DinosaurCreated.php b/src/Realtime/Trigger/DinosaurCreated.php
new file mode 100644
index 0000000..dbc7f08
--- /dev/null
+++ b/src/Realtime/Trigger/DinosaurCreated.php
@@ -0,0 +1,27 @@
+ $dinosaur->getId(),
+ 'name' => $dinosaur->getName(),
+ 'link' => $link,
+ 'message' => "The dinosaur {$dinosaur->getName()} has been created !"
+ ]
+ );
+ }
+}
diff --git a/src/Realtime/Trigger/DinosaurDeleted.php b/src/Realtime/Trigger/DinosaurDeleted.php
new file mode 100644
index 0000000..90f8e0a
--- /dev/null
+++ b/src/Realtime/Trigger/DinosaurDeleted.php
@@ -0,0 +1,23 @@
+ $id,
+ 'message' => "The dinosaur {$dinosaur->getName()} has been removed !"
+ ]
+ );
+ }
+}
diff --git a/src/Realtime/Trigger/DinosaurUpdated.php b/src/Realtime/Trigger/DinosaurUpdated.php
new file mode 100644
index 0000000..433be32
--- /dev/null
+++ b/src/Realtime/Trigger/DinosaurUpdated.php
@@ -0,0 +1,28 @@
+ $dinosaur->getId(),
+ 'name' => $dinosaur->getName(),
+ 'link' => $link,
+ 'message' => "The dinosaur {$oldName} has been edited !"
+ ]
+ );
+ }
+}
diff --git a/src/Service/Realtime/Publisher.php b/src/Service/Realtime/Publisher.php
new file mode 100644
index 0000000..2cc27ef
--- /dev/null
+++ b/src/Service/Realtime/Publisher.php
@@ -0,0 +1,26 @@
+hub->publish(new Update(
+ $trigger->topics,
+ json_encode($trigger),
+ type: $trigger->type
+ ));
+ }
+}
From 7184f98e7e873e053a6d3f1959de69594fd05064 Mon Sep 17 00:00:00 2001
From: AntoineGonzalez <45924026+AntoineGonzalez@users.noreply.github.com>
Date: Mon, 6 May 2024 15:43:51 +0200
Subject: [PATCH 4/6] Practice 3: discovery
---
.env.dist | 3 +-
config/packages/mercure.yaml | 2 +-
config/services.yaml | 4 ++
docker-compose.yml | 6 +-
public/js/dinosaurs.js | 80 +++++++++++++----------
src/Controller/DinosaursController.php | 3 +-
src/Listener/MercureDiscoveryListener.php | 24 +++++++
7 files changed, 82 insertions(+), 40 deletions(-)
create mode 100644 src/Listener/MercureDiscoveryListener.php
diff --git a/.env.dist b/.env.dist
index dccee71..07a293b 100644
--- a/.env.dist
+++ b/.env.dist
@@ -12,5 +12,4 @@ MERCURE_URL=http://mercure/.well-known/mercure
MERCURE_PUBLIC_URL=http://localhost:81/.well-known/mercure
MERCURE_SERVER_NAME=:80
MERCURE_CORS_ALLOWED_ORIGINS="http://localhost"
-MERCURE_PUBLISHER_JWT_KEY=!ChangeThisMercureHubJWTSecretKey!
-MERCURE_SUBSCRIBER_JWT_KEY=!ChangeThisMercureHubJWTSecretKey!
+MERCURE_JWT_KEY=!ChangeThisMercureHubJWTSecretKey!
diff --git a/config/packages/mercure.yaml b/config/packages/mercure.yaml
index 859f91f..4a27845 100644
--- a/config/packages/mercure.yaml
+++ b/config/packages/mercure.yaml
@@ -4,5 +4,5 @@ mercure:
url: '%env(MERCURE_URL)%'
public_url: '%env(MERCURE_PUBLIC_URL)%'
jwt:
- secret: '%env(MERCURE_PUBLISHER_JWT_KEY)%'
+ secret: '%env(MERCURE_JWT_KEY)%'
publish: '*'
diff --git a/config/services.yaml b/config/services.yaml
index 1d3246d..c2965de 100644
--- a/config/services.yaml
+++ b/config/services.yaml
@@ -24,5 +24,9 @@ services:
- '../src/Entity/'
- '../src/Kernel.php'
+ App\Listener\MercureDiscoveryListener:
+ tags:
+ - { name: kernel.event_listener, event: ResponseEvent, method: onKernelResponse }
+
# add more service definitions when explicit configuration is needed
# please note that last definitions always *replace* previous ones
diff --git a/docker-compose.yml b/docker-compose.yml
index a15ed93..3721932 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -47,10 +47,10 @@ services:
command: /usr/bin/caddy run --config /etc/caddy/Caddyfile.dev
environment:
SERVER_NAME: ${MERCURE_SERVER_NAME}
- MERCURE_PUBLISHER_JWT_KEY: ${MERCURE_PUBLISHER_JWT_KEY}
- MERCURE_SUBSCRIBER_JWT_KEY: ${MERCURE_SUBSCRIBER_JWT_KEY}
+ MERCURE_PUBLISHER_JWT_KEY: ${MERCURE_JWT_KEY}
+ MERCURE_SUBSCRIBER_JWT_KEY: ${MERCURE_JWT_KEY}
MERCURE_EXTRA_DIRECTIVES: |
- cors_origin ${MERCURE_CORS_ALLOW_ORIGIN}
+ cors_origins ${MERCURE_CORS_ALLOWED_ORIGINS}
ports:
- 81:80
diff --git a/public/js/dinosaurs.js b/public/js/dinosaurs.js
index 68138be..caf5440 100644
--- a/public/js/dinosaurs.js
+++ b/public/js/dinosaurs.js
@@ -2,10 +2,16 @@ const alertContainer = document.querySelector('#alert-container')
const template = document.querySelector('#dinosaur-item-template')
const dinosaurList = document.querySelector('#dinosaurs-list')
-const hub = new URL('http://localhost:81/.well-known/mercure')
-hub.searchParams.append('topic', 'http://localhost/dinosaurs')
-
-const es = new EventSource(hub);
+const dicoverMercureHub = async () => fetch(window.location)
+ .then(response => {
+ const hubUrl = response
+ .headers
+ .get('Link')
+ .match(/<([^>]+)>;\s+rel=(?:mercure|"[^"]*mercure[^"]*")/)[1];
+
+ return new URL(hubUrl);
+ }
+)
const displayToast = (message) => {
alertContainer.innerHTML = `${message}
`
@@ -50,32 +56,40 @@ const removeDinosaur = (id, message) => {
}
}
-es.addEventListener('created', e => {
- const message = JSON.parse(e.data)
-
- addDinosaur(
- message.id,
- message.name,
- message.link,
- message.message
- )
-})
-
-es.addEventListener('updated', e => {
- const message = JSON.parse(e.data)
-
- updateDinosaur(
- message.id,
- message.name,
- message.message
- )
-})
-
-es.addEventListener('deleted', e => {
- const message = JSON.parse(e.data)
-
- removeDinosaur(
- message.id,
- message.message
- )
-})
+document.addEventListener('DOMContentLoaded', async () => {
+ const hubUrl = await dicoverMercureHub();
+
+ hubUrl.searchParams.append('topic', 'http://localhost/dinosaurs')
+
+ const es = new EventSource(hubUrl);
+
+ es.addEventListener('created', e => {
+ const message = JSON.parse(e.data)
+
+ addDinosaur(
+ message.id,
+ message.name,
+ message.link,
+ message.message
+ )
+ })
+
+ es.addEventListener('updated', e => {
+ const message = JSON.parse(e.data)
+
+ updateDinosaur(
+ message.id,
+ message.name,
+ message.message
+ )
+ })
+
+ es.addEventListener('deleted', e => {
+ const message = JSON.parse(e.data)
+
+ removeDinosaur(
+ message.id,
+ message.message
+ )
+ })
+});
diff --git a/src/Controller/DinosaursController.php b/src/Controller/DinosaursController.php
index 7966659..1a97af3 100644
--- a/src/Controller/DinosaursController.php
+++ b/src/Controller/DinosaursController.php
@@ -13,6 +13,7 @@
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
+use Symfony\Component\Mercure\Discovery;
use Symfony\Component\Routing\Annotation\Route;
final class DinosaursController extends AbstractController
@@ -20,7 +21,7 @@ final class DinosaursController extends AbstractController
#[Route('/dinosaurs', name: 'app_list_dinosaurs')]
public function list(
Request $request,
- ManagerRegistry $doctrine
+ ManagerRegistry $doctrine,
): Response {
$q = null;
$form = $this->createForm(SearchType::class);
diff --git a/src/Listener/MercureDiscoveryListener.php b/src/Listener/MercureDiscoveryListener.php
new file mode 100644
index 0000000..d31efd2
--- /dev/null
+++ b/src/Listener/MercureDiscoveryListener.php
@@ -0,0 +1,24 @@
+getRequest();
+
+ $this->discovery->addLink($request);
+ }
+}
From b565614c7f97d9c97f8f022cd065428a2812c4ca Mon Sep 17 00:00:00 2001
From: AntoineGonzalez <45924026+AntoineGonzalez@users.noreply.github.com>
Date: Tue, 7 May 2024 13:51:56 +0200
Subject: [PATCH 5/6] Practice 4: Authorization with Cookie
---
config/services.yaml | 9 +++
public/js/activity.js | 63 +++++++++++++++++++
public/js/dinosaurs.js | 2 +-
src/Controller/ActivityController.php | 18 ++++++
src/Controller/DinosaursController.php | 1 -
src/Listener/AuthenticationListener.php | 36 +++++++++++
src/Listener/MercureAuthorizationListener.php | 41 ++++++++++++
src/Realtime/Trigger.php | 5 +-
src/Realtime/Trigger/DinosaurCreated.php | 2 +-
src/Realtime/Trigger/DinosaurDeleted.php | 2 +-
src/Realtime/Trigger/DinosaurUpdated.php | 2 +-
src/Realtime/Trigger/UserLoggedIn.php | 22 +++++++
src/Realtime/Trigger/UserLoggetOut.php | 22 +++++++
src/Service/Realtime/Publisher.php | 3 +-
templates/activity.html.twig | 18 ++++++
templates/base.html.twig | 28 ++++++---
16 files changed, 256 insertions(+), 18 deletions(-)
create mode 100644 public/js/activity.js
create mode 100644 src/Controller/ActivityController.php
create mode 100644 src/Listener/AuthenticationListener.php
create mode 100644 src/Listener/MercureAuthorizationListener.php
create mode 100644 src/Realtime/Trigger/UserLoggedIn.php
create mode 100644 src/Realtime/Trigger/UserLoggetOut.php
create mode 100644 templates/activity.html.twig
diff --git a/config/services.yaml b/config/services.yaml
index c2965de..35c5c1c 100644
--- a/config/services.yaml
+++ b/config/services.yaml
@@ -28,5 +28,14 @@ services:
tags:
- { name: kernel.event_listener, event: ResponseEvent, method: onKernelResponse }
+ App\Listener\MercureAuthorizationListener:
+ tags:
+ - { name: kernel.event_listener, event: ResponseEvent, method: onKernelResponse }
+
+ App\Listener\AuthenticationListener:
+ tags:
+ - { name: kernel.event_listener, event: loginSuccessEvent, method: onLoginSuccess }
+ - { name: kernel.event_listener, event: loginFailureEvent, method: onLoginFailure }
+
# add more service definitions when explicit configuration is needed
# please note that last definitions always *replace* previous ones
diff --git a/public/js/activity.js b/public/js/activity.js
new file mode 100644
index 0000000..ea22d89
--- /dev/null
+++ b/public/js/activity.js
@@ -0,0 +1,63 @@
+const activityContainer = document.querySelector('#activity-container')
+
+
+const dicoverMercureHub = async () => fetch(window.location)
+ .then(response => {
+ const hubUrl = response
+ .headers
+ .get('Link')
+ .match(/<([^>]+)>;\s+rel=(?:mercure|"[^"]*mercure[^"]*")/)[1];
+
+ return new URL(hubUrl);
+ }
+)
+
+const addLogItem = (message) => {
+ const item = document.createElement('li')
+
+ item.setAttribute('class', 'alert alert-primary')
+ item.setAttribute('role', 'alert')
+
+ item.innerHTML = message
+
+ activityContainer.append(item)
+}
+
+document.addEventListener('DOMContentLoaded', async () => {
+ const hubUrl = await dicoverMercureHub();
+
+ hubUrl.searchParams.append('topic', 'http://localhost/activity')
+ hubUrl.searchParams.append('topic', 'http://localhost/dinosaurs')
+
+ const es = new EventSource(hubUrl, { withCredentials: true });
+
+ es.addEventListener('login', e => {
+ const message = JSON.parse(e.data)
+
+ addLogItem(message.message)
+ })
+
+ es.addEventListener('logout', e => {
+ const message = JSON.parse(e.data)
+
+ addLogItem(message.message)
+ })
+
+ es.addEventListener('created', e => {
+ const message = JSON.parse(e.data)
+
+ addLogItem(message.message)
+ })
+
+ es.addEventListener('updated', e => {
+ const message = JSON.parse(e.data)
+
+ addLogItem(message.message)
+ })
+
+ es.addEventListener('deleted', e => {
+ const message = JSON.parse(e.data)
+
+ addLogItem(message.message)
+ })
+});
diff --git a/public/js/dinosaurs.js b/public/js/dinosaurs.js
index caf5440..007ffe3 100644
--- a/public/js/dinosaurs.js
+++ b/public/js/dinosaurs.js
@@ -61,7 +61,7 @@ document.addEventListener('DOMContentLoaded', async () => {
hubUrl.searchParams.append('topic', 'http://localhost/dinosaurs')
- const es = new EventSource(hubUrl);
+ const es = new EventSource(hubUrl, { withCredentials: true });
es.addEventListener('created', e => {
const message = JSON.parse(e.data)
diff --git a/src/Controller/ActivityController.php b/src/Controller/ActivityController.php
new file mode 100644
index 0000000..d8e143d
--- /dev/null
+++ b/src/Controller/ActivityController.php
@@ -0,0 +1,18 @@
+render('activity.html.twig');
+ }
+}
diff --git a/src/Controller/DinosaursController.php b/src/Controller/DinosaursController.php
index 1a97af3..85ffc62 100644
--- a/src/Controller/DinosaursController.php
+++ b/src/Controller/DinosaursController.php
@@ -13,7 +13,6 @@
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
-use Symfony\Component\Mercure\Discovery;
use Symfony\Component\Routing\Annotation\Route;
final class DinosaursController extends AbstractController
diff --git a/src/Listener/AuthenticationListener.php b/src/Listener/AuthenticationListener.php
new file mode 100644
index 0000000..331a3cc
--- /dev/null
+++ b/src/Listener/AuthenticationListener.php
@@ -0,0 +1,36 @@
+getAuthenticatedToken()->getUserIdentifier();
+
+ $this->publisher->publish(new UserLoggetIn($identifier));
+ }
+
+ #[AsEventListener(event: LogoutEvent::class)]
+ public function onLogout(LogoutEvent $event): void
+ {
+ $identifier = $event->getToken()->getUserIdentifier();
+
+ $this->publisher->publish(new UserLoggetOut($identifier));
+ }
+}
diff --git a/src/Listener/MercureAuthorizationListener.php b/src/Listener/MercureAuthorizationListener.php
new file mode 100644
index 0000000..4ecb4e9
--- /dev/null
+++ b/src/Listener/MercureAuthorizationListener.php
@@ -0,0 +1,41 @@
+security->isGranted('ROLE_USER')) {
+ $topics[] = 'http://localhost/dinosaurs';
+ }
+
+ if ($this->security->isGranted('ROLE_ADMIN')) {
+ $topics[] = 'http://localhost/activity';
+ }
+
+ if (empty($topics)) {
+ return;
+ }
+
+ $request = $event->getRequest();
+
+ $this->authorization->setCookie($request, $topics);
+ }
+}
diff --git a/src/Realtime/Trigger.php b/src/Realtime/Trigger.php
index 235e460..32d9392 100644
--- a/src/Realtime/Trigger.php
+++ b/src/Realtime/Trigger.php
@@ -6,7 +6,7 @@
use JsonSerializable;
-abstract class Trigger implements JsonSerializable
+abstract readonly class Trigger implements JsonSerializable
{
/**
* @param array $topics
@@ -15,7 +15,8 @@ abstract class Trigger implements JsonSerializable
public function __construct(
public string $type,
public array $topics,
- private array $data
+ private array $data,
+ public bool $isPrivate = true
) {
}
diff --git a/src/Realtime/Trigger/DinosaurCreated.php b/src/Realtime/Trigger/DinosaurCreated.php
index dbc7f08..59a54d1 100644
--- a/src/Realtime/Trigger/DinosaurCreated.php
+++ b/src/Realtime/Trigger/DinosaurCreated.php
@@ -7,7 +7,7 @@
use App\Entity\Dinosaur;
use App\Realtime\Trigger;
-class DinosaurCreated extends Trigger
+final readonly class DinosaurCreated extends Trigger
{
public function __construct(
Dinosaur $dinosaur,
diff --git a/src/Realtime/Trigger/DinosaurDeleted.php b/src/Realtime/Trigger/DinosaurDeleted.php
index 90f8e0a..21b0e99 100644
--- a/src/Realtime/Trigger/DinosaurDeleted.php
+++ b/src/Realtime/Trigger/DinosaurDeleted.php
@@ -7,7 +7,7 @@
use App\Entity\Dinosaur;
use App\Realtime\Trigger;
-class DinosaurDeleted extends Trigger
+final readonly class DinosaurDeleted extends Trigger
{
public function __construct($id, Dinosaur $dinosaur)
{
diff --git a/src/Realtime/Trigger/DinosaurUpdated.php b/src/Realtime/Trigger/DinosaurUpdated.php
index 433be32..33b5e8a 100644
--- a/src/Realtime/Trigger/DinosaurUpdated.php
+++ b/src/Realtime/Trigger/DinosaurUpdated.php
@@ -7,7 +7,7 @@
use App\Entity\Dinosaur;
use App\Realtime\Trigger;
-class DinosaurUpdated extends Trigger
+final readonly class DinosaurUpdated extends Trigger
{
public function __construct(
Dinosaur $dinosaur,
diff --git a/src/Realtime/Trigger/UserLoggedIn.php b/src/Realtime/Trigger/UserLoggedIn.php
new file mode 100644
index 0000000..2ce06bc
--- /dev/null
+++ b/src/Realtime/Trigger/UserLoggedIn.php
@@ -0,0 +1,22 @@
+ $userIdentifier,
+ 'message' => "The user {$userIdentifier} has been logged in !"
+ ]
+ );
+ }
+}
diff --git a/src/Realtime/Trigger/UserLoggetOut.php b/src/Realtime/Trigger/UserLoggetOut.php
new file mode 100644
index 0000000..f7b5950
--- /dev/null
+++ b/src/Realtime/Trigger/UserLoggetOut.php
@@ -0,0 +1,22 @@
+ $userIdentifier,
+ 'message' => "The user {$userIdentifier} has been logged out !"
+ ]
+ );
+ }
+}
diff --git a/src/Service/Realtime/Publisher.php b/src/Service/Realtime/Publisher.php
index 2cc27ef..97756af 100644
--- a/src/Service/Realtime/Publisher.php
+++ b/src/Service/Realtime/Publisher.php
@@ -20,7 +20,8 @@ public function publish(Trigger $trigger): void
$this->hub->publish(new Update(
$trigger->topics,
json_encode($trigger),
- type: $trigger->type
+ type: $trigger->type,
+ private: $trigger->isPrivate
));
}
}
diff --git a/templates/activity.html.twig b/templates/activity.html.twig
new file mode 100644
index 0000000..af75758
--- /dev/null
+++ b/templates/activity.html.twig
@@ -0,0 +1,18 @@
+{% extends 'base.html.twig' %}
+
+{% block title %}Activity panel{% endblock %}
+
+{% block javascripts %}
+
+
+{% endblock %}
+
+{% block body %}
+
+ Activity Panel
+
+
+ {# Here will go the realtime activity messages #}
+
+
+{% endblock %}
diff --git a/templates/base.html.twig b/templates/base.html.twig
index 1fabcb1..a8d5bbc 100644
--- a/templates/base.html.twig
+++ b/templates/base.html.twig
@@ -36,8 +36,8 @@
{% if 'ROLE_ADMIN' in app.user.roles %}
-
Create a dinosaur
@@ -45,7 +45,7 @@
{% endif %}
-
{% if 'ROLE_ADMIN' in app.user.roles %}
-
Create a species
@@ -64,8 +64,16 @@
{% endif %}
-
+ Activity
+
+
+
+
Logout
@@ -73,7 +81,7 @@
{% else %}
-
-
Login
From 3c444efa65ed90021a30f4a8efbbbba3a674f774 Mon Sep 17 00:00:00 2001
From: AntoineGonzalez <45924026+AntoineGonzalez@users.noreply.github.com>
Date: Tue, 7 May 2024 15:12:16 +0200
Subject: [PATCH 6/6] Practice 5: Authorization with HTTP header
---
public/js/activity.js | 15 ++++++++++-----
public/js/dinosaurs.js | 13 +++++++++----
public/js/eventsource.min.js | 6 ++++++
src/Listener/MercureAuthorizationListener.php | 17 +++++++++++++----
templates/activity.html.twig | 1 +
templates/dinosaurs-list.html.twig | 1 +
6 files changed, 40 insertions(+), 13 deletions(-)
create mode 100644 public/js/eventsource.min.js
diff --git a/public/js/activity.js b/public/js/activity.js
index ea22d89..d973042 100644
--- a/public/js/activity.js
+++ b/public/js/activity.js
@@ -8,7 +8,10 @@ const dicoverMercureHub = async () => fetch(window.location)
.get('Link')
.match(/<([^>]+)>;\s+rel=(?:mercure|"[^"]*mercure[^"]*")/)[1];
- return new URL(hubUrl);
+ return {
+ url: new URL(hubUrl),
+ token: response.headers.get('X-Mercure-JWT'),
+ }
}
)
@@ -24,12 +27,14 @@ const addLogItem = (message) => {
}
document.addEventListener('DOMContentLoaded', async () => {
- const hubUrl = await dicoverMercureHub();
+ const { url, token } = await dicoverMercureHub()
- hubUrl.searchParams.append('topic', 'http://localhost/activity')
- hubUrl.searchParams.append('topic', 'http://localhost/dinosaurs')
+ url.searchParams.append('topic', 'http://localhost/activity')
+ url.searchParams.append('topic', 'http://localhost/dinosaurs')
- const es = new EventSource(hubUrl, { withCredentials: true });
+ const options = token ? { headers: { Authorization: `Bearer ${token}` }, withCredentials: true } : {}
+
+ const es = new EventSourcePolyfill(url, options)
es.addEventListener('login', e => {
const message = JSON.parse(e.data)
diff --git a/public/js/dinosaurs.js b/public/js/dinosaurs.js
index 007ffe3..c1929d1 100644
--- a/public/js/dinosaurs.js
+++ b/public/js/dinosaurs.js
@@ -9,7 +9,10 @@ const dicoverMercureHub = async () => fetch(window.location)
.get('Link')
.match(/<([^>]+)>;\s+rel=(?:mercure|"[^"]*mercure[^"]*")/)[1];
- return new URL(hubUrl);
+ return {
+ url: new URL(hubUrl),
+ token: response.headers.get('X-Mercure-JWT'),
+ }
}
)
@@ -57,11 +60,13 @@ const removeDinosaur = (id, message) => {
}
document.addEventListener('DOMContentLoaded', async () => {
- const hubUrl = await dicoverMercureHub();
+ const { url, token } = await dicoverMercureHub();
- hubUrl.searchParams.append('topic', 'http://localhost/dinosaurs')
+ url.searchParams.append('topic', 'http://localhost/dinosaurs')
- const es = new EventSource(hubUrl, { withCredentials: true });
+ const options = token ? { headers: { Authorization: `Bearer ${token}` }, withCredentials: true } : {}
+
+ const es = new EventSourcePolyfill(url, options);
es.addEventListener('created', e => {
const message = JSON.parse(e.data)
diff --git a/public/js/eventsource.min.js b/public/js/eventsource.min.js
new file mode 100644
index 0000000..689c7cd
--- /dev/null
+++ b/public/js/eventsource.min.js
@@ -0,0 +1,6 @@
+/** @license
+ * eventsource.js
+ * Available under MIT License (MIT)
+ * https://github.com/Yaffle/EventSource/
+ */
+!function(e){"use strict";var r,H=e.setTimeout,N=e.clearTimeout,j=e.XMLHttpRequest,o=e.XDomainRequest,t=e.ActiveXObject,n=e.EventSource,i=e.document,w=e.Promise,d=e.fetch,a=e.Response,h=e.TextDecoder,s=e.TextEncoder,p=e.AbortController;function c(){this.bitsNeeded=0,this.codePoint=0}"undefined"==typeof window||void 0===i||"readyState"in i||null!=i.body||(i.readyState="loading",window.addEventListener("load",function(e){i.readyState="complete"},!1)),null==j&&null!=t&&(j=function(){return new t("Microsoft.XMLHTTP")}),null==Object.create&&(Object.create=function(e){function t(){}return t.prototype=e,new t}),Date.now||(Date.now=function(){return(new Date).getTime()}),null==p&&(r=d,d=function(e,t){var n=t.signal;return r(e,{headers:t.headers,credentials:t.credentials,cache:t.cache}).then(function(e){var t=e.body.getReader();return n._reader=t,n._aborted&&n._reader.cancel(),{status:e.status,statusText:e.statusText,headers:e.headers,body:{getReader:function(){return t}}}})},p=function(){this.signal={_reader:null,_aborted:!1},this.abort=function(){null!=this.signal._reader&&this.signal._reader.cancel(),this.signal._aborted=!0}}),c.prototype.decode=function(e){function t(e,t,n){if(1===n)return 128>>t<=e&&e<>t<=e&&e<>t<=e&&e<>t<=e&&e<>6?3:31>10)))+String.fromCharCode(56320+(i-65535-1&1023)))}return this.bitsNeeded=o,this.codePoint=i,r};function u(){}null!=h&&null!=s&&function(){try{return"test"===(new h).decode((new s).encode("test"),{stream:!0})}catch(e){}return!1}()||(h=c);function I(e){this.withCredentials=!1,this.readyState=0,this.status=0,this.statusText="",this.responseText="",this.onprogress=u,this.onload=u,this.onerror=u,this.onreadystatechange=u,this._contentType="",this._xhr=e,this._sendTimeout=0,this._abort=u}function l(e){return e.replace(/[A-Z]/g,function(e){return String.fromCharCode(e.charCodeAt(0)+32)})}function f(e){for(var t=Object.create(null),n=e.split("\r\n"),r=0;rgetRequest();
+ $response = $event->getResponse();
- $this->authorization->setCookie($request, $topics);
+ // Generate the JWT token
+ $JWTfactory = $this->hubInterface->getFactory();
+
+ if (null === $JWTfactory) {
+ throw new \RuntimeException('The hub factory is not available.');
+ }
+
+ $token = $JWTfactory->create($topics);
+
+ $response->headers->set('X-Mercure-JWT', $token);
}
}
diff --git a/templates/activity.html.twig b/templates/activity.html.twig
index af75758..49f59cc 100644
--- a/templates/activity.html.twig
+++ b/templates/activity.html.twig
@@ -3,6 +3,7 @@
{% block title %}Activity panel{% endblock %}
{% block javascripts %}
+
{% endblock %}
diff --git a/templates/dinosaurs-list.html.twig b/templates/dinosaurs-list.html.twig
index 3da7009..de8812f 100644
--- a/templates/dinosaurs-list.html.twig
+++ b/templates/dinosaurs-list.html.twig
@@ -8,6 +8,7 @@
{% endblock %}
{% block javascripts %}
+
{% endblock %}