diff --git a/migrations/Version20220307154836.php b/migrations/Version20220307154836.php new file mode 100644 index 0000000000000000000000000000000000000000..ac675b329b4a4552cf34075703db1bc7e089494e --- /dev/null +++ b/migrations/Version20220307154836.php @@ -0,0 +1,37 @@ +<?php + +declare(strict_types=1); + +namespace DoctrineMigrations; + +use Doctrine\DBAL\Schema\Schema; +use Doctrine\Migrations\AbstractMigration; + +/** + * Auto-generated Migration: Please modify to your needs! + */ +final class Version20220307154836 extends AbstractMigration +{ + public function getDescription(): string + { + return ''; + } + + public function up(Schema $schema): void + { + // this up() migration is auto-generated, please modify it to your needs + $this->addSql('CREATE TABLE `pending_email_address` (id INT AUTO_INCREMENT NOT NULL, user INT NOT NULL, email VARCHAR(255) NOT NULL, INDEX IDX_621FD0118D93D649 (user), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8 COLLATE `utf8_unicode_ci` ENGINE = InnoDB'); + $this->addSql('ALTER TABLE `pending_email_address` ADD CONSTRAINT FK_621FD0118D93D649 FOREIGN KEY (user) REFERENCES `user` (id)'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('DROP TABLE `pending_email_address`'); + $this->addSql('ALTER TABLE capsule CHANGE nom nom VARCHAR(255) NOT NULL COLLATE `utf8_unicode_ci`, CHANGE link link VARCHAR(255) NOT NULL COLLATE `utf8_unicode_ci`, CHANGE edition_link edition_link VARCHAR(255) NOT NULL COLLATE `utf8_unicode_ci`, CHANGE password password VARCHAR(50) NOT NULL COLLATE `utf8_unicode_ci`'); + $this->addSql('ALTER TABLE `group` CHANGE name name VARCHAR(255) NOT NULL COLLATE `utf8_unicode_ci`'); + $this->addSql('ALTER TABLE invitation_editeur_capsule CHANGE email email VARCHAR(255) NOT NULL COLLATE `utf8_unicode_ci`'); + $this->addSql('ALTER TABLE reset_password_request CHANGE selector selector VARCHAR(20) NOT NULL COLLATE `utf8_unicode_ci`, CHANGE hashed_token hashed_token VARCHAR(100) NOT NULL COLLATE `utf8_unicode_ci`'); + $this->addSql('ALTER TABLE `user` CHANGE email email VARCHAR(255) NOT NULL COLLATE `utf8_unicode_ci`, CHANGE email_canonical email_canonical VARCHAR(255) NOT NULL COLLATE `utf8_unicode_ci`, CHANGE name name VARCHAR(255) NOT NULL COLLATE `utf8_unicode_ci`, CHANGE firstname firstname VARCHAR(255) NOT NULL COLLATE `utf8_unicode_ci`, CHANGE username username VARCHAR(255) NOT NULL COLLATE `utf8_unicode_ci`, CHANGE username_canonical username_canonical VARCHAR(255) NOT NULL COLLATE `utf8_unicode_ci`, CHANGE roles roles LONGTEXT NOT NULL COLLATE `utf8_unicode_ci` COMMENT \'(DC2Type:json)\', CHANGE password password VARCHAR(255) NOT NULL COLLATE `utf8_unicode_ci`, CHANGE salt salt VARCHAR(255) NOT NULL COLLATE `utf8_unicode_ci`'); + } +} diff --git a/src/Controller/CapsuleEditorController.php b/src/Controller/CapsuleEditorController.php index 6b08bbcbecb0eaa7f098600e2e17fa56a4f86750..0cc7c3de8b75de491138d3b38e1fcf4252f17827 100755 --- a/src/Controller/CapsuleEditorController.php +++ b/src/Controller/CapsuleEditorController.php @@ -278,9 +278,6 @@ class CapsuleEditorController extends AbstractController int $capsule_id, Request $request ): Response { - $form = $this->createForm(RemoveEditorFormType::class); - $form->handleRequest($request); - $capsule = $this->capsule_repository->findOneBy(['id' => $capsule_id]); if (! $capsule instanceof Capsule) { throw new \Exception('The retrieved capsule is not an instance of Capsule.'); @@ -289,6 +286,9 @@ class CapsuleEditorController extends AbstractController $pending_editor_invitation = $this->capsule_pending_editor_invitation_repository ->findOneBy(['id' => $pending_editor_invitation_id]); + $form = $this->createForm(RemoveEditorFormType::class); + $form->handleRequest($request); + if (! $pending_editor_invitation instanceof PendingEditorInvitation) { $this->addFlash( 'warning', diff --git a/src/Controller/RegistrationController.php b/src/Controller/RegistrationController.php index 4155bb7e618a1911940add7a8a17a066a05719f8..e6410636e992e254eae341243c741209ad1e4129 100755 --- a/src/Controller/RegistrationController.php +++ b/src/Controller/RegistrationController.php @@ -69,7 +69,7 @@ class RegistrationController extends AbstractController } return $this->renderForm('registration/register.html.twig', [ - 'registrationForm' => $form, + 'registrationForm' => $form ]); } @@ -92,7 +92,7 @@ class RegistrationController extends AbstractController try { $this->email_verifier->handleEmailConfirmation($request, $user); } catch (VerifyEmailExceptionInterface $exception) { - $this->addFlash('verify_email_error', $exception->getReason()); + $this->addFlash('error', $exception->getReason()); return $this->redirectToRoute('app_register'); } diff --git a/src/Controller/ResetPasswordController.php b/src/Controller/ResetPasswordController.php index 7169714865de8548846371b272e0a61d08617644..a58c51a80a2cbf43725301164807baafaf8b95ef 100755 --- a/src/Controller/ResetPasswordController.php +++ b/src/Controller/ResetPasswordController.php @@ -43,7 +43,7 @@ class ResetPasswordController extends AbstractController } return $this->renderForm('reset_password/request.html.twig', [ - 'requestForm' => $form, + 'requestForm' => $form ]); } @@ -118,7 +118,7 @@ class ResetPasswordController extends AbstractController } return $this->renderForm('reset_password/reset.html.twig', [ - 'resetForm' => $form, + 'resetForm' => $form ]); } diff --git a/src/Controller/UserController.php b/src/Controller/UserController.php index 52c454c658b8d020c01beba889593f520518ba45..89f7ecae410d7d489d8749a1800b67f471fa688b 100755 --- a/src/Controller/UserController.php +++ b/src/Controller/UserController.php @@ -2,13 +2,18 @@ namespace App\Controller; +use App\Entity\PendingEmailAddress; use App\Entity\User; use App\Form\EditPasswordFormType; use App\Form\EditUserProfileFormType; +use App\Repository\PendingEmailAddressRepository; 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; @@ -17,7 +22,8 @@ class UserController extends AbstractController { public function __construct( private EntityManagerInterface $entity_manager, - private TranslatorInterface $translator + private TranslatorInterface $translator, + private PendingEmailAddressRepository $pending_email_address_repository ) { } @@ -36,26 +42,65 @@ class UserController extends AbstractController } #[Route('/edit_profile', name:'edit_profile')] - public function editProfile(Request $request): Response - { + public function editProfile( + Request $request, + MailerInterface $mailer + ): Response { $current_user = $this->getUser(); if (! $current_user instanceof User) { return $this->redirectToRoute('app_logout'); } - $form = $this->createForm(EditUserProfileFormType::class, $current_user); + $last_pending_email_address = $this->pending_email_address_repository->findOneBy( + ['user' => $current_user->getId()] + ); + if ($last_pending_email_address !== null) { + $this->entity_manager->remove($last_pending_email_address); + } - $form->setData($current_user); + $form = $this->createForm( + EditUserProfileFormType::class, + $current_user, + ['current_email_address' => $current_user->getEmail()] + ); $form->handleRequest($request); if ($form->isSubmitted() && $form->isValid()) { + if ($current_user->getEmail() !== $form->get('email')->getData()) { + $pending_email_address = new PendingEmailAddress(); + $pending_email_address->setEmail($form->get('email')->getData()); + $pending_email_address->setUser($current_user); + $this->entity_manager->persist($pending_email_address); + $this->entity_manager->flush(); + + $email = (new TemplatedEmail()) + ->to(new Address($pending_email_address->getEmail())) + ->subject($this->translator->trans('user.edit.email.title')) + ->htmlTemplate('user/update_email.html.twig') + ->context([ + 'expiration_date' => new \DateTime('+1 day') + ]); + $mailer->send($email); + + $this->addFlash( + 'warning', + $this->translator->trans('user.profile.updated.warning', [ + 'new_email_address' => $form->get('email')->getData() + ]) + ); + + return $this->render('user/edit_email_address.html.twig', [ + 'new_email_address' => $form->get('email')->getData() + ]); + } + $this->entity_manager->persist($current_user); $this->entity_manager->flush(); $this->addFlash( - 'profile_updated_success', - $this->translator->trans('user.profile.updated_success') + 'success', + $this->translator->trans('user.profile.updated.success') ); return $this->redirectToRoute('show_profile'); @@ -69,7 +114,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); @@ -98,4 +143,33 @@ 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('Pending email address 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 new file mode 100644 index 0000000000000000000000000000000000000000..3a29a2209c9673880354bf20bd5f39b00c5d19cd --- /dev/null +++ b/src/Entity/PendingEmailAddress.php @@ -0,0 +1,47 @@ +<?php + +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] + #[ORM\GeneratedValue] + #[ORM\Column(type:'integer')] + private int $id; + + #[ORM\ManyToOne(targetEntity:User::class)] + #[ORM\JoinColumn(name:'user', referencedColumnName:'id', nullable:false)] + private User $user; + + #[Assert\Email(message: 'email.valid')] + #[ORM\Column(type:'string', length:255)] + private string $email; + + public function getUser(): User + { + return $this->user; + } + + public function setUser(User $user): void + { + $this->user = $user; + } + + public function getEmail(): string + { + return $this->email; + } + + public function setEmail(string $email): void + { + $this->email = $email; + } +} diff --git a/src/Form/EditUserProfileFormType.php b/src/Form/EditUserProfileFormType.php index 637d12bdd87d4175bb1358b739fa0e6b51b0ec18..a13cd5b8a7d2d3cb5cd1f9b8d67ad0251d08b3f2 100755 --- a/src/Form/EditUserProfileFormType.php +++ b/src/Form/EditUserProfileFormType.php @@ -3,6 +3,8 @@ namespace App\Form; use App\Entity\User; +use App\Repository\PendingEmailAddressRepository; +use App\Repository\UserRepository; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\EmailType; use Symfony\Component\Form\Extension\Core\Type\PasswordType; @@ -12,11 +14,27 @@ use Symfony\Component\Form\FormBuilderInterface; 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 { + private UserRepository $user_repository; + private PendingEmailAddressRepository $pending_email_address_repository; + + public function __construct( + UserRepository $user_repository, + PendingEmailAddressRepository $pending_email_address_repository + ) { + $this->user_repository = $user_repository; + $this->pending_email_address_repository = $pending_email_address_repository; + } + public function buildForm(FormBuilderInterface $builder, array $options): void { + $user_repository = $this->user_repository; + $pending_email_address_repository = $this->pending_email_address_repository; + $builder ->add( 'firstName', @@ -40,9 +58,36 @@ class EditUserProfileFormType extends AbstractType 'email', EmailType::class, [ - 'constraints' => [new NotBlank(['message' => 'email.not_blank'])], + 'data' => $options['current_email_address'], + 'constraints' => [ + new NotBlank(['message' => 'email.not_blank']), + new Assert\Email(), + new Assert\Callback( + ['callback' => static function ( + string $value, + ExecutionContextInterface $context + ) use ( + $options, + $user_repository, + $pending_email_address_repository + ) { + if ( + $value !== $options['current_email_address'] && + ($user_repository->emailExists($value) || + $pending_email_address_repository->emailIsReserved($value)) + ) { + $context + ->buildViolation(message: 'user.email.unique') + ->addViolation() + ; + } + } + ] + ) + ], 'label' => 'general.email', - 'empty_data' => '' + 'empty_data' => '', + 'mapped' => false ] ) ->add( @@ -70,6 +115,9 @@ class EditUserProfileFormType extends AbstractType { $resolver->setDefaults([ 'data_class' => User::class, - ]); + 'current_email_address' => '', + 'users_emails' => [], + 'pending_emails' => [] + ]); } } diff --git a/src/Repository/PendingEmailAddressRepository.php b/src/Repository/PendingEmailAddressRepository.php new file mode 100644 index 0000000000000000000000000000000000000000..19e82ea794616a42fd03cd84b55c4de1a7b5d63a --- /dev/null +++ b/src/Repository/PendingEmailAddressRepository.php @@ -0,0 +1,42 @@ +<?php + +namespace App\Repository; + +use App\Entity\PendingEmailAddress; +use App\Entity\User; +use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; +use Doctrine\Persistence\ManagerRegistry; + +/** + * @method PendingEmailAddress|null find($id, $lockMode = null, $lockVersion = null) + * @method PendingEmailAddress|null findOneBy(array $criteria, array $orderBy = null) + * @method PendingEmailAddress[] findAll() + * @method PendingEmailAddress[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) + * @template PendingEmailAddress of object + */ +class PendingEmailAddressRepository extends ServiceEntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, PendingEmailAddress::class); + } + + public function findOneByEmail(string $value): ?User + { + return $this->createQueryBuilder('u') + ->andWhere('u.email = :val') + ->setParameter('val', $value) + ->getQuery() + ->getOneOrNullResult() + ; + } + + public function emailIsReserved(string $email): bool + { + return $this->createQueryBuilder('p') + ->andWhere('p.email = :email') + ->setParameter('email', $email) + ->getQuery() + ->getOneOrNullResult() !== null; + } +} diff --git a/src/Repository/UserRepository.php b/src/Repository/UserRepository.php index 147f8c5c26d5ab001ea4d65733de9cf19b9aebd3..8996ca410a19256346f161694cafce5067b65d3e 100755 --- a/src/Repository/UserRepository.php +++ b/src/Repository/UserRepository.php @@ -4,8 +4,6 @@ namespace App\Repository; use App\Entity\User; use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; -use Doctrine\DBAL\Exception; -use Doctrine\DBAL\Exception\DatabaseObjectNotFoundException; use Doctrine\Persistence\ManagerRegistry; use Symfony\Component\Security\Core\Exception\UnsupportedUserException; use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface; @@ -72,4 +70,13 @@ class UserRepository extends ServiceEntityRepository implements PasswordUpgrader ->getQuery() ->getResult(); } + + public function emailExists(string $email): bool + { + return $this->createQueryBuilder('r') + ->andWhere('r.email = :email') + ->setParameter('email', $email) + ->getQuery() + ->getOneOrNullResult() !== null; + } } diff --git a/src/Security/EmailVerifier.php b/src/Security/EmailVerifier.php index ea4389fcac727b3d0ffa02f431f47845f93c0382..aa910c7e1edf292eca79ed7463cdc9ff38ea9379 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/capsule/editors/list_editors.html.twig b/templates/capsule/editors/list_editors.html.twig index 4a04b8997e0c998d9bb57eb18d6e6baf8c91c020..0b71139fb3a355c7088c6e6ea959357c2dc45914 100644 --- a/templates/capsule/editors/list_editors.html.twig +++ b/templates/capsule/editors/list_editors.html.twig @@ -18,13 +18,13 @@ </div> {% for flashWarning in app.flashes('warning') %} - <div class="text-center alert alert-warning col-5 mx-auto my-5 mt-2" role="alert"> + <div class="text-center alert alert-warning col-11 col-md-10 col-lg-9 col-xl-8 mx-auto my-5 mt-2" role="alert"> {{ flashWarning }} </div> {% endfor %} {% for flashSuccess in app.flashes('success') %} - <div class="text-center alert alert-success col-5 mx-auto my-5 mt-2" role="alert"> + <div class="text-center alert alert-success col-11 col-md-10 col-lg-9 col-xl-8 mx-auto my-5 mt-2" role="alert"> {{ flashSuccess }} </div> {% endfor %} diff --git a/templates/capsule/groups/capsule_groups.html.twig b/templates/capsule/groups/capsule_groups.html.twig index 5129485dccce24ff3c8181cdfa6f98efc7461908..ad38080a086ba9178f00752cbd144c58053aef3c 100644 --- a/templates/capsule/groups/capsule_groups.html.twig +++ b/templates/capsule/groups/capsule_groups.html.twig @@ -18,13 +18,13 @@ </div> {% for flashWarning in app.flashes('warning') %} - <div class="text-center alert alert-warning col-5 mx-auto my-5 mt-2" role="alert"> + <div class="text-center alert alert-warning col-11 col-md-10 col-lg-9 col-xl-8 mx-auto my-5 mt-2" role="alert"> {{ flashWarning }} </div> {% endfor %} {% for flashSuccess in app.flashes('success') %} - <div class="text-center alert alert-success col-5 mx-auto my-5 mt-2" role="alert"> + <div class="text-center alert alert-success col-11 col-md-10 col-lg-9 col-xl-8 mx-auto my-5 mt-2" role="alert"> {{ flashSuccess }} </div> {% endfor %} diff --git a/templates/capsule/groups/user_groups.html.twig b/templates/capsule/groups/user_groups.html.twig index d0737cc1dd680adfb544ed8164cb5b06877b2922..808b416dd7f22040826f594ff249afb6cb725fb6 100644 --- a/templates/capsule/groups/user_groups.html.twig +++ b/templates/capsule/groups/user_groups.html.twig @@ -17,13 +17,13 @@ </div> {% for flashWarning in app.flashes('warning') %} - <div class="text-center alert alert-warning col-5 mx-auto my-5 mt-2" role="alert"> + <div class="text-center alert alert-warning col-11 col-md-10 col-lg-9 col-xl-8 mx-auto my-5 mt-2" role="alert"> {{ flashWarning }} </div> {% endfor %} {% for flashSuccess in app.flashes('success') %} - <div class="text-center alert alert-success col-5 mx-auto my-5 mt-2" role="alert"> + <div class="text-center alert alert-success col-11 col-md-10 col-lg-9 col-xl-8 mx-auto my-5 mt-2" role="alert"> {{ flashSuccess }} </div> {% endfor %} diff --git a/templates/capsule/index.html.twig b/templates/capsule/index.html.twig index 5c47a37681b8d7f75caa82c8f17321344afd3025..59b64dbb425eb630fd4ab7dc3bcc59b88974d6ff 100644 --- a/templates/capsule/index.html.twig +++ b/templates/capsule/index.html.twig @@ -34,13 +34,13 @@ </div> {% for flashWarning in app.flashes('warning') %} - <div class="text-center alert alert-warning col-5 mx-auto my-5" role="alert"> + <div class="text-center alert alert-warning col-11 col-md-10 col-lg-9 col-xl-8 mx-auto my-5" role="alert"> {{ flashWarning }} </div> {% endfor %} {% for flashSuccess in app.flashes('success') %} - <div class="text-center alert alert-success col-5 mx-auto my-5" role="alert"> + <div class="text-center alert alert-success col-11 col-md-10 col-lg-9 col-xl-8 mx-auto my-5" role="alert"> {{ flashSuccess }} </div> {% endfor %} diff --git a/templates/legacy/legacy.html.twig b/templates/legacy/legacy.html.twig index 2bf7dbff33604db8efb47927cc9ace0ebe255cf6..8d4d49edc75d8e73e079d838e895ddc18c38ce1d 100644 --- a/templates/legacy/legacy.html.twig +++ b/templates/legacy/legacy.html.twig @@ -1,21 +1,14 @@ {% extends 'layout.html.twig' %} - - {% block body %} <iframe src="{{ url }}" style="width:100%;height:100%;top:0;left:0;position:absolute" - {# width="1200" - height="600" #} - frameborder="0" webkitallowfullscreen mozallowfullscreen allowfullscreen> </iframe> - - {% endblock %} diff --git a/templates/registration/register.html.twig b/templates/registration/register.html.twig index 3f9b4c0bbb3a2729667eb6f563ba0a38c0c7f360..99d5864530a878b05bb61cf9f2bcee6d7bbfa18e 100644 --- a/templates/registration/register.html.twig +++ b/templates/registration/register.html.twig @@ -6,8 +6,10 @@ {% block body %} <div class="mt-4"> - {% for flashError in app.flashes('verify_email_error') %} - <div class="alert alert-danger col-6 mx-auto my-5 h1" role="alert">{{ flashError }}</div> + {% for flashError in app.flashes('error') %} + <div class="alert alert-danger col-11 col-md-10 col-lg-9 col-xl-8 mx-auto my-5 h1" role="alert"> + {{ flashError }} + </div> {% endfor %} {{ form_start(registrationForm, {'attr': {novalidate: 'novalidate', 'class': 'd-flex flex-column justify-content-center'}}) }} diff --git a/templates/user/edit_email_address.html.twig b/templates/user/edit_email_address.html.twig new file mode 100644 index 0000000000000000000000000000000000000000..a3017afedbc6e31f977ba10c7500e4d93edf7f7c --- /dev/null +++ b/templates/user/edit_email_address.html.twig @@ -0,0 +1,21 @@ +{% extends 'layout.html.twig' %} + +{% block title %} + {{ 'user.edit_email_address'|trans }} +{% endblock %} + +{% block body %} + <div class="row gx-0"> + <div class="row-title-box"> + <h3 class="row-title"> + {{ 'user.edit_email_address'|trans }} + </h3> + </div> + </div> + + {% for flashWarning in app.flashes('warning') %} + <div class="text-center alert alert-warning col-11 col-md-10 col-lg-9 col-xl-8 mx-auto my-5" role="alert"> + {{ flashWarning }} + </div> + {% endfor %} +{% endblock %} \ No newline at end of file diff --git a/templates/user/edit_profile.html.twig b/templates/user/edit_profile.html.twig index 57a83762c698e8447599f715fe2c8edc904b1fc3..d916e3c64bb9c12a9afb2e11de4498a47ebdf755 100644 --- a/templates/user/edit_profile.html.twig +++ b/templates/user/edit_profile.html.twig @@ -23,5 +23,4 @@ {{ form_row(editUserProfileForm.update, {'row_attr': {'class' : 'form-group d-flex flex-column m-auto mb-4 col-2'}}) }} {{ form_end(editUserProfileForm) }} </div> - {% endblock %} \ No newline at end of file diff --git a/templates/user/profile.html.twig b/templates/user/profile.html.twig index a02eef8a508eff07569d36094696c98db223f43f..d6e80801ccecf76763cd89894490417248212004 100644 --- a/templates/user/profile.html.twig +++ b/templates/user/profile.html.twig @@ -13,12 +13,18 @@ </div> </div> - {% for flashSuccess in app.flashes('profile_updated_success') %} - <div class="text-center alert alert-success col-5 mx-auto my-5" role="alert"> + {% for flashSuccess in app.flashes('success') %} + <div class="text-center alert alert-success col-11 col-md-10 col-lg-9 col-xl-8 mx-auto mt-0 mb-4" role="alert"> {{ flashSuccess }} </div> {% endfor %} + {% for flashWarning in app.flashes('warning') %} + <div class="text-center alert alert-warning col-11 col-md-10 col-lg-9 col-xl-8 mx-auto mt-0 mb-4" role="alert"> + {{ flashWarning }} + </div> + {% endfor %} + <div class="d-flex flex-column flex-md-row justify-content-center align-items-center"> <div class="profile-block d-flex flex-row ps-3 ps-md-5 pe-3 pe-md-5 pt-4 pb-3 fw-normal me-0 me-md-5"> <div class="pe-3 pe-md-4 text-nowrap"> diff --git a/templates/user/update_email.html.twig b/templates/user/update_email.html.twig new file mode 100644 index 0000000000000000000000000000000000000000..0f092ca27a0b78c7811360894e53620ba7e8bdd3 --- /dev/null +++ b/templates/user/update_email.html.twig @@ -0,0 +1,15 @@ +<h1> + {{ 'user.edit.email.title'|trans }} +</h1> + +<p> + {{ 'user.edit.email.text'|trans }}: + <br><br> + <a href="{{ url('verify_new_email_address') }}"> + {{ 'user.edit.email.confirm_email'|trans }} + </a> +</p> + +<p> + {{ 'general.greeting'|trans }} +</p> diff --git a/tests/functional/UserControllerTest.php b/tests/functional/UserControllerTest.php new file mode 100644 index 0000000000000000000000000000000000000000..427ed8353895dbfabab2e80bd1782f8f74e214c2 --- /dev/null +++ b/tests/functional/UserControllerTest.php @@ -0,0 +1,67 @@ +<?php + +namespace App\Tests\functional; + +use App\Entity\User; +use Exception; +use Symfony\Bundle\FrameworkBundle\KernelBrowser; +use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; + +class UserControllerTest extends WebTestCase +{ + private KernelBrowser $client; + private User $verified_user; + + protected function setUp(): void + { + self::ensureKernelShutdown(); + $this->client = static::createClient(); + + $object_manager = $this->client->getContainer() + ->get('doctrine') + ->getManager(); + + $verified_user = $object_manager + ->getRepository(User::class) + ->findOneBy(['email' => 'defaultUser@localhost.com']); + + if (! $verified_user instanceof User) { + throw new \Exception("User does not exist."); + } + + $this->verified_user = $verified_user; + } + + public function testWhenUserUpdateEmailHeReceivesAConfirmationLinkByEMail(): void + { + $new_email_address = 'defaultuser@localhost.test'; + $this->client->loginUser($this->verified_user); + $crawler = $this->client->request('GET', '/edit_profile'); + + $this->assertResponseIsSuccessful(); + $this->client->enableProfiler(); + + $submit = $crawler->selectButton('Update'); + $form = $submit->form(); + $form['edit_user_profile_form[email]'] = $new_email_address; + $form['edit_user_profile_form[current_password]'] = "password"; + $this->client->submit($form); + + $this->assertEmailCount( + 1, + null, + 'Once the user update his email, the system should send him a confirmation link by email' + ); + $emailMessage = $this->getMailerMessage(0); + + if (null === $emailMessage) { + throw new Exception("Email message could not be found"); + } + + $this->assertEmailAddressContains( + $emailMessage, + 'To', + $new_email_address + ); + } +} diff --git a/translations/messages.en.yaml b/translations/messages.en.yaml index 9b2e68d9d6f9be9aa298936367f51412c3dd0f62..d3c7ea7f6b92117497cd48a3c04e9677a5dcf01b 100644 --- a/translations/messages.en.yaml +++ b/translations/messages.en.yaml @@ -107,7 +107,10 @@ user: title: My profile edit: Edit profile update: Update - updated_success: The profile has been updated + updated: + success: The profile has been updated + warning: In order to update your current email address related to your Memorekall account with the new email address new_email_address, + please confirm this change following the link your received by email. password: edit: Edit password current: Current password @@ -115,6 +118,13 @@ user: updated_success: The password has been updated edit_profile: Edit my profile edit_password: Edit my password + edit_email_address: Edit my email address + 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 + confirm_email: Confirm my email + success: Your MemoRekall account is now related to your new email address new_email_address editors: title: Editors @@ -190,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 55f20cea6a443e77c30b740689f2b7fbcb91b0fc..ec1f912892ae88eb561e4de71aa25173c2d3b213 100644 --- a/translations/messages.fr.yaml +++ b/translations/messages.fr.yaml @@ -106,7 +106,9 @@ user: title: Mon profil edit: Modifier mon profil update: Mettre à jour - updated_success: Votre profil a bien été mis à jour + updated: + success: Votre profil a bien été mis à jour + warning: Veuillez confirmer le changement d'adresse e-mail en activant le lien que vous avez reçu par mail à l'adresse new_email_address. L'adresse e-amil reliée à votre compte Memorekall sera alors mise à jour avec la nouvelle new_email_address. password: edit: Modifier mon mot de passe current: Mot de passe actuel @@ -114,6 +116,13 @@ user: updated_success: Votre mot de passe a bien été modifié edit_profile: Modifier mon mot de passe edit_password: Modifier mon mot de passe + edit_email_address: Modifier mon adresse e-mail + edit: + 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 + 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 0b5aa0fde3be903238c1f689641011a2b4d17311..9fc33d66473360bc5ebbd0da5f7ded8066775290 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 90ce8ff4ed786c94b1923bac182f3ff2b7f99d1c..56d0c6a066164f197b673a09b530c7eabcfd84a1 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