From ef9fd80ad59144327fc76ed68d4aa80760b6ad0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Wed, 13 Apr 2022 18:02:11 +0200 Subject: [PATCH 1/9] update schema to send to emails --- .../ChillMainBundle/Entity/Notification.php | 13 +++++++ .../migrations/Version20220413154743.php | 39 +++++++++++++++++++ 2 files changed, 52 insertions(+) create mode 100644 src/Bundle/ChillMainBundle/migrations/Version20220413154743.php diff --git a/src/Bundle/ChillMainBundle/Entity/Notification.php b/src/Bundle/ChillMainBundle/Entity/Notification.php index b2fe40c60..5257a7a90 100644 --- a/src/Bundle/ChillMainBundle/Entity/Notification.php +++ b/src/Bundle/ChillMainBundle/Entity/Notification.php @@ -105,12 +105,25 @@ class Notification implements TrackUpdateInterface */ private ?User $updatedBy; + /** + * a list of destinee which will receive notifications + * @var array|string[] + * @ORM\Column(type="json") + */ + public array $adressesEmails = []; + + /** + * @ORM\Column(type="text", nullable=false) + */ + private string $accessKey; + public function __construct() { $this->addressees = new ArrayCollection(); $this->unreadBy = new ArrayCollection(); $this->comments = new ArrayCollection(); $this->setDate(new DateTimeImmutable()); + $this->accessKey = bin2hex(openssl_random_pseudo_bytes(24)); } public function addAddressee(User $addressee): self diff --git a/src/Bundle/ChillMainBundle/migrations/Version20220413154743.php b/src/Bundle/ChillMainBundle/migrations/Version20220413154743.php new file mode 100644 index 000000000..88a31c92c --- /dev/null +++ b/src/Bundle/ChillMainBundle/migrations/Version20220413154743.php @@ -0,0 +1,39 @@ +addSql('ALTER TABLE chill_main_notification ADD adressesEmails JSON NOT NULL DEFAULT \'[]\';'); + $this->addSql('ALTER TABLE chill_main_notification ADD accessKey TEXT DEFAULT NULL'); + $this->addSql('WITH randoms AS (select + n.id, + string_agg(substr(characters, (random() * length(characters) + 0.5)::integer, 1), \'\') as random_word + from (values(\'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789\')) as symbols(characters) + -- length of word + join generate_series(1, 16) on 1 = 1 + JOIN chill_main_notification n ON true + GROUP BY n.id) + UPDATE chill_main_notification SET accessKey = randoms.random_word FROM randoms WHERE chill_main_notification.id = randoms.id'); + $this->addSql('ALTER TABLE chill_main_notification ALTER accessKey DROP DEFAULT'); + $this->addSql('ALTER TABLE chill_main_notification ALTER accessKey SET NOT NULL'); + } + + public function down(Schema $schema): void + { + $this->addSql('ALTER TABLE chill_main_notification DROP adressesEmails'); + $this->addSql('ALTER TABLE chill_main_notification DROP accessKey'); + } +} From a8db07a3836d92bcec9bd46292d47437f59abe4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Wed, 13 Apr 2022 21:33:18 +0200 Subject: [PATCH 2/9] notification / add email: fix entity Notification --- .../ChillMainBundle/Entity/Notification.php | 49 ++++++++++++++++++- .../Tests/Entity/NotificationTest.php | 18 +++++++ .../migrations/Version20220413154743.php | 2 +- 3 files changed, 67 insertions(+), 2 deletions(-) diff --git a/src/Bundle/ChillMainBundle/Entity/Notification.php b/src/Bundle/ChillMainBundle/Entity/Notification.php index 5257a7a90..9b2dded40 100644 --- a/src/Bundle/ChillMainBundle/Entity/Notification.php +++ b/src/Bundle/ChillMainBundle/Entity/Notification.php @@ -110,7 +110,14 @@ class Notification implements TrackUpdateInterface * @var array|string[] * @ORM\Column(type="json") */ - public array $adressesEmails = []; + private array $addressesEmails = []; + + /** + * a list of emails adresses which were added to the notification + * + * @var array|string[] + */ + private array $addressesEmailsAdded = []; /** * @ORM\Column(type="text", nullable=false) @@ -126,6 +133,46 @@ class Notification implements TrackUpdateInterface $this->accessKey = bin2hex(openssl_random_pseudo_bytes(24)); } + /** + * @return array|string[] + */ + public function getAddressesEmails(): array + { + return $this->addressesEmails; + } + + /** + * @return array|string[] + */ + public function getAddressesEmailsAdded(): array + { + return $this->addressesEmailsAdded; + } + + /** + * @return string + */ + public function getAccessKey(): string + { + return $this->accessKey; + } + + public function addAddressesEmail(string $email) + { + if (!in_array($email, $this->addressesEmails)) { + $this->addressesEmails[] = $email; + $this->addressesEmailsAdded[] = $email; + } + } + + public function removeAddressesEmail(string $email) + { + if (in_array($email, $this->addressesEmails)) { + $this->addressesEmails = array_filter($this->addressesEmails, fn ($e) => $e !== $email); + $this->addressesEmailsAdded = array_filter($this->addressesEmailsAdded, fn ($e) => $e !== $email); + } + } + public function addAddressee(User $addressee): self { if (!$this->addressees->contains($addressee)) { diff --git a/src/Bundle/ChillMainBundle/Tests/Entity/NotificationTest.php b/src/Bundle/ChillMainBundle/Tests/Entity/NotificationTest.php index 8110fa11d..9fc0d6cb2 100644 --- a/src/Bundle/ChillMainBundle/Tests/Entity/NotificationTest.php +++ b/src/Bundle/ChillMainBundle/Tests/Entity/NotificationTest.php @@ -122,4 +122,22 @@ final class NotificationTest extends KernelTestCase $this->assertContains($addresseeId, $unreadIds); } } + + public function testAddressesEmail(): void + { + $notification = new Notification(); + + $notification->addAddressesEmail('test'); + $notification->addAddressesEmail('other'); + + $this->assertContains('test', $notification->getAddressesEmails()); + $this->assertContains('other', $notification->getAddressesEmails()); + $this->assertContains('test', $notification->getAddressesEmailsAdded()); + $this->assertContains('other', $notification->getAddressesEmailsAdded()); + + $notification->removeAddressesEmail('other'); + + $this->assertNotContains('other', $notification->getAddressesEmails()); + $this->assertNotContains('other', $notification->getAddressesEmailsAdded()); + } } diff --git a/src/Bundle/ChillMainBundle/migrations/Version20220413154743.php b/src/Bundle/ChillMainBundle/migrations/Version20220413154743.php index 88a31c92c..87da6a7f8 100644 --- a/src/Bundle/ChillMainBundle/migrations/Version20220413154743.php +++ b/src/Bundle/ChillMainBundle/migrations/Version20220413154743.php @@ -16,7 +16,7 @@ final class Version20220413154743 extends AbstractMigration public function up(Schema $schema): void { - $this->addSql('ALTER TABLE chill_main_notification ADD adressesEmails JSON NOT NULL DEFAULT \'[]\';'); + $this->addSql('ALTER TABLE chill_main_notification ADD addressesEmails JSON NOT NULL DEFAULT \'[]\';'); $this->addSql('ALTER TABLE chill_main_notification ADD accessKey TEXT DEFAULT NULL'); $this->addSql('WITH randoms AS (select n.id, From 4425f2ad4998bf97d174f2f86a8d4a397f623b24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Wed, 13 Apr 2022 21:43:41 +0200 Subject: [PATCH 3/9] fix type for Notification email addresses --- src/Bundle/ChillMainBundle/Entity/Notification.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Bundle/ChillMainBundle/Entity/Notification.php b/src/Bundle/ChillMainBundle/Entity/Notification.php index 9b2dded40..62994497e 100644 --- a/src/Bundle/ChillMainBundle/Entity/Notification.php +++ b/src/Bundle/ChillMainBundle/Entity/Notification.php @@ -159,7 +159,7 @@ class Notification implements TrackUpdateInterface public function addAddressesEmail(string $email) { - if (!in_array($email, $this->addressesEmails)) { + if (!in_array($email, $this->addressesEmails, true)) { $this->addressesEmails[] = $email; $this->addressesEmailsAdded[] = $email; } @@ -167,7 +167,7 @@ class Notification implements TrackUpdateInterface public function removeAddressesEmail(string $email) { - if (in_array($email, $this->addressesEmails)) { + if (in_array($email, $this->addressesEmails, true)) { $this->addressesEmails = array_filter($this->addressesEmails, fn ($e) => $e !== $email); $this->addressesEmailsAdded = array_filter($this->addressesEmailsAdded, fn ($e) => $e !== $email); } From 24d28b0a5252201fc7ab6ed4dccdf07ab0653816 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Wed, 13 Apr 2022 22:11:01 +0200 Subject: [PATCH 4/9] notification: alter form type to add and remove email addresses --- .../ChillMainBundle/Entity/Notification.php | 17 +++++++++++++- .../ChillMainBundle/Form/NotificationType.php | 23 +++++++++++++++++++ .../views/Notification/create.html.twig | 4 +++- .../views/Notification/edit.html.twig | 2 ++ .../translations/messages.fr.yml | 5 ++++ 5 files changed, 49 insertions(+), 2 deletions(-) diff --git a/src/Bundle/ChillMainBundle/Entity/Notification.php b/src/Bundle/ChillMainBundle/Entity/Notification.php index 62994497e..9dd3694b1 100644 --- a/src/Bundle/ChillMainBundle/Entity/Notification.php +++ b/src/Bundle/ChillMainBundle/Entity/Notification.php @@ -18,6 +18,7 @@ use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; use Symfony\Component\Validator\Constraints as Assert; +use Symfony\Component\Validator\Context\ExecutionContextInterface; /** * @ORM\Entity @@ -36,7 +37,6 @@ class Notification implements TrackUpdateInterface /** * @ORM\ManyToMany(targetEntity=User::class) * @ORM\JoinTable(name="chill_main_notification_addresses_user") - * @Assert\Count(min="1", minMessage="notification.At least one addressee") */ private Collection $addressees; @@ -400,4 +400,19 @@ class Notification implements TrackUpdateInterface return $this; } + + /** + * @Assert\Callback() + * @param ExecutionContextInterface $context + * @param array $payload + * @return void + */ + public function assertCountAddresses(ExecutionContextInterface $context, $payload): void + { + if (0 === (count($this->getAddressesEmails()) + count($this->getAddressees()))) { + $context->buildViolation('notification.At least one addressee') + ->atPath('addressees') + ->addViolation(); + } + } } diff --git a/src/Bundle/ChillMainBundle/Form/NotificationType.php b/src/Bundle/ChillMainBundle/Form/NotificationType.php index b24513524..d0c8bc2cd 100644 --- a/src/Bundle/ChillMainBundle/Form/NotificationType.php +++ b/src/Bundle/ChillMainBundle/Form/NotificationType.php @@ -12,12 +12,17 @@ declare(strict_types=1); namespace Chill\MainBundle\Form; use Chill\MainBundle\Entity\Notification; +use Chill\MainBundle\Form\Type\ChillCollectionType; use Chill\MainBundle\Form\Type\ChillTextareaType; use Chill\MainBundle\Form\Type\PickUserDynamicType; use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\Extension\Core\Type\EmailType; use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; +use Symfony\Component\Validator\Constraints\Email; +use Symfony\Component\Validator\Constraints\NotBlank; +use Symfony\Component\Validator\Constraints\NotNull; class NotificationType extends AbstractType { @@ -30,9 +35,27 @@ class NotificationType extends AbstractType ]) ->add('addressees', PickUserDynamicType::class, [ 'multiple' => true, + 'required' => false, ]) ->add('message', ChillTextareaType::class, [ 'required' => false, + ]) + ->add('addressesEmails', ChillCollectionType::class, [ + 'label' => 'notification.dest by email', + 'help' => 'notification.dest by email help', + 'by_reference' => false, + 'allow_add' => true, + 'allow_delete' => true, + 'entry_type' => EmailType::class, + 'button_add_label' => 'notification.Add an email', + 'button_remove_label' => 'notification.Remove an email', + 'empty_collection_explain' => 'notification.Any email', + 'entry_options' => [ + 'constraints' => [ + new NotNull(), new NotBlank(), new Email(['checkMX' => true]), + ], + 'label' => 'Email', + ], ]); } diff --git a/src/Bundle/ChillMainBundle/Resources/views/Notification/create.html.twig b/src/Bundle/ChillMainBundle/Resources/views/Notification/create.html.twig index f15380083..4dfd340b6 100644 --- a/src/Bundle/ChillMainBundle/Resources/views/Notification/create.html.twig +++ b/src/Bundle/ChillMainBundle/Resources/views/Notification/create.html.twig @@ -20,7 +20,9 @@ {{ form_row(form.title, { 'label': 'notification.subject'|trans }) }} {{ form_row(form.addressees, { 'label': 'notification.sent_to'|trans }) }} - + + {{ form_row(form.addressesEmails) }} + {% include handler.template(notification) with handler.templateData(notification) %}
diff --git a/src/Bundle/ChillMainBundle/Resources/views/Notification/edit.html.twig b/src/Bundle/ChillMainBundle/Resources/views/Notification/edit.html.twig index b51cc4dab..fd3b3b6bf 100644 --- a/src/Bundle/ChillMainBundle/Resources/views/Notification/edit.html.twig +++ b/src/Bundle/ChillMainBundle/Resources/views/Notification/edit.html.twig @@ -21,6 +21,8 @@ {{ form_row(form.title, { 'label': 'notification.subject'|trans }) }} {{ form_row(form.addressees, { 'label': 'notification.sent_to'|trans }) }} + {{ form_row(form.addressesEmails) }} + {% include handler.template(notification) with handler.templateData(notification) %}
diff --git a/src/Bundle/ChillMainBundle/translations/messages.fr.yml b/src/Bundle/ChillMainBundle/translations/messages.fr.yml index 685a0c373..6e9f0374a 100644 --- a/src/Bundle/ChillMainBundle/translations/messages.fr.yml +++ b/src/Bundle/ChillMainBundle/translations/messages.fr.yml @@ -450,4 +450,9 @@ notification: subject: Objet see_comments_thread: Voir le fil de commentaires associé object_prefix: "[CHILL] notification - " + dest by email: Lien d'accès par email + Any email: Aucun email + Add an email: Ajouter un email + dest by email help: Les adresses email mentionnées ici recevront un lien d'accès. Un compte utilisateur sera toujours nécessaire. + Remove an email: Supprimer l'adresse email From a41d6cf7440870c63b4dd626e3181c5a683ef8d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Wed, 13 Apr 2022 22:50:32 +0200 Subject: [PATCH 5/9] notification: send an email to addressesEmails --- .../Controller/NotificationController.php | 11 ++++++ .../ChillMainBundle/Entity/Notification.php | 8 ++++ .../Notification/Email/NotificationMailer.php | 38 +++++++++++++++++++ ...m_notification_content_to_email.fr.md.twig | 20 ++++++++++ .../config/services/notification.yaml | 9 +++++ 5 files changed, 86 insertions(+) create mode 100644 src/Bundle/ChillMainBundle/Resources/views/Notification/email_non_system_notification_content_to_email.fr.md.twig diff --git a/src/Bundle/ChillMainBundle/Controller/NotificationController.php b/src/Bundle/ChillMainBundle/Controller/NotificationController.php index af9e1b2b1..0e02c0a82 100644 --- a/src/Bundle/ChillMainBundle/Controller/NotificationController.php +++ b/src/Bundle/ChillMainBundle/Controller/NotificationController.php @@ -65,6 +65,17 @@ class NotificationController extends AbstractController $this->translator = $translator; } + /** + * @Route("/{id}/access_key", name="chill_main_notification_grant_access_by_access_key") + */ + public function getAccessByAccessKey(Notification $notification, Request $request): Response + { + $this->denyAccessUnlessGranted('IS_AUTHENTICATED_REMEMBERED'); + + return new Response('Invalid access key'); + + } + /** * @Route("/create", name="chill_main_notification_create") */ diff --git a/src/Bundle/ChillMainBundle/Entity/Notification.php b/src/Bundle/ChillMainBundle/Entity/Notification.php index 9dd3694b1..d3d4dd0eb 100644 --- a/src/Bundle/ChillMainBundle/Entity/Notification.php +++ b/src/Bundle/ChillMainBundle/Entity/Notification.php @@ -183,6 +183,14 @@ class Notification implements TrackUpdateInterface return $this; } + /** + * @return array + */ + public function getAddedAddresses(): array + { + return $this->addedAddresses; + } + public function addComment(NotificationComment $comment): self { if (!$this->comments->contains($comment)) { diff --git a/src/Bundle/ChillMainBundle/Notification/Email/NotificationMailer.php b/src/Bundle/ChillMainBundle/Notification/Email/NotificationMailer.php index ce5587ed9..69fcf66f0 100644 --- a/src/Bundle/ChillMainBundle/Notification/Email/NotificationMailer.php +++ b/src/Bundle/ChillMainBundle/Notification/Email/NotificationMailer.php @@ -73,6 +73,17 @@ class NotificationMailer * Send a email after a notification is persisted. */ public function postPersistNotification(Notification $notification, LifecycleEventArgs $eventArgs): void + { + $this->sendNotificationEmailsToAddresses($notification); + $this->sendNotificationEmailsToAddressesEmails($notification); + } + + public function postUpdateNotification(Notification $notification, LifecycleEventArgs $eventArgs): void + { + $this->sendNotificationEmailsToAddressesEmails($notification); + } + + private function sendNotificationEmailsToAddresses(Notification $notification): void { foreach ($notification->getAddressees() as $addressee) { if (null === $addressee->getEmail()) { @@ -108,4 +119,31 @@ class NotificationMailer } } } + + private function sendNotificationEmailsToAddressesEmails(Notification $notification): void + { + foreach ($notification->getAddressesEmailsAdded() as $emailAddress) { + $email = new TemplatedEmail(); + $email + ->textTemplate('@ChillMain/Notification/email_non_system_notification_content_to_email.fr.md.twig') + ->context([ + 'notification' => $notification, + 'dest' => $emailAddress, + ]); + + $email + ->subject($notification->getTitle()) + ->to($emailAddress); + + try { + $this->mailer->send($email); + } catch (TransportExceptionInterface $e) { + $this->logger->warning('[NotificationMailer] could not send an email notification', [ + 'to' => $emailAddress, + 'error_message' => $e->getMessage(), + 'error_trace' => $e->getTraceAsString(), + ]); + } + } + } } diff --git a/src/Bundle/ChillMainBundle/Resources/views/Notification/email_non_system_notification_content_to_email.fr.md.twig b/src/Bundle/ChillMainBundle/Resources/views/Notification/email_non_system_notification_content_to_email.fr.md.twig new file mode 100644 index 000000000..9a32f0c15 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Resources/views/Notification/email_non_system_notification_content_to_email.fr.md.twig @@ -0,0 +1,20 @@ +{{ dest }}, + +{{ notification.sender.label }} a créé une notification pour vous: + +> {{ notification.title }} +> +> +{%- for line in notification.message|split("\n") %} +> {{ line }} +{%- if not loop.last %} +> +{%- endif %} +{%- endfor %} + +Vous pouvez cliquer sur ce lien pour obtenir un accès permanent à la notification: + +{{ absolute_url(path('chill_main_notification_grant_access_by_access_key', {'_locale': 'fr', 'id': notification.id, 'accessKey': notification.accessKey, 'email': dest})) }} + +-- +Le logiciel Chill diff --git a/src/Bundle/ChillMainBundle/config/services/notification.yaml b/src/Bundle/ChillMainBundle/config/services/notification.yaml index b972dced7..544f5544c 100644 --- a/src/Bundle/ChillMainBundle/config/services/notification.yaml +++ b/src/Bundle/ChillMainBundle/config/services/notification.yaml @@ -61,6 +61,15 @@ services: # set the 'lazy' option to TRUE to only instantiate listeners when they are used lazy: true method: 'postPersistNotification' + + - + name: 'doctrine.orm.entity_listener' + event: 'postUpdate' + entity: 'Chill\MainBundle\Entity\Notification' + # set the 'lazy' option to TRUE to only instantiate listeners when they are used + lazy: true + method: 'postUpdateNotification' + - name: 'doctrine.orm.entity_listener' event: 'postPersist' From e7f0cd50c9f712a748ebef7b0567cb421dd42368 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Wed, 13 Apr 2022 23:05:38 +0200 Subject: [PATCH 6/9] controller to grant access to notification by access key --- .../Controller/NotificationController.php | 43 ++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/src/Bundle/ChillMainBundle/Controller/NotificationController.php b/src/Bundle/ChillMainBundle/Controller/NotificationController.php index 0e02c0a82..5c1508321 100644 --- a/src/Bundle/ChillMainBundle/Controller/NotificationController.php +++ b/src/Bundle/ChillMainBundle/Controller/NotificationController.php @@ -22,6 +22,7 @@ use Chill\MainBundle\Pagination\PaginatorFactory; use Chill\MainBundle\Repository\NotificationRepository; use Chill\MainBundle\Security\Authorization\NotificationVoter; use Doctrine\ORM\EntityManagerInterface; +use Psr\Log\LoggerInterface; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; @@ -39,6 +40,10 @@ class NotificationController extends AbstractController { private EntityManagerInterface $em; + private LoggerInterface $chillLogger; + + private LoggerInterface $logger; + private NotificationHandlerManager $notificationHandlerManager; private NotificationRepository $notificationRepository; @@ -51,6 +56,8 @@ class NotificationController extends AbstractController public function __construct( EntityManagerInterface $em, + LoggerInterface $chillLogger, + LoggerInterface $logger, Security $security, NotificationRepository $notificationRepository, NotificationHandlerManager $notificationHandlerManager, @@ -58,6 +65,8 @@ class NotificationController extends AbstractController TranslatorInterface $translator ) { $this->em = $em; + $this->logger = $logger; + $this->chillLogger = $chillLogger; $this->security = $security; $this->notificationRepository = $notificationRepository; $this->notificationHandlerManager = $notificationHandlerManager; @@ -72,8 +81,40 @@ class NotificationController extends AbstractController { $this->denyAccessUnlessGranted('IS_AUTHENTICATED_REMEMBERED'); - return new Response('Invalid access key'); + if (!$this->security->getUser() instanceof User) { + throw new AccessDeniedHttpException('You must be authenticated and a user to create a notification'); + } + foreach (['accessKey', 'email'] as $param) { + if (!$request->query->has($param)) { + throw new BadRequestHttpException("Missing $param parameter"); + } + } + + if ($notification->getAccessKey() !== $request->query->getAlnum('accessKey')) { + throw new AccessDeniedHttpException('access key is invalid'); + } + + if (!in_array($request->query->get('email'), $notification->getAddressesEmails())) { + return (new Response('The email address is no more associated with this notification')) + ->setStatusCode(Response::HTTP_FORBIDDEN); + } + + $notification->addAddressee($this->security->getUser()); + + $this->getDoctrine()->getManager()->flush(); + + $logMsg = '[Notification] a user is granted access to notification trough an access key'; + $context = [ + 'notificationId' => $notification->getId(), + 'email' => $request->query->get('email'), + 'user' => $this->security->getUser()->getId(), + ]; + + $this->logger->info($logMsg, $context); + $this->chillLogger->info($logMsg, $context); + + return $this->redirectToRoute('chill_main_notification_show', ['id' => $notification->getId()]); } /** From 2a53fb9341cea1a873aa3c2ee9b016c0f208c6f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Wed, 13 Apr 2022 23:16:02 +0200 Subject: [PATCH 7/9] show email adresses on notification list --- .../Resources/views/Notification/_list_item.html.twig | 5 +++++ src/Bundle/ChillMainBundle/translations/messages.fr.yml | 1 + 2 files changed, 6 insertions(+) diff --git a/src/Bundle/ChillMainBundle/Resources/views/Notification/_list_item.html.twig b/src/Bundle/ChillMainBundle/Resources/views/Notification/_list_item.html.twig index 02a02b4d0..1024d56b3 100644 --- a/src/Bundle/ChillMainBundle/Resources/views/Notification/_list_item.html.twig +++ b/src/Bundle/ChillMainBundle/Resources/views/Notification/_list_item.html.twig @@ -40,6 +40,11 @@ {{ a|chill_entity_render_string }} {% endfor %} + {% for a in c.notification.addressesEmails %} + + {{ a }} + + {% endfor %} {% endif %} diff --git a/src/Bundle/ChillMainBundle/translations/messages.fr.yml b/src/Bundle/ChillMainBundle/translations/messages.fr.yml index 6e9f0374a..452ad306c 100644 --- a/src/Bundle/ChillMainBundle/translations/messages.fr.yml +++ b/src/Bundle/ChillMainBundle/translations/messages.fr.yml @@ -455,4 +455,5 @@ notification: Add an email: Ajouter un email dest by email help: Les adresses email mentionnées ici recevront un lien d'accès. Un compte utilisateur sera toujours nécessaire. Remove an email: Supprimer l'adresse email + Email with access link: Adresse email ayant reçu un lien d'accès From 35c7d55b8c9d45640254296af34ff6c817724fd0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Wed, 13 Apr 2022 23:17:16 +0200 Subject: [PATCH 8/9] fix cs --- .../Controller/NotificationController.php | 91 ++++++----- .../ChillMainBundle/Entity/Notification.php | 154 +++++++++--------- .../ChillMainBundle/Form/NotificationType.php | 2 +- .../Tests/Entity/NotificationTest.php | 36 ++-- .../migrations/Version20220413154743.php | 19 ++- 5 files changed, 153 insertions(+), 149 deletions(-) diff --git a/src/Bundle/ChillMainBundle/Controller/NotificationController.php b/src/Bundle/ChillMainBundle/Controller/NotificationController.php index 5c1508321..f40c85ffb 100644 --- a/src/Bundle/ChillMainBundle/Controller/NotificationController.php +++ b/src/Bundle/ChillMainBundle/Controller/NotificationController.php @@ -32,16 +32,17 @@ use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Security\Core\Security; use Symfony\Contracts\Translation\TranslatorInterface; +use function in_array; /** * @Route("/{_locale}/notification") */ class NotificationController extends AbstractController { - private EntityManagerInterface $em; - private LoggerInterface $chillLogger; + private EntityManagerInterface $em; + private LoggerInterface $logger; private NotificationHandlerManager $notificationHandlerManager; @@ -74,49 +75,6 @@ class NotificationController extends AbstractController $this->translator = $translator; } - /** - * @Route("/{id}/access_key", name="chill_main_notification_grant_access_by_access_key") - */ - public function getAccessByAccessKey(Notification $notification, Request $request): Response - { - $this->denyAccessUnlessGranted('IS_AUTHENTICATED_REMEMBERED'); - - if (!$this->security->getUser() instanceof User) { - throw new AccessDeniedHttpException('You must be authenticated and a user to create a notification'); - } - - foreach (['accessKey', 'email'] as $param) { - if (!$request->query->has($param)) { - throw new BadRequestHttpException("Missing $param parameter"); - } - } - - if ($notification->getAccessKey() !== $request->query->getAlnum('accessKey')) { - throw new AccessDeniedHttpException('access key is invalid'); - } - - if (!in_array($request->query->get('email'), $notification->getAddressesEmails())) { - return (new Response('The email address is no more associated with this notification')) - ->setStatusCode(Response::HTTP_FORBIDDEN); - } - - $notification->addAddressee($this->security->getUser()); - - $this->getDoctrine()->getManager()->flush(); - - $logMsg = '[Notification] a user is granted access to notification trough an access key'; - $context = [ - 'notificationId' => $notification->getId(), - 'email' => $request->query->get('email'), - 'user' => $this->security->getUser()->getId(), - ]; - - $this->logger->info($logMsg, $context); - $this->chillLogger->info($logMsg, $context); - - return $this->redirectToRoute('chill_main_notification_show', ['id' => $notification->getId()]); - } - /** * @Route("/create", name="chill_main_notification_create") */ @@ -202,6 +160,49 @@ class NotificationController extends AbstractController ]); } + /** + * @Route("/{id}/access_key", name="chill_main_notification_grant_access_by_access_key") + */ + public function getAccessByAccessKey(Notification $notification, Request $request): Response + { + $this->denyAccessUnlessGranted('IS_AUTHENTICATED_REMEMBERED'); + + if (!$this->security->getUser() instanceof User) { + throw new AccessDeniedHttpException('You must be authenticated and a user to create a notification'); + } + + foreach (['accessKey', 'email'] as $param) { + if (!$request->query->has($param)) { + throw new BadRequestHttpException("Missing {$param} parameter"); + } + } + + if ($notification->getAccessKey() !== $request->query->getAlnum('accessKey')) { + throw new AccessDeniedHttpException('access key is invalid'); + } + + if (!in_array($request->query->get('email'), $notification->getAddressesEmails(), true)) { + return (new Response('The email address is no more associated with this notification')) + ->setStatusCode(Response::HTTP_FORBIDDEN); + } + + $notification->addAddressee($this->security->getUser()); + + $this->getDoctrine()->getManager()->flush(); + + $logMsg = '[Notification] a user is granted access to notification trough an access key'; + $context = [ + 'notificationId' => $notification->getId(), + 'email' => $request->query->get('email'), + 'user' => $this->security->getUser()->getId(), + ]; + + $this->logger->info($logMsg, $context); + $this->chillLogger->info($logMsg, $context); + + return $this->redirectToRoute('chill_main_notification_show', ['id' => $notification->getId()]); + } + /** * @Route("/inbox", name="chill_main_notification_my") */ diff --git a/src/Bundle/ChillMainBundle/Entity/Notification.php b/src/Bundle/ChillMainBundle/Entity/Notification.php index d3d4dd0eb..8dad39d12 100644 --- a/src/Bundle/ChillMainBundle/Entity/Notification.php +++ b/src/Bundle/ChillMainBundle/Entity/Notification.php @@ -19,6 +19,8 @@ use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; use Symfony\Component\Validator\Constraints as Assert; use Symfony\Component\Validator\Context\ExecutionContextInterface; +use function count; +use function in_array; /** * @ORM\Entity @@ -32,6 +34,11 @@ use Symfony\Component\Validator\Context\ExecutionContextInterface; */ class Notification implements TrackUpdateInterface { + /** + * @ORM\Column(type="text", nullable=false) + */ + private string $accessKey; + private array $addedAddresses = []; /** @@ -40,6 +47,21 @@ class Notification implements TrackUpdateInterface */ private Collection $addressees; + /** + * a list of destinee which will receive notifications. + * + * @var array|string[] + * @ORM\Column(type="json") + */ + private array $addressesEmails = []; + + /** + * a list of emails adresses which were added to the notification. + * + * @var array|string[] + */ + private array $addressesEmailsAdded = []; + private ?ArrayCollection $addressesOnLoad = null; /** @@ -105,25 +127,6 @@ class Notification implements TrackUpdateInterface */ private ?User $updatedBy; - /** - * a list of destinee which will receive notifications - * @var array|string[] - * @ORM\Column(type="json") - */ - private array $addressesEmails = []; - - /** - * a list of emails adresses which were added to the notification - * - * @var array|string[] - */ - private array $addressesEmailsAdded = []; - - /** - * @ORM\Column(type="text", nullable=false) - */ - private string $accessKey; - public function __construct() { $this->addressees = new ArrayCollection(); @@ -133,46 +136,6 @@ class Notification implements TrackUpdateInterface $this->accessKey = bin2hex(openssl_random_pseudo_bytes(24)); } - /** - * @return array|string[] - */ - public function getAddressesEmails(): array - { - return $this->addressesEmails; - } - - /** - * @return array|string[] - */ - public function getAddressesEmailsAdded(): array - { - return $this->addressesEmailsAdded; - } - - /** - * @return string - */ - public function getAccessKey(): string - { - return $this->accessKey; - } - - public function addAddressesEmail(string $email) - { - if (!in_array($email, $this->addressesEmails, true)) { - $this->addressesEmails[] = $email; - $this->addressesEmailsAdded[] = $email; - } - } - - public function removeAddressesEmail(string $email) - { - if (in_array($email, $this->addressesEmails, true)) { - $this->addressesEmails = array_filter($this->addressesEmails, fn ($e) => $e !== $email); - $this->addressesEmailsAdded = array_filter($this->addressesEmailsAdded, fn ($e) => $e !== $email); - } - } - public function addAddressee(User $addressee): self { if (!$this->addressees->contains($addressee)) { @@ -183,12 +146,12 @@ class Notification implements TrackUpdateInterface return $this; } - /** - * @return array - */ - public function getAddedAddresses(): array + public function addAddressesEmail(string $email) { - return $this->addedAddresses; + if (!in_array($email, $this->addressesEmails, true)) { + $this->addressesEmails[] = $email; + $this->addressesEmailsAdded[] = $email; + } } public function addComment(NotificationComment $comment): self @@ -210,6 +173,30 @@ class Notification implements TrackUpdateInterface return $this; } + /** + * @Assert\Callback + * + * @param array $payload + */ + public function assertCountAddresses(ExecutionContextInterface $context, $payload): void + { + if (0 === (count($this->getAddressesEmails()) + count($this->getAddressees()))) { + $context->buildViolation('notification.At least one addressee') + ->atPath('addressees') + ->addViolation(); + } + } + + public function getAccessKey(): string + { + return $this->accessKey; + } + + public function getAddedAddresses(): array + { + return $this->addedAddresses; + } + /** * @return Collection|User[] */ @@ -223,6 +210,22 @@ class Notification implements TrackUpdateInterface return $this->addressees; } + /** + * @return array|string[] + */ + public function getAddressesEmails(): array + { + return $this->addressesEmails; + } + + /** + * @return array|string[] + */ + public function getAddressesEmailsAdded(): array + { + return $this->addressesEmailsAdded; + } + public function getComments(): Collection { return $this->comments; @@ -339,6 +342,14 @@ class Notification implements TrackUpdateInterface return $this; } + public function removeAddressesEmail(string $email) + { + if (in_array($email, $this->addressesEmails, true)) { + $this->addressesEmails = array_filter($this->addressesEmails, static fn ($e) => $e !== $email); + $this->addressesEmailsAdded = array_filter($this->addressesEmailsAdded, static fn ($e) => $e !== $email); + } + } + public function removeComment(NotificationComment $comment): self { $this->comments->removeElement($comment); @@ -408,19 +419,4 @@ class Notification implements TrackUpdateInterface return $this; } - - /** - * @Assert\Callback() - * @param ExecutionContextInterface $context - * @param array $payload - * @return void - */ - public function assertCountAddresses(ExecutionContextInterface $context, $payload): void - { - if (0 === (count($this->getAddressesEmails()) + count($this->getAddressees()))) { - $context->buildViolation('notification.At least one addressee') - ->atPath('addressees') - ->addViolation(); - } - } } diff --git a/src/Bundle/ChillMainBundle/Form/NotificationType.php b/src/Bundle/ChillMainBundle/Form/NotificationType.php index d0c8bc2cd..22fd19baf 100644 --- a/src/Bundle/ChillMainBundle/Form/NotificationType.php +++ b/src/Bundle/ChillMainBundle/Form/NotificationType.php @@ -40,7 +40,7 @@ class NotificationType extends AbstractType ->add('message', ChillTextareaType::class, [ 'required' => false, ]) - ->add('addressesEmails', ChillCollectionType::class, [ + ->add('addressesEmails', ChillCollectionType::class, [ 'label' => 'notification.dest by email', 'help' => 'notification.dest by email help', 'by_reference' => false, diff --git a/src/Bundle/ChillMainBundle/Tests/Entity/NotificationTest.php b/src/Bundle/ChillMainBundle/Tests/Entity/NotificationTest.php index 9fc0d6cb2..0575fa52a 100644 --- a/src/Bundle/ChillMainBundle/Tests/Entity/NotificationTest.php +++ b/src/Bundle/ChillMainBundle/Tests/Entity/NotificationTest.php @@ -88,6 +88,24 @@ final class NotificationTest extends KernelTestCase $this->assertNotContains($user1, $notification->getUnreadBy()->toArray()); } + public function testAddressesEmail(): void + { + $notification = new Notification(); + + $notification->addAddressesEmail('test'); + $notification->addAddressesEmail('other'); + + $this->assertContains('test', $notification->getAddressesEmails()); + $this->assertContains('other', $notification->getAddressesEmails()); + $this->assertContains('test', $notification->getAddressesEmailsAdded()); + $this->assertContains('other', $notification->getAddressesEmailsAdded()); + + $notification->removeAddressesEmail('other'); + + $this->assertNotContains('other', $notification->getAddressesEmails()); + $this->assertNotContains('other', $notification->getAddressesEmailsAdded()); + } + /** * @dataProvider generateNotificationData */ @@ -122,22 +140,4 @@ final class NotificationTest extends KernelTestCase $this->assertContains($addresseeId, $unreadIds); } } - - public function testAddressesEmail(): void - { - $notification = new Notification(); - - $notification->addAddressesEmail('test'); - $notification->addAddressesEmail('other'); - - $this->assertContains('test', $notification->getAddressesEmails()); - $this->assertContains('other', $notification->getAddressesEmails()); - $this->assertContains('test', $notification->getAddressesEmailsAdded()); - $this->assertContains('other', $notification->getAddressesEmailsAdded()); - - $notification->removeAddressesEmail('other'); - - $this->assertNotContains('other', $notification->getAddressesEmails()); - $this->assertNotContains('other', $notification->getAddressesEmailsAdded()); - } } diff --git a/src/Bundle/ChillMainBundle/migrations/Version20220413154743.php b/src/Bundle/ChillMainBundle/migrations/Version20220413154743.php index 87da6a7f8..174419cda 100644 --- a/src/Bundle/ChillMainBundle/migrations/Version20220413154743.php +++ b/src/Bundle/ChillMainBundle/migrations/Version20220413154743.php @@ -1,5 +1,12 @@ addSql('ALTER TABLE chill_main_notification DROP adressesEmails'); + $this->addSql('ALTER TABLE chill_main_notification DROP accessKey'); + } + public function getDescription(): string { return 'add access keys and emails dest to notifications'; @@ -30,10 +43,4 @@ final class Version20220413154743 extends AbstractMigration $this->addSql('ALTER TABLE chill_main_notification ALTER accessKey DROP DEFAULT'); $this->addSql('ALTER TABLE chill_main_notification ALTER accessKey SET NOT NULL'); } - - public function down(Schema $schema): void - { - $this->addSql('ALTER TABLE chill_main_notification DROP adressesEmails'); - $this->addSql('ALTER TABLE chill_main_notification DROP accessKey'); - } } From 8770188d5487ec368959512d7f233ee3ddceacff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Wed, 13 Apr 2022 23:18:50 +0200 Subject: [PATCH 9/9] update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a61c63a73..451a672ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ and this project adheres to * [docgen] add more persons choices in docgen for course: amongst requestor (if person), resources of course (if person), and PersonResource (if person); * [docgen] add a new context with a list of activities in course * [docgen] add a comment in budget lines +* [notifications] allow to send a notification to an email address. The address receive an access link ## Test releases