diff --git a/src/Maker/MakeRegistrationForm.php b/src/Maker/MakeRegistrationForm.php
index 6718f87c5..66b03df53 100644
--- a/src/Maker/MakeRegistrationForm.php
+++ b/src/Maker/MakeRegistrationForm.php
@@ -16,6 +16,7 @@
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Symfony\Bundle\MakerBundle\ConsoleStyle;
use Symfony\Bundle\MakerBundle\DependencyBuilder;
+use Symfony\Bundle\MakerBundle\Doctrine\DoctrineHelper;
use Symfony\Bundle\MakerBundle\Exception\RuntimeCommandException;
use Symfony\Bundle\MakerBundle\FileManager;
use Symfony\Bundle\MakerBundle\Generator;
@@ -55,11 +56,14 @@ final class MakeRegistrationForm extends AbstractMaker
private $router;
- public function __construct(FileManager $fileManager, FormTypeRenderer $formTypeRenderer, RouterInterface $router)
+ private $doctrineHelper;
+
+ public function __construct(FileManager $fileManager, FormTypeRenderer $formTypeRenderer, RouterInterface $router, DoctrineHelper $doctrineHelper)
{
$this->fileManager = $fileManager;
$this->formTypeRenderer = $formTypeRenderer;
$this->router = $router;
+ $this->doctrineHelper = $doctrineHelper;
}
public static function getCommandName(): string
@@ -83,6 +87,7 @@ public function interact(InputInterface $input, ConsoleStyle $io, Command $comma
->addArgument('username-field')
->addArgument('password-field')
->addArgument('will-verify-email')
+ ->addArgument('verify-email-anonymously')
->addArgument('id-getter')
->addArgument('email-getter')
->addArgument('from-email-address')
@@ -138,9 +143,22 @@ public function interact(InputInterface $input, ConsoleStyle $io, Command $comma
$input->setArgument('will-verify-email', $willVerify);
+ // This must be preset to true to avoid code being generated if $willVerify === false
+ $input->setArgument('verify-email-anonymously', false);
+
if ($willVerify) {
$this->checkComponentsExist($io);
+ $emailText[] = 'By default, users are required to be authenticated when they click the verification link that is emailed to them.';
+ $emailText[] = 'This prevents the user from registering on their laptop, then clicking the link on their phone, without';
+ $emailText[] = 'having to log in. To allow multi device email verification, we can embed a user id in the verification link.';
+ $io->text($emailText);
+ $io->newLine();
+ $input->setArgument(
+ 'verify-email-anonymously',
+ $io->confirm('Would you like to include the user id in the verification link to allow anonymous email verification?', false)
+ );
+
$input->setArgument('id-getter', $interactiveSecurityHelper->guessIdGetter($io, $userClass));
$input->setArgument('email-getter', $interactiveSecurityHelper->guessEmailGetter($io, $userClass, 'email'));
@@ -219,6 +237,26 @@ public function generate(InputInterface $input, ConsoleStyle $io, Generator $gen
'Entity\\'
);
+ $userDoctrineDetails = $this->doctrineHelper->createDoctrineDetails($userClassNameDetails->getFullName());
+
+ $userRepoVars = [
+ 'repository_full_class_name' => 'Doctrine\ORM\EntityManagerInterface',
+ 'repository_class_name' => 'EntityManagerInterface',
+ 'repository_var' => '$manager',
+ ];
+
+ $userRepository = $userDoctrineDetails->getRepositoryClass();
+
+ if (null !== $userRepository) {
+ $userRepoClassDetails = $generator->createClassNameDetails('\\'.$userRepository, 'Repository\\', 'Repository');
+
+ $userRepoVars = [
+ 'repository_full_class_name' => $userRepoClassDetails->getFullName(),
+ 'repository_class_name' => $userRepoClassDetails->getShortName(),
+ 'repository_var' => sprintf('$%s', lcfirst($userRepoClassDetails->getShortName())),
+ ];
+ }
+
$verifyEmailServiceClassNameDetails = $generator->createClassNameDetails(
'EmailVerifier',
'Security\\'
@@ -228,10 +266,13 @@ public function generate(InputInterface $input, ConsoleStyle $io, Generator $gen
$generator->generateClass(
$verifyEmailServiceClassNameDetails->getFullName(),
'verifyEmail/EmailVerifier.tpl.php',
- [
- 'id_getter' => $input->getArgument('id-getter'),
- 'email_getter' => $input->getArgument('email-getter'),
- ]
+ array_merge([
+ 'id_getter' => $input->getArgument('id-getter'),
+ 'email_getter' => $input->getArgument('email-getter'),
+ 'verify_email_anonymously' => $input->getArgument('verify-email-anonymously'),
+ ],
+ $userRepoVars
+ )
);
$generator->generateTemplate(
@@ -258,24 +299,27 @@ public function generate(InputInterface $input, ConsoleStyle $io, Generator $gen
$generator->generateController(
$controllerClassNameDetails->getFullName(),
'registration/RegistrationController.tpl.php',
- [
- 'route_path' => '/register',
- 'route_name' => 'app_register',
- 'form_class_name' => $formClassDetails->getShortName(),
- 'form_full_class_name' => $formClassDetails->getFullName(),
- 'user_class_name' => $userClassNameDetails->getShortName(),
- 'user_full_class_name' => $userClassNameDetails->getFullName(),
- 'password_field' => $input->getArgument('password-field'),
- 'will_verify_email' => $input->getArgument('will-verify-email'),
- 'verify_email_security_service' => $verifyEmailServiceClassNameDetails->getFullName(),
- 'from_email' => $input->getArgument('from-email-address'),
- 'from_email_name' => $input->getArgument('from-email-name'),
- 'email_getter' => $input->getArgument('email-getter'),
- 'authenticator_class_name' => $authenticatorClassName ? Str::getShortClassName($authenticatorClassName) : null,
- 'authenticator_full_class_name' => $authenticatorClassName,
- 'firewall_name' => $input->getOption('firewall-name'),
- 'redirect_route_name' => $input->getOption('redirect-route-name'),
- ]
+ array_merge([
+ 'route_path' => '/register',
+ 'route_name' => 'app_register',
+ 'form_class_name' => $formClassDetails->getShortName(),
+ 'form_full_class_name' => $formClassDetails->getFullName(),
+ 'user_class_name' => $userClassNameDetails->getShortName(),
+ 'user_full_class_name' => $userClassNameDetails->getFullName(),
+ 'password_field' => $input->getArgument('password-field'),
+ 'will_verify_email' => $input->getArgument('will-verify-email'),
+ 'verify_email_anonymously' => $input->getArgument('verify-email-anonymously'),
+ 'verify_email_security_service' => $verifyEmailServiceClassNameDetails->getFullName(),
+ 'from_email' => $input->getArgument('from-email-address'),
+ 'from_email_name' => $input->getArgument('from-email-name'),
+ 'email_getter' => $input->getArgument('email-getter'),
+ 'authenticator_class_name' => $authenticatorClassName ? Str::getShortClassName($authenticatorClassName) : null,
+ 'authenticator_full_class_name' => $authenticatorClassName,
+ 'firewall_name' => $input->getOption('firewall-name'),
+ 'redirect_route_name' => $input->getOption('redirect-route-name'),
+ ],
+ $userRepoVars
+ )
);
// 3) Generate the template
diff --git a/src/Resources/config/makers.xml b/src/Resources/config/makers.xml
index e17b72538..eafbed7b8 100644
--- a/src/Resources/config/makers.xml
+++ b/src/Resources/config/makers.xml
@@ -71,6 +71,7 @@
+
diff --git a/src/Resources/skeleton/registration/RegistrationController.tpl.php b/src/Resources/skeleton/registration/RegistrationController.tpl.php
index 8b87b5253..9f9f3bca4 100644
--- a/src/Resources/skeleton/registration/RegistrationController.tpl.php
+++ b/src/Resources/skeleton/registration/RegistrationController.tpl.php
@@ -11,6 +11,9 @@
use = $authenticator_full_class_name; ?>;
+
+use = $repository_full_class_name; ?>;
+
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
use Symfony\Bundle\FrameworkBundle\Controller\= $parent_class_name; ?>;
@@ -102,13 +105,33 @@ public function register(Request $request, UserPasswordEncoderInterface $passwor
* @Route("/verify/email", name="app_verify_email")
*/
- public function verifyUserEmail(Request $request): Response
+ public function verifyUserEmail(Request $request= $verify_email_anonymously ? sprintf(', %s %s', $repository_class_name, $repository_var) : null ?>): Response
{
+
$this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY');
+
+ $id = $request->get('id');
+
+ if (null === $id) {
+ return $this->redirectToRoute('app_register');
+ }
+
+
+ $repository = $manager->getRepository(= $user_class_name ?>::class);
+ $user = $repository->find($id);
+
+
+ = $repository_var; ?>->find($id);
+
+
+ if (null === $user) {
+ return $this->redirectToRoute('app_register');
+ }
+
// validate email confirmation link, sets User::isVerified=true and persists
try {
- $this->emailVerifier->handleEmailConfirmation($request, $this->getUser());
+ $this->emailVerifier->handleEmailConfirmation($request, = $verify_email_anonymously ? '$user' : '$this->getUser()' ?>);
} catch (VerifyEmailExceptionInterface $exception) {
$this->addFlash('verify_email_error', $exception->getReason());
diff --git a/src/Resources/skeleton/verifyEmail/EmailVerifier.tpl.php b/src/Resources/skeleton/verifyEmail/EmailVerifier.tpl.php
index c355696f8..e9ff81381 100644
--- a/src/Resources/skeleton/verifyEmail/EmailVerifier.tpl.php
+++ b/src/Resources/skeleton/verifyEmail/EmailVerifier.tpl.php
@@ -28,7 +28,12 @@ public function sendEmailConfirmation(string $verifyEmailRouteName, UserInterfac
$signatureComponents = $this->verifyEmailHelper->generateSignature(
$verifyEmailRouteName,
$user->= $id_getter ?>(),
+
+ $user->= $email_getter ?>(),
+ ['id' => $user->getId()]
+
$user->= $email_getter ?>()
+
);
$context = $email->getContext();
diff --git a/tests/Maker/MakeRegistrationFormTest.php b/tests/Maker/MakeRegistrationFormTest.php
index 0470541fb..1f057bedf 100644
--- a/tests/Maker/MakeRegistrationFormTest.php
+++ b/tests/Maker/MakeRegistrationFormTest.php
@@ -79,7 +79,8 @@ public function getTestDetails()
$this->getMakerInstance(MakeRegistrationForm::class),
[
'n', // add UniqueEntity
- 'y', // no verify user
+ 'y', // verify user
+ 'y', // require authentication to verify user email
'jr@rushlow.dev', // from email address
'SymfonyCasts', // From Name
'n', // no authenticate after
@@ -110,7 +111,8 @@ function (string $output, string $directory) {
$this->getMakerInstance(MakeRegistrationForm::class),
[
'n', // add UniqueEntity
- 'y', // no verify user
+ 'y', // verify user
+ 'n', // require authentication to verify user email
'jr@rushlow.dev', // from email address
'SymfonyCasts', // From Name
'', // yes authenticate after
@@ -125,5 +127,26 @@ function (string $output, string $directory) {
->addExtraDependencies('symfony/web-profiler-bundle')
->addExtraDependencies('mailer'),
];
+
+ yield 'verify_email_no_auth_functional_test' => [MakerTestDetails::createTest(
+ $this->getMakerInstance(MakeRegistrationForm::class),
+ [
+ 'n', // add UniqueEntity
+ 'y', // verify user's email
+ 'y', // require authentication to verify user email
+ 'jr@rushlow.dev', // from email address
+ 'SymfonyCasts', // From Name
+ '', // yes authenticate after
+ 'main', // redirect to route after registration
+ ])
+ ->setRequiredPhpVersion(70200)
+ ->setFixtureFilesPath(__DIR__.'/../fixtures/MakeRegistrationFormVerifyEmailNoAuthFunctionalTest')
+ ->addExtraDependencies('symfonycasts/verify-email-bundle')
+ ->configureDatabase()
+ ->updateSchemaAfterCommand()
+ // needed for internal functional test
+ ->addExtraDependencies('symfony/web-profiler-bundle')
+ ->addExtraDependencies('mailer'),
+ ];
}
}
diff --git a/tests/fixtures/MakeRegistrationFormVerifyEmailNoAuthFunctionalTest/config/packages/mailer.yaml b/tests/fixtures/MakeRegistrationFormVerifyEmailNoAuthFunctionalTest/config/packages/mailer.yaml
new file mode 100644
index 000000000..6da81a36b
--- /dev/null
+++ b/tests/fixtures/MakeRegistrationFormVerifyEmailNoAuthFunctionalTest/config/packages/mailer.yaml
@@ -0,0 +1,3 @@
+framework:
+ mailer:
+ dsn: 'null://null'
\ No newline at end of file
diff --git a/tests/fixtures/MakeRegistrationFormVerifyEmailNoAuthFunctionalTest/config/packages/security.yaml b/tests/fixtures/MakeRegistrationFormVerifyEmailNoAuthFunctionalTest/config/packages/security.yaml
new file mode 100644
index 000000000..2fa285946
--- /dev/null
+++ b/tests/fixtures/MakeRegistrationFormVerifyEmailNoAuthFunctionalTest/config/packages/security.yaml
@@ -0,0 +1,20 @@
+security:
+ encoders:
+ App\Entity\User: bcrypt
+
+ # https://symfony.com/doc/current/security.html#where-do-users-come-from-user-providers
+ providers:
+ app_user_provider:
+ entity:
+ class: App\Entity\User
+ property: email
+
+ firewalls:
+ dev:
+ pattern: ^/(_(profiler|wdt)|css|images|js)/
+ security: false
+ main:
+ anonymous: true
+# guard:
+# authenticators:
+# - App\Security\StubAuthenticator
\ No newline at end of file
diff --git a/tests/fixtures/MakeRegistrationFormVerifyEmailNoAuthFunctionalTest/src/Controller/MyController.php b/tests/fixtures/MakeRegistrationFormVerifyEmailNoAuthFunctionalTest/src/Controller/MyController.php
new file mode 100644
index 000000000..72d6fab61
--- /dev/null
+++ b/tests/fixtures/MakeRegistrationFormVerifyEmailNoAuthFunctionalTest/src/Controller/MyController.php
@@ -0,0 +1,18 @@
+id;
+ }
+
+ public function getEmail()
+ {
+ return $this->email;
+ }
+
+ public function setEmail(string $email): self
+ {
+ $this->email = $email;
+
+ return $this;
+ }
+
+ public function getUsername(): string
+ {
+ return (string) $this->email;
+ }
+
+ public function getRoles(): array
+ {
+ $roles = $this->roles;
+ // guarantee every user at least has ROLE_USER
+ $roles[] = 'ROLE_USER';
+
+ return array_unique($roles);
+ }
+
+ public function setRoles(array $roles): self
+ {
+ $this->roles = $roles;
+
+ return $this;
+ }
+
+ public function getPassword(): string
+ {
+ return (string) $this->password;
+ }
+
+ public function setPassword(string $password): self
+ {
+ $this->password = $password;
+
+ return $this;
+ }
+
+ public function getSalt()
+ {
+ }
+
+ public function eraseCredentials()
+ {
+ }
+}
diff --git a/tests/fixtures/MakeRegistrationFormVerifyEmailNoAuthFunctionalTest/src/Repository/UserRepository.php b/tests/fixtures/MakeRegistrationFormVerifyEmailNoAuthFunctionalTest/src/Repository/UserRepository.php
new file mode 100644
index 000000000..8f695cbe0
--- /dev/null
+++ b/tests/fixtures/MakeRegistrationFormVerifyEmailNoAuthFunctionalTest/src/Repository/UserRepository.php
@@ -0,0 +1,21 @@
+request('GET', '/register');
+
+ $form = $crawler->selectButton('Register')->form();
+ $form['registration_form[email]'] = 'jr@rushlow.dev';
+ $form['registration_form[plainPassword]'] = 'noAuth';
+ $form['registration_form[agreeTerms]'] = true;
+
+ $client->submit($form);
+
+ $messages = $this->getMailerMessages();
+ self::assertCount(1, $messages);
+
+ /** @var EntityManager $em */
+ $em = self::$kernel->getContainer()
+ ->get('doctrine')
+ ->getManager()
+ ;
+
+ $query = $em->createQuery('SELECT u FROM App\\Entity\\User u WHERE u.email = \'jr@rushlow.dev\'');
+
+ self::assertFalse(($query->getSingleResult())->isVerified());
+
+ $context = $messages[0]->getContext();
+
+ $client->request('GET', $context['signedUrl']);
+
+ self::assertTrue(($query->getSingleResult())->isVerified());
+ }
+}