Skip to content

Commit fef2582

Browse files
committed
Reset Password files added.
1 parent b009079 commit fef2582

File tree

12 files changed

+421
-4
lines changed

12 files changed

+421
-4
lines changed

config/packages/reset_password.yaml

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,2 @@
11
symfonycasts_reset_password:
2-
# Replace symfonycasts.reset_password.fake_request_repository with the full
3-
# namespace of the password reset request repository after it has been created.
4-
# i.e. App\Repository\ResetPasswordRequestRepository
5-
request_password_repository: symfonycasts.reset_password.fake_request_repository
2+
request_password_repository: App\Repository\ResetPasswordRequestRepository

migrations/Version20200727090737.php

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace DoctrineMigrations;
6+
7+
use Doctrine\DBAL\Schema\Schema;
8+
use Doctrine\Migrations\AbstractMigration;
9+
10+
/**
11+
* Auto-generated Migration: Please modify to your needs!
12+
*/
13+
final class Version20200727090737 extends AbstractMigration
14+
{
15+
public function getDescription() : string
16+
{
17+
return '';
18+
}
19+
20+
public function up(Schema $schema) : void
21+
{
22+
// this up() migration is auto-generated, please modify it to your needs
23+
$this->addSql('CREATE TABLE reset_password_request (id INT AUTO_INCREMENT NOT NULL, user_id INT NOT NULL, selector VARCHAR(20) NOT NULL, hashed_token VARCHAR(100) NOT NULL, requested_at DATETIME NOT NULL COMMENT \'(DC2Type:datetime_immutable)\', expires_at DATETIME NOT NULL COMMENT \'(DC2Type:datetime_immutable)\', INDEX IDX_7CE748AA76ED395 (user_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB');
24+
$this->addSql('ALTER TABLE reset_password_request ADD CONSTRAINT FK_7CE748AA76ED395 FOREIGN KEY (user_id) REFERENCES user (id)');
25+
$this->addSql('CREATE UNIQUE INDEX UNIQ_8D93D649E7927C74 ON user (email)');
26+
}
27+
28+
public function down(Schema $schema) : void
29+
{
30+
// this down() migration is auto-generated, please modify it to your needs
31+
$this->addSql('DROP TABLE reset_password_request');
32+
$this->addSql('DROP INDEX UNIQ_8D93D649E7927C74 ON user');
33+
}
34+
}
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
<?php
2+
3+
namespace App\Controller;
4+
5+
use App\Entity\User;
6+
use App\Form\ChangePasswordFormType;
7+
use App\Form\ResetPasswordRequestFormType;
8+
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
9+
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
10+
use Symfony\Component\HttpFoundation\RedirectResponse;
11+
use Symfony\Component\HttpFoundation\Request;
12+
use Symfony\Component\HttpFoundation\Response;
13+
use Symfony\Component\Mailer\MailerInterface;
14+
use Symfony\Component\Mime\Address;
15+
use Symfony\Component\Routing\Annotation\Route;
16+
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
17+
use SymfonyCasts\Bundle\ResetPassword\Controller\ResetPasswordControllerTrait;
18+
use SymfonyCasts\Bundle\ResetPassword\Exception\ResetPasswordExceptionInterface;
19+
use SymfonyCasts\Bundle\ResetPassword\ResetPasswordHelperInterface;
20+
21+
/**
22+
* @Route("/reset-password")
23+
*/
24+
class ResetPasswordController extends AbstractController
25+
{
26+
use ResetPasswordControllerTrait;
27+
28+
private $resetPasswordHelper;
29+
30+
public function __construct(ResetPasswordHelperInterface $resetPasswordHelper)
31+
{
32+
$this->resetPasswordHelper = $resetPasswordHelper;
33+
}
34+
35+
/**
36+
* Display & process form to request a password reset.
37+
*
38+
* @Route("", name="app_forgot_password_request")
39+
*/
40+
public function request(Request $request, MailerInterface $mailer): Response
41+
{
42+
$form = $this->createForm(ResetPasswordRequestFormType::class);
43+
$form->handleRequest($request);
44+
45+
if ($form->isSubmitted() && $form->isValid()) {
46+
return $this->processSendingPasswordResetEmail(
47+
$form->get('email')->getData(),
48+
$mailer
49+
);
50+
}
51+
52+
return $this->render('reset_password/request.html.twig', [
53+
'requestForm' => $form->createView(),
54+
]);
55+
}
56+
57+
/**
58+
* Confirmation page after a user has requested a password reset.
59+
*
60+
* @Route("/check-email", name="app_check_email")
61+
*/
62+
public function checkEmail(): Response
63+
{
64+
// We prevent users from directly accessing this page
65+
if (!$this->canCheckEmail()) {
66+
return $this->redirectToRoute('app_forgot_password_request');
67+
}
68+
69+
return $this->render('reset_password/check_email.html.twig', [
70+
'tokenLifetime' => $this->resetPasswordHelper->getTokenLifetime(),
71+
]);
72+
}
73+
74+
/**
75+
* Validates and process the reset URL that the user clicked in their email.
76+
*
77+
* @Route("/reset/{token}", name="app_reset_password")
78+
*/
79+
public function reset(Request $request, UserPasswordEncoderInterface $passwordEncoder, string $token = null): Response
80+
{
81+
if ($token) {
82+
// We store the token in session and remove it from the URL, to avoid the URL being
83+
// loaded in a browser and potentially leaking the token to 3rd party JavaScript.
84+
$this->storeTokenInSession($token);
85+
86+
return $this->redirectToRoute('app_reset_password');
87+
}
88+
89+
$token = $this->getTokenFromSession();
90+
if (null === $token) {
91+
throw $this->createNotFoundException('No reset password token found in the URL or in the session.');
92+
}
93+
94+
try {
95+
$user = $this->resetPasswordHelper->validateTokenAndFetchUser($token);
96+
} catch (ResetPasswordExceptionInterface $e) {
97+
$this->addFlash('reset_password_error', sprintf(
98+
'There was a problem validating your reset request - %s',
99+
$e->getReason()
100+
));
101+
102+
return $this->redirectToRoute('app_forgot_password_request');
103+
}
104+
105+
// The token is valid; allow the user to change their password.
106+
$form = $this->createForm(ChangePasswordFormType::class);
107+
$form->handleRequest($request);
108+
109+
if ($form->isSubmitted() && $form->isValid()) {
110+
// A password reset token should be used only once, remove it.
111+
$this->resetPasswordHelper->removeResetRequest($token);
112+
113+
// Encode the plain password, and set it.
114+
$encodedPassword = $passwordEncoder->encodePassword(
115+
$user,
116+
$form->get('plainPassword')->getData()
117+
);
118+
119+
$user->setPassword($encodedPassword);
120+
$this->getDoctrine()->getManager()->flush();
121+
122+
// The session is cleaned up after the password has been changed.
123+
$this->cleanSessionAfterReset();
124+
125+
return $this->redirectToRoute('home');
126+
}
127+
128+
return $this->render('reset_password/reset.html.twig', [
129+
'resetForm' => $form->createView(),
130+
]);
131+
}
132+
133+
private function processSendingPasswordResetEmail(string $emailFormData, MailerInterface $mailer): RedirectResponse
134+
{
135+
$user = $this->getDoctrine()->getRepository(User::class)->findOneBy([
136+
'email' => $emailFormData,
137+
]);
138+
139+
// Marks that you are allowed to see the app_check_email page.
140+
$this->setCanCheckEmailInSession();
141+
142+
// Do not reveal whether a user account was found or not.
143+
if (!$user) {
144+
return $this->redirectToRoute('app_check_email');
145+
}
146+
147+
try {
148+
$resetToken = $this->resetPasswordHelper->generateResetToken($user);
149+
} catch (ResetPasswordExceptionInterface $e) {
150+
// If you want to tell the user why a reset email was not sent, uncomment
151+
// the lines below and change the redirect to 'app_forgot_password_request'.
152+
// Caution: This may reveal if a user is registered or not.
153+
//
154+
// $this->addFlash('reset_password_error', sprintf(
155+
// 'There was a problem handling your password reset request - %s',
156+
// $e->getReason()
157+
// ));
158+
159+
return $this->redirectToRoute('app_check_email');
160+
}
161+
162+
$email = (new TemplatedEmail())
163+
->from(new Address('[email protected]', 'Symfony Auth App'))
164+
->to($user->getEmail())
165+
->subject('Your password reset request')
166+
->htmlTemplate('reset_password/email.html.twig')
167+
->context([
168+
'resetToken' => $resetToken,
169+
'tokenLifetime' => $this->resetPasswordHelper->getTokenLifetime(),
170+
])
171+
;
172+
173+
$mailer->send($email);
174+
175+
return $this->redirectToRoute('app_check_email');
176+
}
177+
}

src/Entity/ResetPasswordRequest.php

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<?php
2+
3+
namespace App\Entity;
4+
5+
use App\Repository\ResetPasswordRequestRepository;
6+
use Doctrine\ORM\Mapping as ORM;
7+
use SymfonyCasts\Bundle\ResetPassword\Model\ResetPasswordRequestInterface;
8+
use SymfonyCasts\Bundle\ResetPassword\Model\ResetPasswordRequestTrait;
9+
10+
/**
11+
* @ORM\Entity(repositoryClass=ResetPasswordRequestRepository::class)
12+
*/
13+
class ResetPasswordRequest implements ResetPasswordRequestInterface
14+
{
15+
use ResetPasswordRequestTrait;
16+
17+
/**
18+
* @ORM\Id()
19+
* @ORM\GeneratedValue()
20+
* @ORM\Column(type="integer")
21+
*/
22+
private $id;
23+
24+
/**
25+
* @ORM\ManyToOne(targetEntity=User::class)
26+
* @ORM\JoinColumn(nullable=false)
27+
*/
28+
private $user;
29+
30+
public function __construct(object $user, \DateTimeInterface $expiresAt, string $selector, string $hashedToken)
31+
{
32+
$this->user = $user;
33+
$this->initialize($expiresAt, $selector, $hashedToken);
34+
}
35+
36+
public function getId(): ?int
37+
{
38+
return $this->id;
39+
}
40+
41+
public function getUser(): object
42+
{
43+
return $this->user;
44+
}
45+
}

src/Form/ChangePasswordFormType.php

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<?php
2+
3+
namespace App\Form;
4+
5+
use Symfony\Component\Form\AbstractType;
6+
use Symfony\Component\Form\Extension\Core\Type\PasswordType;
7+
use Symfony\Component\Form\Extension\Core\Type\RepeatedType;
8+
use Symfony\Component\Form\FormBuilderInterface;
9+
use Symfony\Component\OptionsResolver\OptionsResolver;
10+
use Symfony\Component\Validator\Constraints\Length;
11+
use Symfony\Component\Validator\Constraints\NotBlank;
12+
13+
class ChangePasswordFormType extends AbstractType
14+
{
15+
public function buildForm(FormBuilderInterface $builder, array $options): void
16+
{
17+
$builder
18+
->add('plainPassword', RepeatedType::class, [
19+
'type' => PasswordType::class,
20+
'first_options' => [
21+
'constraints' => [
22+
new NotBlank([
23+
'message' => 'Please enter a password',
24+
]),
25+
new Length([
26+
'min' => 6,
27+
'minMessage' => 'Your password should be at least {{ limit }} characters',
28+
// max length allowed by Symfony for security reasons
29+
'max' => 4096,
30+
]),
31+
],
32+
'label' => 'New password',
33+
],
34+
'second_options' => [
35+
'label' => 'Repeat Password',
36+
],
37+
'invalid_message' => 'The password fields must match.',
38+
// Instead of being set onto the object directly,
39+
// this is read and encoded in the controller
40+
'mapped' => false,
41+
])
42+
;
43+
}
44+
45+
public function configureOptions(OptionsResolver $resolver): void
46+
{
47+
$resolver->setDefaults([]);
48+
}
49+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?php
2+
3+
namespace App\Form;
4+
5+
use Symfony\Component\Form\AbstractType;
6+
use Symfony\Component\Form\Extension\Core\Type\EmailType;
7+
use Symfony\Component\Form\FormBuilderInterface;
8+
use Symfony\Component\OptionsResolver\OptionsResolver;
9+
use Symfony\Component\Validator\Constraints\NotBlank;
10+
11+
class ResetPasswordRequestFormType extends AbstractType
12+
{
13+
public function buildForm(FormBuilderInterface $builder, array $options): void
14+
{
15+
$builder
16+
->add('email', EmailType::class, [
17+
'constraints' => [
18+
new NotBlank([
19+
'message' => 'Please enter your email',
20+
]),
21+
],
22+
])
23+
;
24+
}
25+
26+
public function configureOptions(OptionsResolver $resolver): void
27+
{
28+
$resolver->setDefaults([]);
29+
}
30+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php
2+
3+
namespace App\Repository;
4+
5+
use App\Entity\ResetPasswordRequest;
6+
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
7+
use Doctrine\Common\Persistence\ManagerRegistry;
8+
use SymfonyCasts\Bundle\ResetPassword\Model\ResetPasswordRequestInterface;
9+
use SymfonyCasts\Bundle\ResetPassword\Persistence\Repository\ResetPasswordRequestRepositoryTrait;
10+
use SymfonyCasts\Bundle\ResetPassword\Persistence\ResetPasswordRequestRepositoryInterface;
11+
12+
/**
13+
* @method ResetPasswordRequest|null find($id, $lockMode = null, $lockVersion = null)
14+
* @method ResetPasswordRequest|null findOneBy(array $criteria, array $orderBy = null)
15+
* @method ResetPasswordRequest[] findAll()
16+
* @method ResetPasswordRequest[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
17+
*/
18+
class ResetPasswordRequestRepository extends ServiceEntityRepository implements ResetPasswordRequestRepositoryInterface
19+
{
20+
use ResetPasswordRequestRepositoryTrait;
21+
22+
public function __construct(ManagerRegistry $registry)
23+
{
24+
parent::__construct($registry, ResetPasswordRequest::class);
25+
}
26+
27+
public function createResetPasswordRequest(object $user, \DateTimeInterface $expiresAt, string $selector, string $hashedToken): ResetPasswordRequestInterface
28+
{
29+
return new ResetPasswordRequest($user, $expiresAt, $selector, $hashedToken);
30+
}
31+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{% extends 'base.html.twig' %}
2+
3+
{% block title %}Password Reset Email Sent{% endblock %}
4+
5+
{% block body %}
6+
<p>An email has been sent that contains a link that you can click to reset your password. This link will expire in {{ tokenLifetime|date('g') }} hour(s).</p>
7+
<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>
8+
{% endblock %}

0 commit comments

Comments
 (0)