Add reset password form

pull/3262/head
Julio Montoya 5 years ago
parent b29eabbfd0
commit d2f4c8425e
  1. 4
      .env
  2. 41
      composer.json
  3. 2
      config/packages/dev/maker.yaml
  4. 2
      config/packages/reset_password.yaml
  5. 5
      config/routes.yaml
  6. 173
      src/CoreBundle/Controller/ResetPasswordController.php
  7. 38
      src/CoreBundle/Entity/ResetPasswordRequest.php
  8. 49
      src/CoreBundle/Form/ChangePasswordFormType.php
  9. 30
      src/CoreBundle/Form/ResetPasswordRequestFormType.php
  10. 30
      src/CoreBundle/Migrations/Schema/V200/Version20200505064121.php
  11. 40
      src/CoreBundle/Repository/ResetPasswordRequestRepository.php
  12. 2
      src/CoreBundle/Resources/views/Layout/footer.html.twig
  13. 2
      src/CoreBundle/Resources/views/Layout/head.html.twig
  14. 8
      src/CoreBundle/Resources/views/reset_password/check_email.html.twig
  15. 11
      src/CoreBundle/Resources/views/reset_password/email.html.twig
  16. 22
      src/CoreBundle/Resources/views/reset_password/request.html.twig
  17. 12
      src/CoreBundle/Resources/views/reset_password/reset.html.twig

@ -31,3 +31,7 @@ APP_MULTIPLE_ACCESS_URL=''
###> symfony/mailer ### ###> symfony/mailer ###
MAILER_DSN=sendmail://localhost MAILER_DSN=sendmail://localhost
###< symfony/mailer ### ###< symfony/mailer ###
###> nelmio/cors-bundle ###
CORS_ALLOW_ORIGIN=^https?://(localhost|127\.0\.0\.1)(:[0-9]+)?$
###< nelmio/cors-bundle ###

@ -48,6 +48,22 @@
} }
], ],
"require": { "require": {
"php": "^7.3",
"ext-bcmath": "*",
"ext-curl": "*",
"ext-dom": "*",
"ext-exif": "*",
"ext-fileinfo": "*",
"ext-gd": "*",
"ext-intl": "*",
"ext-json": "*",
"ext-libxml": "*",
"ext-mbstring": "*",
"ext-soap": "*",
"ext-xml": "*",
"ext-xmlreader": "*",
"ext-zip": "*",
"ext-zlib": "*",
"a2lix/translation-form-bundle": "^3.0", "a2lix/translation-form-bundle": "^3.0",
"antishov/doctrine-extensions-bundle": "~1.4", "antishov/doctrine-extensions-bundle": "~1.4",
"api-platform/api-pack": "^1.2", "api-platform/api-pack": "^1.2",
@ -66,21 +82,6 @@
"endroid/qr-code": "~3.7", "endroid/qr-code": "~3.7",
"erusev/parsedown": "~1.7", "erusev/parsedown": "~1.7",
"essence/essence": "~3.0", "essence/essence": "~3.0",
"ext-bcmath": "*",
"ext-curl": "*",
"ext-dom": "*",
"ext-exif": "*",
"ext-fileinfo": "*",
"ext-gd": "*",
"ext-intl": "*",
"ext-json": "*",
"ext-libxml": "*",
"ext-mbstring": "*",
"ext-soap" : "*",
"ext-xml": "*",
"ext-xmlreader": "*",
"ext-zip": "*",
"ext-zlib": "*",
"ezyang/htmlpurifier": "~4.12", "ezyang/htmlpurifier": "~4.12",
"firebase/php-jwt": "^5.0", "firebase/php-jwt": "^5.0",
"friendsofsymfony/jsrouting-bundle": "~2.0", "friendsofsymfony/jsrouting-bundle": "~2.0",
@ -114,10 +115,9 @@
"oneup/flysystem-bundle": "~3.0", "oneup/flysystem-bundle": "~3.0",
"oneup/uploader-bundle": "~3.0", "oneup/uploader-bundle": "~3.0",
"overblog/graphql-bundle": "~0.11", "overblog/graphql-bundle": "~0.11",
"pagerfanta/pagerfanta": "~2.0",
"paragonie/random-lib": "~2.0", "paragonie/random-lib": "~2.0",
"patchwork/utf8": "~1.3", "patchwork/utf8": "~1.3",
"pagerfanta/pagerfanta": "~2.0",
"php": "^7.3",
"php-ffmpeg/php-ffmpeg": "~0.14", "php-ffmpeg/php-ffmpeg": "~0.14",
"phpoffice/phpspreadsheet": "~1.9", "phpoffice/phpspreadsheet": "~1.9",
"phpoffice/phpword": "~0.16", "phpoffice/phpword": "~0.16",
@ -146,6 +146,7 @@
"symfony/validator": "^5.0", "symfony/validator": "^5.0",
"symfony/webpack-encore-bundle": "^1.7", "symfony/webpack-encore-bundle": "^1.7",
"symfony/yaml": "^5.0", "symfony/yaml": "^5.0",
"symfonycasts/reset-password-bundle": "^1.1",
"tgalopin/html-sanitizer-bundle": "^1.1", "tgalopin/html-sanitizer-bundle": "^1.1",
"twig/extensions": "^1.5", "twig/extensions": "^1.5",
"vich/uploader-bundle": "^1.13", "vich/uploader-bundle": "^1.13",
@ -199,11 +200,5 @@
"pre-update-cmd": [ "pre-update-cmd": [
], ],
"phpcs": "phpcs --standard=.php_cs.dist" "phpcs": "phpcs --standard=.php_cs.dist"
},
"extra": {
"symfony": {
"allow-contrib": false,
"require": "5.0.*"
}
} }
} }

@ -0,0 +1,2 @@
maker:
root_namespace: 'Chamilo\CoreBundle'

@ -0,0 +1,2 @@
symfonycasts_reset_password:
request_password_repository: Chamilo\CoreBundle\Repository\ResetPasswordRequestRepository

@ -36,11 +36,6 @@ core_admin:
type: annotation type: annotation
prefix: /admin prefix: /admin
#resources:
# type: rest
# prefix: /api/{tool}/{type}
# resource: Chamilo\CoreBundle\Controller\ResourceApiController
#liip_theme: #liip_theme:
# resource: "@LiipThemeBundle/Resources/config/routing.xml" # resource: "@LiipThemeBundle/Resources/config/routing.xml"
# prefix: /theme # prefix: /theme

@ -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
);
}
}

@ -16,7 +16,7 @@
</a>&copy; {{ "now"|date("Y") }} </a>&copy; {{ "now"|date("Y") }}
</div> </div>
<div class="ml-auto"> <div class="ml-auto">
{% if administrator_name is not null %} {% if administrator_name is defined %}
{{ administrator_name }} {{ administrator_name }}
{% endif %} {% endif %}
</div> </div>

@ -5,7 +5,7 @@
<link href="https://chamilo.org/the-association/" rel="author"/> <link href="https://chamilo.org/the-association/" rel="author"/>
<link href="https://chamilo.org/the-association/" rel="copyright"/> <link href="https://chamilo.org/the-association/" rel="copyright"/>
<link rel="apple-touch-icon" href="{{ url('home') ~ 'apple-touch-icon.png' }}"/> <link rel="apple-touch-icon" href="{{ url('home') ~ 'apple-touch-icon.png' }}"/>
<link rel="shortcut icon" href="{{ url('home') ~ favico}}" type="image/x-icon" /> {#<link rel="shortcut icon" href="{{ url('home') ~ favico}}" type="image/x-icon" />#}
<title> <title>
{% block title %} {% block title %}
{{ chamilo_settings_get('platform.institution') }} - {{ chamilo_settings_get('platform.site_name') }} {{ chamilo_settings_get('platform.institution') }} - {{ chamilo_settings_get('platform.site_name') }}

@ -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…
Cancel
Save