parent
b29eabbfd0
commit
d2f4c8425e
@ -0,0 +1,2 @@ |
||||
maker: |
||||
root_namespace: 'Chamilo\CoreBundle' |
@ -0,0 +1,2 @@ |
||||
symfonycasts_reset_password: |
||||
request_password_repository: Chamilo\CoreBundle\Repository\ResetPasswordRequestRepository |
@ -0,0 +1,173 @@ |
||||
<?php |
||||
|
||||
namespace Chamilo\CoreBundle\Controller; |
||||
|
||||
use Chamilo\CoreBundle\Entity\User; |
||||
use Chamilo\CoreBundle\Form\ChangePasswordFormType; |
||||
use Chamilo\CoreBundle\Form\ResetPasswordRequestFormType; |
||||
use Symfony\Bridge\Twig\Mime\TemplatedEmail; |
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; |
||||
use Symfony\Component\HttpFoundation\RedirectResponse; |
||||
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; |
||||
|
||||
/** |
||||
* @Route("/reset-password") |
||||
*/ |
||||
class ResetPasswordController extends AbstractController |
||||
{ |
||||
use ResetPasswordControllerTrait; |
||||
|
||||
private $resetPasswordHelper; |
||||
|
||||
public function __construct(ResetPasswordHelperInterface $resetPasswordHelper) |
||||
{ |
||||
$this->resetPasswordHelper = $resetPasswordHelper; |
||||
} |
||||
|
||||
/** |
||||
* Display & process form to request a password reset. |
||||
* |
||||
* @Route("", name="app_forgot_password_request") |
||||
*/ |
||||
public function request(Request $request, MailerInterface $mailer): Response |
||||
{ |
||||
$form = $this->createForm(ResetPasswordRequestFormType::class); |
||||
$form->handleRequest($request); |
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) { |
||||
return $this->processSendingPasswordResetEmail( |
||||
$form->get('email')->getData(), |
||||
$mailer |
||||
); |
||||
} |
||||
|
||||
return $this->render('@ChamiloCore/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 |
||||
{ |
||||
// We prevent users from directly accessing this page |
||||
if (!$this->canCheckEmail()) { |
||||
return $this->redirectToRoute('app_forgot_password_request'); |
||||
} |
||||
|
||||
return $this->render('@ChamiloCore/reset_password/check_email.html.twig', [ |
||||
'tokenLifetime' => $this->resetPasswordHelper->getTokenLifetime(), |
||||
]); |
||||
} |
||||
|
||||
/** |
||||
* 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, 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('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( |
||||
'There was a problem validating your reset request - %s', |
||||
$e->getReason() |
||||
)); |
||||
|
||||
return $this->redirectToRoute('app_forgot_password_request'); |
||||
} |
||||
|
||||
// The token is valid; allow the user to change their password. |
||||
$form = $this->createForm(ChangePasswordFormType::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 the plain password, and set it. |
||||
$encodedPassword = $passwordEncoder->encodePassword( |
||||
$user, |
||||
$form->get('plainPassword')->getData() |
||||
); |
||||
|
||||
$user->setPassword($encodedPassword); |
||||
$this->getDoctrine()->getManager()->flush(); |
||||
|
||||
// The session is cleaned up after the password has been changed. |
||||
$this->cleanSessionAfterReset(); |
||||
|
||||
return $this->redirectToRoute('home'); |
||||
} |
||||
|
||||
return $this->render('@ChamiloCore/reset_password/reset.html.twig', [ |
||||
'resetForm' => $form->createView(), |
||||
]); |
||||
} |
||||
|
||||
private function processSendingPasswordResetEmail(string $emailFormData, MailerInterface $mailer): RedirectResponse |
||||
{ |
||||
$user = $this->getDoctrine()->getRepository(User::class)->findOneBy([ |
||||
'email' => $emailFormData, |
||||
]); |
||||
|
||||
// Marks that you are allowed to see the app_check_email page. |
||||
$this->setCanCheckEmailInSession(); |
||||
|
||||
// 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) { |
||||
$this->addFlash('reset_password_error', sprintf( |
||||
'There was a problem handling your password reset request - %s', |
||||
$e->getReason() |
||||
)); |
||||
|
||||
return $this->redirectToRoute('app_forgot_password_request'); |
||||
} |
||||
|
||||
$email = (new TemplatedEmail()) |
||||
->from(new Address('test@test.com', 'test')) |
||||
->to($user->getEmail()) |
||||
->subject('Your password reset request') |
||||
->htmlTemplate('@ChamiloCore/reset_password/email.html.twig') |
||||
->context([ |
||||
'resetToken' => $resetToken, |
||||
'tokenLifetime' => $this->resetPasswordHelper->getTokenLifetime(), |
||||
]) |
||||
; |
||||
|
||||
$mailer->send($email); |
||||
|
||||
return $this->redirectToRoute('app_check_email'); |
||||
} |
||||
} |
@ -0,0 +1,38 @@ |
||||
<?php |
||||
|
||||
namespace Chamilo\CoreBundle\Entity; |
||||
|
||||
use Doctrine\ORM\Mapping as ORM; |
||||
use SymfonyCasts\Bundle\ResetPassword\Model\ResetPasswordRequestInterface; |
||||
use SymfonyCasts\Bundle\ResetPassword\Model\ResetPasswordRequestTrait; |
||||
|
||||
/** |
||||
* @ORM\Entity(repositoryClass="Chamilo\CoreBundle\Repository\ResetPasswordRequestRepository") |
||||
*/ |
||||
class ResetPasswordRequest implements ResetPasswordRequestInterface |
||||
{ |
||||
use ResetPasswordRequestTrait; |
||||
|
||||
/** |
||||
* @ORM\Id() |
||||
* @ORM\GeneratedValue() |
||||
* @ORM\Column(type="integer") |
||||
*/ |
||||
private $id; |
||||
|
||||
/** |
||||
* @ORM\ManyToOne(targetEntity="Chamilo\CoreBundle\Entity\User") |
||||
*/ |
||||
private $user; |
||||
|
||||
public function __construct(object $user, \DateTimeInterface $expiresAt, string $selector, string $hashedToken) |
||||
{ |
||||
$this->user = $user; |
||||
$this->initialize($expiresAt, $selector, $hashedToken); |
||||
} |
||||
|
||||
public function getUser(): object |
||||
{ |
||||
return $this->user; |
||||
} |
||||
} |
@ -0,0 +1,49 @@ |
||||
<?php |
||||
|
||||
namespace Chamilo\CoreBundle\Form; |
||||
|
||||
use Symfony\Component\Form\AbstractType; |
||||
use Symfony\Component\Form\Extension\Core\Type\PasswordType; |
||||
use Symfony\Component\Form\Extension\Core\Type\RepeatedType; |
||||
use Symfony\Component\Form\FormBuilderInterface; |
||||
use Symfony\Component\OptionsResolver\OptionsResolver; |
||||
use Symfony\Component\Validator\Constraints\Length; |
||||
use Symfony\Component\Validator\Constraints\NotBlank; |
||||
|
||||
class ChangePasswordFormType extends AbstractType |
||||
{ |
||||
public function buildForm(FormBuilderInterface $builder, array $options): void |
||||
{ |
||||
$builder |
||||
->add('plainPassword', RepeatedType::class, [ |
||||
'type' => PasswordType::class, |
||||
'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([]); |
||||
} |
||||
} |
@ -0,0 +1,30 @@ |
||||
<?php |
||||
|
||||
namespace Chamilo\CoreBundle\Form; |
||||
|
||||
use Symfony\Component\Form\AbstractType; |
||||
use Symfony\Component\Form\Extension\Core\Type\EmailType; |
||||
use Symfony\Component\Form\FormBuilderInterface; |
||||
use Symfony\Component\OptionsResolver\OptionsResolver; |
||||
use Symfony\Component\Validator\Constraints\NotBlank; |
||||
|
||||
class ResetPasswordRequestFormType extends AbstractType |
||||
{ |
||||
public function buildForm(FormBuilderInterface $builder, array $options): void |
||||
{ |
||||
$builder |
||||
->add('email', EmailType::class, [ |
||||
'constraints' => [ |
||||
new NotBlank([ |
||||
'message' => 'Please enter your email', |
||||
]), |
||||
], |
||||
]) |
||||
; |
||||
} |
||||
|
||||
public function configureOptions(OptionsResolver $resolver): void |
||||
{ |
||||
$resolver->setDefaults([]); |
||||
} |
||||
} |
@ -0,0 +1,30 @@ |
||||
<?php |
||||
|
||||
|
||||
namespace Chamilo\CoreBundle\Migrations\Schema\V200; |
||||
|
||||
use Chamilo\CoreBundle\Migrations\AbstractMigrationChamilo; |
||||
use Doctrine\DBAL\Schema\Schema; |
||||
use Doctrine\Migrations\AbstractMigration; |
||||
|
||||
/** |
||||
* Auto-generated Migration: Please modify to your needs! |
||||
*/ |
||||
final class Version20200505064121 extends AbstractMigrationChamilo |
||||
{ |
||||
public function getDescription() : string |
||||
{ |
||||
return ''; |
||||
} |
||||
|
||||
public function up(Schema $schema) : void |
||||
{ |
||||
$this->addSql('CREATE TABLE reset_password_request (id INT AUTO_INCREMENT NOT NULL, user_id INT DEFAULT 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 ROW_FORMAT = DYNAMIC'); |
||||
$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'); |
||||
} |
||||
} |
@ -0,0 +1,40 @@ |
||||
<?php |
||||
|
||||
namespace Chamilo\CoreBundle\Repository; |
||||
|
||||
use Chamilo\CoreBundle\Entity\ResetPasswordRequest; |
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; |
||||
use Doctrine\Common\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 |
||||
); |
||||
} |
||||
} |
@ -0,0 +1,8 @@ |
||||
{% extends 'base.html.twig' %} |
||||
|
||||
{% block title %}Password Reset Email Sent{% endblock %} |
||||
|
||||
{% block body %} |
||||
<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> |
||||
<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 %} |
@ -0,0 +1,11 @@ |
||||
<h1>Hi!</h1> |
||||
|
||||
<p> |
||||
To reset your password, please visit |
||||
<a href="{{ url('app_reset_password', {token: resetToken.token}) }}">here</a> |
||||
This link will expire in {{ tokenLifetime|date('g') }} hour(s).. |
||||
</p> |
||||
|
||||
<p> |
||||
Cheers! |
||||
</p> |
@ -0,0 +1,22 @@ |
||||
{% extends 'base.html.twig' %} |
||||
|
||||
{% block title %}Reset your password{% endblock %} |
||||
|
||||
{% block body %} |
||||
{% for flashError in app.flashes('reset_password_error') %} |
||||
<div class="alert alert-danger" role="alert">{{ flashError }}</div> |
||||
{% endfor %} |
||||
<h1>Reset your password</h1> |
||||
|
||||
{{ form_start(requestForm) }} |
||||
{{ form_row(requestForm.email) }} |
||||
<div> |
||||
<small> |
||||
Enter your email address and we we will send you a |
||||
link to reset your password. |
||||
</small> |
||||
</div> |
||||
|
||||
<button class="btn btn-primary">Send password reset email</button> |
||||
{{ form_end(requestForm) }} |
||||
{% endblock %} |
@ -0,0 +1,12 @@ |
||||
{% extends 'base.html.twig' %} |
||||
|
||||
{% block title %}Reset your password{% endblock %} |
||||
|
||||
{% block body %} |
||||
<h1>Reset your password</h1> |
||||
|
||||
{{ form_start(resetForm) }} |
||||
{{ form_row(resetForm.plainPassword) }} |
||||
<button class="btn btn-primary">Reset password</button> |
||||
{{ form_end(resetForm) }} |
||||
{% endblock %} |
Loading…
Reference in new issue