-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
16 changed files
with
567 additions
and
225 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,2 @@ | ||
symfonycasts_reset_password: | ||
# Replace symfonycasts.reset_password.fake_request_repository with the full | ||
# namespace of the password reset request repository after it has been created. | ||
# i.e. App\Repository\ResetPasswordRequestRepository | ||
request_password_repository: symfonycasts.reset_password.fake_request_repository | ||
request_password_repository: App\Repository\ResetPasswordRequestRepository |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace DoctrineMigrations; | ||
|
||
use Doctrine\DBAL\Schema\Schema; | ||
use Doctrine\Migrations\AbstractMigration; | ||
|
||
final class Version20210308102817 extends AbstractMigration | ||
{ | ||
|
||
public function getDescription(): string | ||
{ | ||
return 'Add password reset requests.'; | ||
} | ||
|
||
public function up(Schema $schema): void | ||
{ | ||
$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' | ||
); | ||
$this->addSql( | ||
'ALTER TABLE reset_password_request | ||
ADD CONSTRAINT FK_7CE748AA76ED395 FOREIGN KEY (user_id) REFERENCES user (id)' | ||
); | ||
} | ||
|
||
public function down(Schema $schema): void | ||
{ | ||
$this->addSql('DROP TABLE reset_password_request'); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,148 @@ | ||
<?php | ||
|
||
namespace App\Controller; | ||
|
||
use App\Entity\User; | ||
use App\Repository\UserRepository; | ||
use App\Settings; | ||
use Exception; | ||
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\Routing\Annotation\Route; | ||
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface; | ||
use SymfonyCasts\Bundle\ResetPassword\Controller\ResetPasswordControllerTrait; | ||
use SymfonyCasts\Bundle\ResetPassword\Exception\ResetPasswordExceptionInterface; | ||
use SymfonyCasts\Bundle\ResetPassword\ResetPasswordHelperInterface; | ||
|
||
class ResetPasswordController extends AbstractController | ||
{ | ||
use ResetPasswordControllerTrait; | ||
|
||
private $resetPasswordHelper; | ||
|
||
public function __construct(ResetPasswordHelperInterface $resetPasswordHelper) | ||
{ | ||
$this->resetPasswordHelper = $resetPasswordHelper; | ||
} | ||
|
||
/** | ||
* @Route("/reset", name="reset") | ||
*/ | ||
public function request( | ||
Request $request, | ||
MailerInterface $mailer, | ||
UserRepository $userRepository, | ||
Settings $settings | ||
): Response { | ||
// For GET requests, display the 'request' button. | ||
if (!$request->isMethod('POST')) { | ||
return $this->render('reset_password/request.html.twig', []); | ||
} | ||
// Otherwise, send the request confirmation email. | ||
// Check the CSRF token. | ||
if (!$this->isCsrfTokenValid('reset_request', $request->get('csrf_token'))) { | ||
throw new Exception('Invalid CSRF token'); | ||
} | ||
// Find the user. | ||
$user = $userRepository->findOneBy(['username' => $request->get('username')]); | ||
if (!$user) { | ||
// Pretend, if there's no user with this username. | ||
return $this->redirectToRoute('reset_check'); | ||
} | ||
try { | ||
$resetToken = $this->resetPasswordHelper->generateResetToken($user); | ||
} catch (ResetPasswordExceptionInterface $e) { | ||
// Pretend, if anything went wrong. | ||
return $this->redirectToRoute('reset_check'); | ||
} | ||
// Send the email. | ||
$email = (new TemplatedEmail()) | ||
->from(new Address($settings->getMailFrom())) | ||
->to($user->getEmail()) | ||
->subject('[' . $settings->siteName() . '] Password reset request') | ||
->htmlTemplate('reset_password/email.html.twig') | ||
->context(['resetToken' => $resetToken]); | ||
$mailer->send($email); | ||
// Store the token for later checking. | ||
$this->setTokenObjectInSession($resetToken); | ||
// Show a 'check email' confirmation message. | ||
return $this->redirectToRoute('reset_check'); | ||
} | ||
|
||
/** | ||
* @Route("/reset-check", name="reset_check") | ||
*/ | ||
public function checkEmail(Request $request): Response | ||
{ | ||
$resetToken = $this->getTokenObjectFromSession(); | ||
if (!$resetToken) { | ||
return $this->redirectToRoute('reset'); | ||
} | ||
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/{token}", name="reset_token") | ||
*/ | ||
public function reset( | ||
Request $request, | ||
UserPasswordEncoderInterface $passwordEncoder, | ||
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('reset_token'); | ||
} | ||
|
||
$token = $this->getTokenFromSession(); | ||
if (null === $token) { | ||
throw $this->createNotFoundException('No reset password token found in the URL or in the session.'); | ||
} | ||
|
||
try { | ||
/** @var User $user */ | ||
$user = $this->resetPasswordHelper->validateTokenAndFetchUser($token); | ||
} catch (ResetPasswordExceptionInterface $e) { | ||
$this->addFlash('error', $e->getReason()); | ||
return $this->redirectToRoute('reset'); | ||
} | ||
|
||
if (!$request->isMethod('post')) { | ||
return $this->render('reset_password/reset.html.twig', []); | ||
} | ||
|
||
// Check the CSRF token. | ||
if (!$this->isCsrfTokenValid('reset_password', $request->get('csrf_token'))) { | ||
throw new Exception('Invalid token'); | ||
} | ||
|
||
$pass = $request->get('password'); | ||
$pass2 = $request->get('password_verification'); | ||
if (!$pass || !$pass2 || $pass !== $pass2) { | ||
$this->addFlash('error', 'Passwords do not match.'); | ||
return $this->redirectToRoute('reset_token'); | ||
} | ||
|
||
// A password reset token should be used only once, remove it. | ||
$this->resetPasswordHelper->removeResetRequest($token); | ||
|
||
// Encode the plain password, and set it. | ||
$user->setPassword($passwordEncoder->encodePassword($user, $pass)); | ||
$em = $this->getDoctrine()->getManager(); | ||
$em->persist($user); | ||
$em->flush(); | ||
$this->cleanSessionAfterReset(); | ||
$this->addFlash('success', 'Password changed. You can now log in with your new password.'); | ||
return $this->redirectToRoute('login'); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
<?php | ||
|
||
namespace App\Entity; | ||
|
||
use App\Repository\ResetPasswordRequestRepository; | ||
use DateTimeInterface; | ||
use Doctrine\ORM\Mapping as ORM; | ||
use SymfonyCasts\Bundle\ResetPassword\Model\ResetPasswordRequestInterface; | ||
use SymfonyCasts\Bundle\ResetPassword\Model\ResetPasswordRequestTrait; | ||
|
||
/** | ||
* @ORM\Entity(repositoryClass=ResetPasswordRequestRepository::class) | ||
*/ | ||
class ResetPasswordRequest implements ResetPasswordRequestInterface | ||
{ | ||
use ResetPasswordRequestTrait; | ||
|
||
/** | ||
* @ORM\Id | ||
* @ORM\GeneratedValue | ||
* @ORM\Column(type="integer") | ||
*/ | ||
private $id; | ||
|
||
/** | ||
* @ORM\ManyToOne(targetEntity=User::class) | ||
* @ORM\JoinColumn(nullable=false) | ||
*/ | ||
private $user; | ||
|
||
public function __construct(object $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(): object | ||
{ | ||
return $this->user; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
<?php | ||
|
||
namespace App\Repository; | ||
|
||
use App\Entity\ResetPasswordRequest; | ||
use DateTimeInterface; | ||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; | ||
use Doctrine\Persistence\ManagerRegistry; | ||
use SymfonyCasts\Bundle\ResetPassword\Model\ResetPasswordRequestInterface; | ||
use SymfonyCasts\Bundle\ResetPassword\Persistence\Repository\ResetPasswordRequestRepositoryTrait; | ||
use SymfonyCasts\Bundle\ResetPassword\Persistence\ResetPasswordRequestRepositoryInterface; | ||
|
||
/** | ||
* @method ResetPasswordRequest|null find($id, $lockMode = null, $lockVersion = null) | ||
* @method ResetPasswordRequest|null findOneBy(array $criteria, array $orderBy = null) | ||
* @method ResetPasswordRequest[] findAll() | ||
* @method ResetPasswordRequest[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) | ||
*/ | ||
class ResetPasswordRequestRepository extends ServiceEntityRepository implements ResetPasswordRequestRepositoryInterface | ||
{ | ||
use ResetPasswordRequestRepositoryTrait; | ||
|
||
public function __construct(ManagerRegistry $registry) | ||
{ | ||
parent::__construct($registry, ResetPasswordRequest::class); | ||
} | ||
|
||
public function createResetPasswordRequest( | ||
object $user, | ||
DateTimeInterface $expiresAt, | ||
string $selector, | ||
string $hashedToken | ||
): ResetPasswordRequestInterface { | ||
return new ResetPasswordRequest($user, $expiresAt, $selector, $hashedToken); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
{% extends 'base.html.twig' %} | ||
|
||
{% block title %}Reminder{% endblock %} | ||
|
||
{% block body %} | ||
|
||
<h1>Check your email</h1> | ||
|
||
<p> | ||
If your details are correct, | ||
you will receive an email | ||
containing a URL at which to reset your password. | ||
</p> | ||
|
||
<p> | ||
If you don't receive an email please | ||
check your spam folder, | ||
<a href="{{ path('reset') }}">try again</a>, | ||
or contact a site administrator. | ||
</p> | ||
|
||
{% endblock %} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
<p>A request was received to reset your password on <em>{{ settings.siteName }}</em>.</p> | ||
|
||
<p>If this was not you, please disregard this email; there is no need to take any action.</p> | ||
|
||
<p>To reset your password, please visit the following link:</p> | ||
|
||
<p><a href="{{ url('reset_token', {token: resetToken.token}) }}">{{ url('reset_token', {token: resetToken.token}) }}</a></p> |
Oops, something went wrong.