Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP - experimental - bundled maker #282

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@
"symfony/framework-bundle": "^5.4 | ^6.0 | ^7.0",
"symfony/phpunit-bridge": "^5.4 | ^6.0 | ^7.0",
"doctrine/doctrine-bundle": "^2.8",
"doctrine/annotations": "^1.0"
"doctrine/annotations": "^1.0",
"symfony/maker-bundle": "^1.53"
},
"autoload": {
"psr-4": {
Expand Down
72 changes: 72 additions & 0 deletions src/MakerBundle/MakeResetPassword.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<?php

/*
* This file is part of the SymfonyCasts ResetPasswordBundle package.
* Copyright (c) SymfonyCasts <https://symfonycasts.com/>
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace SymfonyCasts\Bundle\ResetPassword\MakerBundle;

use Symfony\Bridge\Twig\Mime\TemplatedEmail;
use Symfony\Bundle\MakerBundle\ConsoleStyle;
use Symfony\Bundle\MakerBundle\DependencyBuilder;
use Symfony\Bundle\MakerBundle\Generator;
use Symfony\Bundle\MakerBundle\InputConfiguration;
use Symfony\Bundle\MakerBundle\MakerInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\EmailType;
use Symfony\Component\Form\Extension\Core\Type\PasswordType;
use Symfony\Component\Form\Extension\Core\Type\RepeatedType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Mailer\MailerInterface;
use Symfony\Component\Mime\Address;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
use Symfony\Component\Translation\Translator;
use Symfony\Component\Validator\Constraints\Length;
use Symfony\Component\Validator\Constraints\NotBlank;
use Symfony\Component\Yaml\Yaml;
use Symfony\Contracts\Translation\TranslatorInterface;

class MakeResetPassword implements MakerInterface
{
public static function getCommandName(): string
{
return 'make:rp-bundle';
}

public static function getCommandDescription(): string
{
return 'Some Description';
}

/**
* Configure the command: set description, input arguments, options, etc.
*
* By default, all arguments will be asked interactively. If you want
* to avoid that, use the $inputConfig->setArgumentAsNonInteractive() method.
*/
public function configureCommand(Command $command, InputConfiguration $inputConfig): void
{
}

/**
* Configure any library dependencies that your maker requires.
*/
public function configureDependencies(DependencyBuilder $dependencies): void
{
}

public function interact(InputInterface $input, ConsoleStyle $io, Command $command): void
{
$io->title('Let\'s make a password reset feature!');
}

public function generate(InputInterface $input, ConsoleStyle $io, Generator $generator): void
{
}
}
48 changes: 48 additions & 0 deletions src/MakerBundle/Templates/ChangePasswordFormType.tpl.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<?php echo "<?php\n"; ?>

namespace <?php echo $namespace; ?>;

Check failure on line 3 in src/MakerBundle/Templates/ChangePasswordFormType.tpl.php

View workflow job for this annotation

GitHub Actions / sca / Psalm

UndefinedGlobalVariable

src/MakerBundle/Templates/ChangePasswordFormType.tpl.php:3:22: UndefinedGlobalVariable: Cannot find referenced variable $namespace in global scope (see https://psalm.dev/127)

<?php echo $use_statements; ?>

Check failure on line 5 in src/MakerBundle/Templates/ChangePasswordFormType.tpl.php

View workflow job for this annotation

GitHub Actions / sca / Psalm

UndefinedGlobalVariable

src/MakerBundle/Templates/ChangePasswordFormType.tpl.php:5:12: UndefinedGlobalVariable: Cannot find referenced variable $use_statements in global scope (see https://psalm.dev/127)

class <?php echo $class_name; ?> extends AbstractType

Check failure on line 7 in src/MakerBundle/Templates/ChangePasswordFormType.tpl.php

View workflow job for this annotation

GitHub Actions / sca / Psalm

UndefinedGlobalVariable

src/MakerBundle/Templates/ChangePasswordFormType.tpl.php:7:18: UndefinedGlobalVariable: Cannot find referenced variable $class_name in global scope (see https://psalm.dev/127)
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('plainPassword', RepeatedType::class, [
'type' => PasswordType::class,
'options' => [
'attr' => [
'autocomplete' => 'new-password',
],
],
'first_options' => [
'constraints' => [
new NotBlank([
'message' => 'Please enter a password',
]),
new Length([
'min' => 6,
'minMessage' => 'Your password should be at least {{ limit }} characters',
// max length allowed by Symfony for security reasons
'max' => 4096,
]),
],
'label' => 'New password',
],
'second_options' => [
'label' => 'Repeat Password',
],
'invalid_message' => 'The password fields must match.',
// Instead of being set onto the object directly,
// this is read and encoded in the controller
'mapped' => false,
])
;
}

public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([]);
}
}
160 changes: 160 additions & 0 deletions src/MakerBundle/Templates/ResetPasswordController.tpl.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
<?php echo "<?php\n"; ?>

namespace <?php echo $namespace; ?>;

Check failure on line 3 in src/MakerBundle/Templates/ResetPasswordController.tpl.php

View workflow job for this annotation

GitHub Actions / sca / Psalm

UndefinedGlobalVariable

src/MakerBundle/Templates/ResetPasswordController.tpl.php:3:22: UndefinedGlobalVariable: Cannot find referenced variable $namespace in global scope (see https://psalm.dev/127)

<?php echo $use_statements; ?>

Check failure on line 5 in src/MakerBundle/Templates/ResetPasswordController.tpl.php

View workflow job for this annotation

GitHub Actions / sca / Psalm

UndefinedGlobalVariable

src/MakerBundle/Templates/ResetPasswordController.tpl.php:5:12: UndefinedGlobalVariable: Cannot find referenced variable $use_statements in global scope (see https://psalm.dev/127)

#[Route('/reset-password')]
class <?php echo $class_name; ?> extends AbstractController

Check failure on line 8 in src/MakerBundle/Templates/ResetPasswordController.tpl.php

View workflow job for this annotation

GitHub Actions / sca / Psalm

UndefinedGlobalVariable

src/MakerBundle/Templates/ResetPasswordController.tpl.php:8:18: UndefinedGlobalVariable: Cannot find referenced variable $class_name in global scope (see https://psalm.dev/127)
{
use ResetPasswordControllerTrait;

public function __construct(
private ResetPasswordHelperInterface $resetPasswordHelper,
private EntityManagerInterface $entityManager
) {
}

/**
* Display & process form to request a password reset.
*/
#[Route('', name: 'app_forgot_password_request')]
public function request(Request $request, MailerInterface $mailer<?php if ($translator_available) { ?>, TranslatorInterface $translator<?php } ?>): Response

Check failure on line 22 in src/MakerBundle/Templates/ResetPasswordController.tpl.php

View workflow job for this annotation

GitHub Actions / sca / Psalm

UndefinedGlobalVariable

src/MakerBundle/Templates/ResetPasswordController.tpl.php:22:80: UndefinedGlobalVariable: Cannot find referenced variable $translator_available in global scope (see https://psalm.dev/127)
{
$form = $this->createForm(<?php echo $request_form_type_class_name; ?>::class);

Check failure on line 24 in src/MakerBundle/Templates/ResetPasswordController.tpl.php

View workflow job for this annotation

GitHub Actions / sca / Psalm

UndefinedGlobalVariable

src/MakerBundle/Templates/ResetPasswordController.tpl.php:24:46: UndefinedGlobalVariable: Cannot find referenced variable $request_form_type_class_name in global scope (see https://psalm.dev/127)
$form->handleRequest($request);

if ($form->isSubmitted() && $form->isValid()) {
return $this->processSendingPasswordResetEmail(
$form->get('<?php echo $email_field; ?>')->getData(),

Check failure on line 29 in src/MakerBundle/Templates/ResetPasswordController.tpl.php

View workflow job for this annotation

GitHub Actions / sca / Psalm

UndefinedGlobalVariable

src/MakerBundle/Templates/ResetPasswordController.tpl.php:29:40: UndefinedGlobalVariable: Cannot find referenced variable $email_field in global scope (see https://psalm.dev/127)
$mailer<?php if ($translator_available) { ?>,
$translator<?php } ?><?php echo "\n"; ?>
);
}

return $this->render('reset_password/request.html.twig', [
'requestForm' => $form->createView(),
]);
}

/**
* Confirmation page after a user has requested a password reset.
*/
#[Route('/check-email', name: 'app_check_email')]
public function checkEmail(): Response
{
// Generate a fake token if the user does not exist or someone hit this page directly.
// This prevents exposing whether or not a user was found with the given email address or not
if (null === ($resetToken = $this->getTokenObjectFromSession())) {
$resetToken = $this->resetPasswordHelper->generateFakeResetToken();
}

return $this->render('reset_password/check_email.html.twig', [
'resetToken' => $resetToken,
]);
}

/**
* Validates and process the reset URL that the user clicked in their email.
*/
#[Route('/reset/{token}', name: 'app_reset_password')]
public function reset(Request $request, UserPasswordHasherInterface $passwordHasher<?php if ($translator_available) { ?>, TranslatorInterface $translator<?php } ?>, string $token = null): Response
{
if ($token) {
// We store the token in session and remove it from the URL, to avoid the URL being
// loaded in a browser and potentially leaking the token to 3rd party JavaScript.
$this->storeTokenInSession($token);

return $this->redirectToRoute('app_reset_password');
}

$token = $this->getTokenFromSession();
if (null === $token) {
throw $this->createNotFoundException('No reset password token found in the URL or in the session.');
}

try {
$user = $this->resetPasswordHelper->validateTokenAndFetchUser($token);
} catch (ResetPasswordExceptionInterface $e) {
$this->addFlash('reset_password_error', sprintf(
'%s - %s',
<?php if ($translator_available) { ?>$translator->trans(<?php echo $problem_validate_message_or_constant; ?>, [], 'ResetPasswordBundle')<?php } else { ?><?php echo $problem_validate_message_or_constant; ?><?php } ?>,

Check failure on line 81 in src/MakerBundle/Templates/ResetPasswordController.tpl.php

View workflow job for this annotation

GitHub Actions / sca / Psalm

UndefinedGlobalVariable

src/MakerBundle/Templates/ResetPasswordController.tpl.php:81:84: UndefinedGlobalVariable: Cannot find referenced variable $problem_validate_message_or_constant in global scope (see https://psalm.dev/127)
<?php if ($translator_available) { ?>$translator->trans($e->getReason(), [], 'ResetPasswordBundle')<?php } else { ?>$e->getReason()<?php } ?><?php echo "\n"; ?>
));

return $this->redirectToRoute('app_forgot_password_request');
}

// The token is valid; allow the user to change their password.
$form = $this->createForm(<?php echo $reset_form_type_class_name; ?>::class);
$form->handleRequest($request);

if ($form->isSubmitted() && $form->isValid()) {
// A password reset token should be used only once, remove it.
$this->resetPasswordHelper->removeResetRequest($token);

// Encode(hash) the plain password, and set it.
$encodedPassword = $passwordHasher->hashPassword(
$user,
$form->get('plainPassword')->getData()
);

$user-><?php echo $password_setter; ?>($encodedPassword);
$this->entityManager->flush();

// The session is cleaned up after the password has been changed.
$this->cleanSessionAfterReset();

return $this->redirectToRoute('<?php echo $success_redirect_route; ?>');
}

return $this->render('reset_password/reset.html.twig', [
'resetForm' => $form->createView(),
]);
}

private function processSendingPasswordResetEmail(string $emailFormData, MailerInterface $mailer<?php if ($translator_available) { ?>, TranslatorInterface $translator<?php } ?>): RedirectResponse
{
$user = $this->entityManager->getRepository(<?php echo $user_class_name; ?>::class)->findOneBy([
'<?php echo $email_field; ?>' => $emailFormData,
]);

// Do not reveal whether a user account was found or not.
if (!$user) {
return $this->redirectToRoute('app_check_email');
}

try {
$resetToken = $this->resetPasswordHelper->generateResetToken($user);
} catch (ResetPasswordExceptionInterface $e) {
// If you want to tell the user why a reset email was not sent, uncomment
// the lines below and change the redirect to 'app_forgot_password_request'.
// Caution: This may reveal if a user is registered or not.
//
// $this->addFlash('reset_password_error', sprintf(
// '%s - %s',
// <?php if ($translator_available) { ?>$translator->trans(<?php echo $problem_handle_message_or_constant; ?>, [], 'ResetPasswordBundle')<?php } else { ?><?php echo $problem_handle_message_or_constant; ?><?php } ?>,
// <?php if ($translator_available) { ?>$translator->trans($e->getReason(), [], 'ResetPasswordBundle')<?php } else { ?>$e->getReason()<?php } ?><?php echo "\n"; ?>
// ));

return $this->redirectToRoute('app_check_email');
}

$email = (new TemplatedEmail())
->from(new Address('<?php echo $from_email; ?>', '<?php echo $from_email_name; ?>'))
->to($user-><?php echo $email_getter; ?>())
->subject('Your password reset request')
->htmlTemplate('reset_password/email.html.twig')
->context([
'resetToken' => $resetToken,
])
;

$mailer->send($email);

// Store the token object in session for retrieval in check-email route.
$this->setTokenObjectInSession($resetToken);

return $this->redirectToRoute('app_check_email');
}
}
36 changes: 36 additions & 0 deletions src/MakerBundle/Templates/ResetPasswordRequest.tpl.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php echo "<?php\n"; ?>

namespace <?php echo $namespace; ?>;

<?php echo $use_statements; ?>

#[ORM\Entity(repositoryClass: ResetPasswordRequestRepository::class)]
class ResetPasswordRequest implements ResetPasswordRequestInterface
{
use ResetPasswordRequestTrait;

#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;

#[ORM\ManyToOne]
#[ORM\JoinColumn(nullable: false)]
private ?<?= user_short_class ?> $user = null;

public function __construct(<?= user_short_class ?> $user, \DateTimeInterface $expiresAt, string $selector, string $hashedToken)
{
$this->user = $user;
$this->initialize($expiresAt, $selector, $hashedToken);
}

public function getId(): ?int
{
return $this->id;
}

public function getUser(): <?= user_short_class ?>
{
return $this->user;
}
}
27 changes: 27 additions & 0 deletions src/MakerBundle/Templates/ResetPasswordRequestFormType.tpl.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php echo "<?php\n"; ?>

namespace <?php echo $namespace; ?>;

<?php echo $use_statements; ?>

class <?php echo $class_name; ?> extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('<?php echo $email_field; ?>', EmailType::class, [
'attr' => ['autocomplete' => 'email'],
'constraints' => [
new NotBlank([
'message' => 'Please enter your email',
]),
],
])
;
}

public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([]);
}
}
11 changes: 11 additions & 0 deletions src/MakerBundle/Templates/twig_check_email.tpl.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{% extends 'base.html.twig' %}

{% block title %}Password Reset Email Sent{% endblock %}

{% block body %}
<p>
If an account matching your email exists, then an email was just sent that contains a link that you can use to reset your password.
This link will expire in {{ resetToken.expirationMessageKey|trans(resetToken.expirationMessageData, 'ResetPasswordBundle') }}.
</p>
<p>If you don't receive an email please check your spam folder or <a href="{{ path('app_forgot_password_request') }}">try again</a>.</p>
{% endblock %}
9 changes: 9 additions & 0 deletions src/MakerBundle/Templates/twig_email.tpl.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<h1>Hi!</h1>

<p>To reset your password, please visit the following link</p>

<a href="{{ url('app_reset_password', {token: resetToken.token}) }}">{{ url('app_reset_password', {token: resetToken.token}) }}</a>

<p>This link will expire in {{ resetToken.expirationMessageKey|trans(resetToken.expirationMessageData, 'ResetPasswordBundle') }}.</p>

<p>Cheers!</p>
Loading
Loading