From 568dc76195a49dbb56bf1a0445003bc9eda283af Mon Sep 17 00:00:00 2001 From: Camille Simiand <camille.simiand@tetras-libre.fr> Date: Tue, 8 Mar 2022 17:42:48 +0100 Subject: [PATCH] Add unique email constraint + translations --- src/Controller/UserController.php | 80 ++++++++++++++----- src/Entity/PendingEmailAddress.php | 2 + src/Form/EditUserProfileFormType.php | 29 ++++++- .../PendingEmailAddressRepository.php | 28 +++++++ src/Repository/UserRepository.php | 15 ++++ src/Security/EmailVerifier.php | 4 +- templates/user/update_email.html.twig | 7 +- translations/messages.en.yaml | 9 ++- translations/messages.fr.yaml | 3 +- translations/validators.en.yaml | 10 ++- translations/validators.fr.yaml | 10 ++- 11 files changed, 165 insertions(+), 32 deletions(-) diff --git a/src/Controller/UserController.php b/src/Controller/UserController.php index a3afb46..f10d1eb 100755 --- a/src/Controller/UserController.php +++ b/src/Controller/UserController.php @@ -6,12 +6,15 @@ use App\Entity\PendingEmailAddress; use App\Entity\User; use App\Form\EditPasswordFormType; use App\Form\EditUserProfileFormType; -use App\Security\EmailVerifier; +use App\Repository\PendingEmailAddressRepository; +use App\Repository\UserRepository; use Doctrine\ORM\EntityManagerInterface; use Symfony\Bridge\Twig\Mime\TemplatedEmail; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Mailer\MailerInterface; +use Symfony\Component\Mime\Address; use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface; use Symfony\Component\Routing\Annotation\Route; use Symfony\Contracts\Translation\TranslatorInterface; @@ -20,7 +23,9 @@ class UserController extends AbstractController { public function __construct( private EntityManagerInterface $entity_manager, - private TranslatorInterface $translator + private TranslatorInterface $translator, + private PendingEmailAddressRepository $pending_email_address_repository, + private UserRepository $user_repository ) { } @@ -39,25 +44,31 @@ class UserController extends AbstractController } #[Route('/edit_profile', name:'edit_profile')] - public function editProfile(Request $request, EmailVerifier $email_verifier): Response - { + public function editProfile( + Request $request, + MailerInterface $mailer + ): Response { $current_user = $this->getUser(); if (! $current_user instanceof User) { return $this->redirectToRoute('app_logout'); } + $users_emails = $this->user_repository->getAllEmails(); + $pending_email_addresses = $this->pending_email_address_repository->getAllEmails(); + $form = $this->createForm( EditUserProfileFormType::class, $current_user, - ['current_email_address' => $current_user->getEmail()] + [ + 'current_email_address' => $current_user->getEmail(), + 'users_emails' => $users_emails, + 'pending_emails' => $pending_email_addresses + ] ); $form->handleRequest($request); if ($form->isSubmitted() && $form->isValid()) { - $this->entity_manager->persist($current_user); - $this->entity_manager->flush(); - if ($current_user->getEmail() !== $form->get('email')->getData()) { $pending_email_address = new PendingEmailAddress(); $pending_email_address->setEmail($form->get('email')->getData()); @@ -65,15 +76,14 @@ class UserController extends AbstractController $this->entity_manager->persist($pending_email_address); $this->entity_manager->flush(); - $email_verifier->sendEmailConfirmation( - 'app_verify_email', - $pending_email_address->getUser()->getId(), - $pending_email_address->getEmail(), - (new TemplatedEmail()) - ->to($pending_email_address->getEmail()) - ->subject('Please Confirm your Email') - ->htmlTemplate('user/update_email.html.twig') - ); + $email = (new TemplatedEmail()) + ->to(new Address($pending_email_address->getEmail())) + ->subject('Please Confirm your Email') + ->htmlTemplate('user/update_email.html.twig') + ->context([ + 'expiration_date' => new \DateTime('+1 day') + ]); + $mailer->send($email); $this->addFlash( 'warning', @@ -95,6 +105,9 @@ class UserController extends AbstractController return $this->redirectToRoute('show_profile'); } + $this->entity_manager->persist($current_user); + $this->entity_manager->flush(); + return $this->renderForm('user/edit_profile.html.twig', [ 'editUserProfileForm' => $form ]); @@ -103,7 +116,7 @@ class UserController extends AbstractController #[Route('/edit_password', name:'edit_password')] public function editPassword( Request $request, - UserPasswordHasherInterface $user_password_hasher, + UserPasswordHasherInterface $user_password_hasher ): Response { $form = $this->createForm(EditPasswordFormType::class); $form->handleRequest($request); @@ -132,4 +145,35 @@ class UserController extends AbstractController 'editPasswordForm' => $form ]); } + + #[Route('/edit/email/', name:'verify_new_email_address')] + public function verifyNewEmailAddress(): Response + { + $current_user = $this->getUser(); + + if (! $current_user instanceof User) { + return $this->redirectToRoute('app_logout'); + } + + $pending_email_address = $this->pending_email_address_repository->findOneBy(['user' => $current_user->getId()]); + if (! $pending_email_address instanceof PendingEmailAddress) { + throw new \Exception('Not found'); + } + + $current_user->setEmail($pending_email_address->getEmail()); + $this->entity_manager->persist($current_user); + + $this->entity_manager->remove($pending_email_address); + $this->entity_manager->flush(); + + $this->addFlash( + 'success', + $this->translator->trans( + 'user.edit.email.success', + ['new_email_address' => $pending_email_address->getEmail()] + ) + ); + + return $this->redirectToRoute('show_profile'); + } } diff --git a/src/Entity/PendingEmailAddress.php b/src/Entity/PendingEmailAddress.php index 3f9a9f3..3a29a22 100644 --- a/src/Entity/PendingEmailAddress.php +++ b/src/Entity/PendingEmailAddress.php @@ -4,10 +4,12 @@ namespace App\Entity; use App\Repository\PendingEmailAddressRepository; use Doctrine\ORM\Mapping as ORM; +use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; use Symfony\Component\Validator\Constraints as Assert; #[ORM\Entity(repositoryClass:PendingEmailAddressRepository::class)] #[ORM\Table(name:'`pending_email_address`')] +#[UniqueEntity(fields: ['user', 'email'], message: 'pending_email_address.email.unique', errorPath: 'email')] class PendingEmailAddress { #[ORM\Id] diff --git a/src/Form/EditUserProfileFormType.php b/src/Form/EditUserProfileFormType.php index 0c41c79..0f0a03b 100755 --- a/src/Form/EditUserProfileFormType.php +++ b/src/Form/EditUserProfileFormType.php @@ -13,6 +13,7 @@ use Symfony\Component\OptionsResolver\OptionsResolver; use Symfony\Component\Security\Core\Validator\Constraints\UserPassword; use Symfony\Component\Validator\Constraints\NotBlank; use Symfony\Component\Validator\Constraints as Assert; +use Symfony\Component\Validator\Context\ExecutionContextInterface; class EditUserProfileFormType extends AbstractType { @@ -44,11 +45,29 @@ class EditUserProfileFormType extends AbstractType 'data' => $options['current_email_address'], 'constraints' => [ new NotBlank(['message' => 'email.not_blank']), - new Assert\Email() + new Assert\Email(), + new Assert\Callback( + ['callback' => static function ( + ?string $value, + ExecutionContextInterface $context + ) use ($options) { + if ( + $value !== $options['current_email_address'] && + (in_array($value, $options['users_emails']) || + in_array($value, $options['pending_emails'])) + ) { + $context + ->buildViolation(message: 'user.email.unique') + ->addViolation() + ; + } + } + ] + ) ], 'label' => 'general.email', 'empty_data' => '', - 'mapped' => false, + 'mapped' => false ] ) ->add( @@ -76,7 +95,9 @@ class EditUserProfileFormType extends AbstractType { $resolver->setDefaults([ 'data_class' => User::class, - 'current_email_address' => '' - ]); + 'current_email_address' => '', + 'users_emails' => [], + 'pending_emails' => [] + ]); } } diff --git a/src/Repository/PendingEmailAddressRepository.php b/src/Repository/PendingEmailAddressRepository.php index 78585ce..dbc3379 100644 --- a/src/Repository/PendingEmailAddressRepository.php +++ b/src/Repository/PendingEmailAddressRepository.php @@ -7,6 +7,8 @@ use App\Entity\User; use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; use Doctrine\Persistence\ManagerRegistry; +use function PHPUnit\Framework\assertInstanceOf; + /** * @method PendingEmailAddress|null find($id, $lockMode = null, $lockVersion = null) * @method PendingEmailAddress|null findOneBy(array $criteria, array $orderBy = null) @@ -20,4 +22,30 @@ class PendingEmailAddressRepository extends ServiceEntityRepository { parent::__construct($registry, PendingEmailAddress::class); } + + public function findOneByEmail(string $value): ?User + { + return $this->createQueryBuilder('u') + ->andWhere('u.email = :val') + ->setParameter('val', $value) + ->getQuery() + ->getOneOrNullResult() + ; + } + + /** + * @return string[] + */ + public function getAllEmails(): array + { + $all_pending_email_addresses = $this->findAll(); + $all_emails = []; + + foreach ($all_pending_email_addresses as $pending_email_address) { + assertInstanceOf(PendingEmailAddress::class, $pending_email_address); + $all_emails[] = $pending_email_address->getEmail(); + } + + return $all_emails; + } } diff --git a/src/Repository/UserRepository.php b/src/Repository/UserRepository.php index 147f8c5..476ef81 100755 --- a/src/Repository/UserRepository.php +++ b/src/Repository/UserRepository.php @@ -72,4 +72,19 @@ class UserRepository extends ServiceEntityRepository implements PasswordUpgrader ->getQuery() ->getResult(); } + + /** + * @return string[] + */ + public function getAllEmails(): array + { + $all_users = $this->findAll(); + $all_emails = []; + + foreach ($all_users as $user) { + $all_emails[] = $user->getEmail(); + } + + return $all_emails; + } } diff --git a/src/Security/EmailVerifier.php b/src/Security/EmailVerifier.php index ea4389f..aa910c7 100755 --- a/src/Security/EmailVerifier.php +++ b/src/Security/EmailVerifier.php @@ -27,13 +27,13 @@ class EmailVerifier } public function sendEmailConfirmation( - string $verifyEmailRouteName, + string $verify_email_route_name, int $user_id, string $email_address, TemplatedEmail $email ): void { $signatureComponents = $this->verifyEmailHelper->generateSignature( - $verifyEmailRouteName, + $verify_email_route_name, (string) $user_id, $email_address, ['id' => $user_id] diff --git a/templates/user/update_email.html.twig b/templates/user/update_email.html.twig index e292b58..082e02f 100644 --- a/templates/user/update_email.html.twig +++ b/templates/user/update_email.html.twig @@ -5,11 +5,12 @@ <p> {{ 'user.edit.email.text'|trans }}: <br><br> - <a href="{{ signedUrl }}"> + <a href="{{ path('verify_new_email_address') }}"> {{ 'user.edit.email.confirm_email'|trans }} </a> - {{ 'general.link_expire'|trans({'%expirationDuration%': - expiresAtMessageKey|trans(expiresAtMessageData, 'VerifyEmailBundle')}) }} + {{ 'general.link_expire'|trans + + }} </p> <p> diff --git a/translations/messages.en.yaml b/translations/messages.en.yaml index 215c4aa..d3c7ea7 100644 --- a/translations/messages.en.yaml +++ b/translations/messages.en.yaml @@ -122,8 +122,9 @@ user: edit: email: title: Hi! Please confirm your email - text: Please click the following link to update your user account and for now on use this email address. + text: Please click the following link to update your user account and for now on use this email address confirm_email: Confirm my email + success: Your MemoRekall account is now related to your new email address new_email_address editors: title: Editors @@ -199,4 +200,8 @@ groups: success: Group group_name removed successfully filter: label: Filter by group - no_filter: Show all \ No newline at end of file + no_filter: Show all + +pending_email_address: + email: + unique: \ No newline at end of file diff --git a/translations/messages.fr.yaml b/translations/messages.fr.yaml index 63e962c..ec1f912 100644 --- a/translations/messages.fr.yaml +++ b/translations/messages.fr.yaml @@ -121,7 +121,8 @@ user: email: title: Bonjour ! Veuillez confirmer votre adresse email text: Veuillez cliquer sur le lien suivant pour mettre à jour votre profil utilisateur et utiliser dorénavant cette adresse e-mail. - confirm_email: Confirmer mon adresse e-mail + confirm_email: Confirmer mon adresse e-mail + success: Votre compte MemoRekall est dorénavant relié à cette nouvelle adresse e-mail new_email_address editors: title: Editeurs d'une capsule diff --git a/translations/validators.en.yaml b/translations/validators.en.yaml index 0b5aa0f..9fc33d6 100644 --- a/translations/validators.en.yaml +++ b/translations/validators.en.yaml @@ -27,4 +27,12 @@ capsule: group: name: not_blank: Please enter a group name - unique: There is already a group with this name \ No newline at end of file + unique: There is already a group with this name + +user: + email: + unique: Email address already used + +pending_email_address: + email: + unique: Currently waiting for validation to use this new email address for your account \ No newline at end of file diff --git a/translations/validators.fr.yaml b/translations/validators.fr.yaml index 90ce8ff..56d0c6a 100644 --- a/translations/validators.fr.yaml +++ b/translations/validators.fr.yaml @@ -27,4 +27,12 @@ capsule: group: name: not_blank: Veuillez saisir le nom du groupe - unique: Il existe déjà un groupe avec ce nom \ No newline at end of file + unique: Il existe déjà un groupe avec ce nom + +user: + email: + unique: Il existe déjà un compte relié à cette adresse e-mail + +pending_email_address: + email: + unique: Cette adresse e-mail est en attente de validation pour votre compte \ No newline at end of file -- GitLab