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