From 40118a0edcce60aafd7f3c69598d7c7fe134570d Mon Sep 17 00:00:00 2001 From: Alexey Skobkin Date: Wed, 20 Jul 2022 03:48:19 +0300 Subject: [PATCH] Password change (!13) Reviewed-on: https://git.skobk.in/skobkin/magnetico-web/pulls/13 --- config/routes.yaml | 6 +++ src/Command/AddInvitesCommand.php | 2 +- src/Command/AddUserCommand.php | 2 +- src/Controller/AccountController.php | 48 ++++++++++++++++-- src/Controller/UserController.php | 72 +++++++++++++-------------- src/Form/Data/PasswordChangeData.php | 19 +++++++ src/Form/LoginType.php | 2 +- src/Form/PasswordChangeType.php | 38 ++++++++++++++ src/Form/PasswordResetRequestType.php | 4 +- src/Form/PasswordResetType.php | 4 +- src/Form/RegisterType.php | 4 +- src/User/UserManager.php | 5 -- templates/Account/password.html.twig | 5 ++ templates/Account/profile.html.twig | 9 +++- 14 files changed, 166 insertions(+), 54 deletions(-) create mode 100644 src/Form/Data/PasswordChangeData.php create mode 100644 src/Form/PasswordChangeType.php create mode 100644 templates/Account/password.html.twig diff --git a/config/routes.yaml b/config/routes.yaml index 0c938b7..886f857 100644 --- a/config/routes.yaml +++ b/config/routes.yaml @@ -53,6 +53,12 @@ user_account: requirements: method: GET +user_account_password_change: + path: /account/password + controller: App\Controller\AccountController::changePassword + requirements: + method: POST + user_account_token_create: path: /profile/api/token/create controller: App\Controller\AccountController::addApiToken diff --git a/src/Command/AddInvitesCommand.php b/src/Command/AddInvitesCommand.php index 1c30c14..c27a585 100644 --- a/src/Command/AddInvitesCommand.php +++ b/src/Command/AddInvitesCommand.php @@ -44,7 +44,7 @@ class AddInvitesCommand extends Command $this->em->flush(); - $output->writeln(sprintf('%d invites added to \'%s\'.', $number, $user->getUsername())); + $output->writeln(sprintf('%d invites added to \'%s\'.', $number, $user->getUserIdentifier())); return Command::SUCCESS; } diff --git a/src/Command/AddUserCommand.php b/src/Command/AddUserCommand.php index 8f4da3c..eb438ac 100644 --- a/src/Command/AddUserCommand.php +++ b/src/Command/AddUserCommand.php @@ -74,7 +74,7 @@ class AddUserCommand extends Command $this->em->flush(); - $output->writeln(sprintf('User \'%s\' registered, %d invites added.', $user->getUsername(), $invites)); + $output->writeln(sprintf('User \'%s\' registered, %d invites added.', $user->getUserIdentifier(), $invites)); return Command::SUCCESS; } diff --git a/src/Controller/AccountController.php b/src/Controller/AccountController.php index 5792131..be38502 100644 --- a/src/Controller/AccountController.php +++ b/src/Controller/AccountController.php @@ -5,14 +5,21 @@ namespace App\Controller; use App\Entity\{ApiToken, User}; use App\Repository\{ApiTokenRepository, InviteRepository}; +use App\Form\Data\PasswordChangeData; +use App\Form\PasswordChangeType; use Doctrine\ORM\EntityManagerInterface; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; -use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Form\Extension\Core\Type\SubmitType; +use Symfony\Component\Form\FormInterface; +use Symfony\Component\HttpFoundation\{Request, Response}; +use Symfony\Component\PasswordHasher\Hasher\PasswordHasherFactoryInterface; class AccountController extends AbstractController { - public function account(InviteRepository $inviteRepo, ApiTokenRepository $apiTokenRepo): Response - { + public function account( + InviteRepository $inviteRepo, + ApiTokenRepository $apiTokenRepo + ): Response { /** @var User $user */ if (null === $user = $this->getUser()) { throw $this->createAccessDeniedException('User not found exception'); @@ -26,6 +33,32 @@ class AccountController extends AbstractController ]); } + public function changePassword( + Request $request, + EntityManagerInterface $em, + PasswordHasherFactoryInterface $hasherFactory, + ): Response { + $data = new PasswordChangeData(); + $form = $this->createChangePasswordForm($data); + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + /** @var User $user */ + $user = $this->getUser(); + $hasher = $hasherFactory->getPasswordHasher($user); + + $user->changePassword($hasher, $data->newPassword); + $em->flush(); + $this->addFlash('success', 'Password changed.'); + + return $this->redirectToRoute('user_account'); + } + + return $this->renderForm('Account/password.html.twig', [ + 'form' => $form, + ]); + } + public function addApiToken(EntityManagerInterface $em): Response { /** @var User|null $user */ @@ -57,4 +90,13 @@ class AccountController extends AbstractController return $this->redirectToRoute('user_account'); } + + private function createChangePasswordForm(PasswordChangeData $data): FormInterface + { + return $this + ->createForm(PasswordChangeType::class, $data, [ + 'action' => $this->generateUrl('user_account_password_change'), + ]) + ->add('submit', SubmitType::class); + } } diff --git a/src/Controller/UserController.php b/src/Controller/UserController.php index a02f478..fe3827a 100644 --- a/src/Controller/UserController.php +++ b/src/Controller/UserController.php @@ -5,13 +5,15 @@ namespace App\Controller; use App\Entity\{Invite, PasswordResetToken}; use App\Repository\PasswordResetTokenRepository; -use App\Form\{Data\PasswordResetRequestData, Data\PasswordResetData, PasswordResetRequestType, PasswordResetType, RegisterType, Data\RegisterData}; +use App\Form\Data\{PasswordResetRequestData, PasswordResetData, RegisterData}; +use App\Form\{PasswordResetRequestType, PasswordResetType, RegisterType}; use App\Repository\InviteRepository; use App\User\{Exception\UserNotFoundException, InviteManager, PasswordResetManager, UserManager}; use Doctrine\ORM\EntityManagerInterface; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\Form\{Extension\Core\Type\SubmitType, FormError, FormInterface}; use Symfony\Component\HttpFoundation\{Request, Response}; +use Symfony\Component\PasswordHasher\Hasher\PasswordHasherFactoryInterface; class UserController extends AbstractController { @@ -23,8 +25,8 @@ class UserController extends AbstractController InviteManager $inviteManager, InviteRepository $inviteRepo ): Response { - $formData = new RegisterData($code); - $form = $this->createRegisterForm($formData, $code); + $data = new RegisterData($code); + $form = $this->createRegisterForm($data, $code); /** @var Invite $invite */ $invite = $inviteRepo->findOneBy(['code' => $code, 'usedBy' => null]); @@ -33,9 +35,9 @@ class UserController extends AbstractController if ($form->isSubmitted() && $form->isValid()) { $user = $userManager->createUserByInvite( - $formData->username, - $formData->password, - $formData->email, + $data->username, + $data->password, + $data->email, $invite ); @@ -54,8 +56,8 @@ class UserController extends AbstractController public function requestReset(Request $request, EntityManagerInterface $em, PasswordResetManager $manager): Response { - $formData = new PasswordResetRequestData(); - $form = $this->createResetRequestForm($formData); + $data = new PasswordResetRequestData(); + $form = $this->createResetRequestForm($data); $form->handleRequest($request); @@ -63,7 +65,7 @@ class UserController extends AbstractController if ($form->isSubmitted() && $form->isValid()) { try { - $manager->sendResetLink($formData->email); + $manager->sendResetLink($data->email); $message = 'Password reset link was sent'; } catch (UserNotFoundException $e) { @@ -84,12 +86,12 @@ class UserController extends AbstractController string $code, Request $request, EntityManagerInterface $em, - UserManager $manager, - PasswordResetTokenRepository $tokenRepository + PasswordHasherFactoryInterface $hasherFactory, + PasswordResetTokenRepository $tokenRepository, ): Response { - $formData = new PasswordResetData(); - $form = $this->createResetForm($formData, $code); + $data = new PasswordResetData(); + $form = $this->createPasswordResetForm($data, $code); /** @var PasswordResetToken $token */ $token = $tokenRepository->find($code); @@ -98,15 +100,17 @@ class UserController extends AbstractController if ($form->isSubmitted() && $form->isValid()) { if ($token && $token->isValid()) { - $manager->changePassword($token->getUser(), $formData->password); + $user = $token->getUser(); + $hasher = $hasherFactory->getPasswordHasher($user); + $user->changePassword($hasher, $data->password); $em->remove($token); $em->flush(); return $this->redirectToRoute('user_auth_login'); - } else { - $form->addError(new FormError('Invalid token.')); } + + $form->addError(new FormError('Invalid token.')); } return $this->render('User/reset.html.twig', [ @@ -116,32 +120,28 @@ class UserController extends AbstractController private function createResetRequestForm(PasswordResetRequestData $formData): FormInterface { - $form = $this->createForm(PasswordResetRequestType::class, $formData, [ - 'action' => $this->generateUrl('user_reset_request'), - ]); - $form->add('submit', SubmitType::class); - - return $form; + return $this + ->createForm(PasswordResetRequestType::class, $formData, [ + 'action' => $this->generateUrl('user_reset_request'), + ]) + ->add('submit', SubmitType::class); } - private function createResetForm(PasswordResetData $formData, string $code): FormInterface + private function createPasswordResetForm(PasswordResetData $data, string $code): FormInterface { - $form = $this->createForm(PasswordResetType::class, $formData, [ - 'action' => $this->generateUrl('user_reset', ['code' => $code]), - ]); - $form->add('submit', SubmitType::class); - - return $form; + return $this + ->createForm(PasswordResetType::class, $data, [ + 'action' => $this->generateUrl('user_reset', ['code' => $code]), + ]) + ->add('submit', SubmitType::class); } private function createRegisterForm(RegisterData $formData, string $code): FormInterface { - $form = $this->createForm(RegisterType::class, $formData, [ - 'action' => $this->generateUrl('user_register', ['code' => $code]), - ]); - - $form->add('submit', SubmitType::class); - - return $form; + return $this + ->createForm(RegisterType::class, $formData, [ + 'action' => $this->generateUrl('user_register', ['code' => $code]), + ]) + ->add('submit', SubmitType::class); } } \ No newline at end of file diff --git a/src/Form/Data/PasswordChangeData.php b/src/Form/Data/PasswordChangeData.php new file mode 100644 index 0000000..ff87186 --- /dev/null +++ b/src/Form/Data/PasswordChangeData.php @@ -0,0 +1,19 @@ +add('_username', TextType::class, ['mapped' => false, 'required' => true]) diff --git a/src/Form/PasswordChangeType.php b/src/Form/PasswordChangeType.php new file mode 100644 index 0000000..36fe90c --- /dev/null +++ b/src/Form/PasswordChangeType.php @@ -0,0 +1,38 @@ +add('currentPassword', PasswordType::class, [ + 'label' => 'Current password', + 'invalid_message' => 'Wrong password.', + 'required' => true, + ]) + ->add('newPassword', RepeatedType::class, [ + 'type' => PasswordType::class, + 'invalid_message' => 'The password fields must match.', + 'required' => true, + 'first_options' => ['label' => 'New password'], + 'second_options' => ['label' => 'Repeat'], + ]) + ; + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'data_class' => PasswordChangeData::class, + ]); + } +} diff --git a/src/Form/PasswordResetRequestType.php b/src/Form/PasswordResetRequestType.php index a3adf34..156774b 100644 --- a/src/Form/PasswordResetRequestType.php +++ b/src/Form/PasswordResetRequestType.php @@ -10,7 +10,7 @@ use Symfony\Component\OptionsResolver\OptionsResolver; class PasswordResetRequestType extends AbstractType { - public function buildForm(FormBuilderInterface $builder, array $options) + public function buildForm(FormBuilderInterface $builder, array $options): void { $builder ->add('email', EmailType::class, ['required' => true]) @@ -18,7 +18,7 @@ class PasswordResetRequestType extends AbstractType ; } - public function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver): void { $resolver->setDefaults([ 'data_class' => PasswordResetRequestData::class, diff --git a/src/Form/PasswordResetType.php b/src/Form/PasswordResetType.php index b955c6f..06d2690 100644 --- a/src/Form/PasswordResetType.php +++ b/src/Form/PasswordResetType.php @@ -11,7 +11,7 @@ use Symfony\Component\OptionsResolver\OptionsResolver; class PasswordResetType extends AbstractType { - public function buildForm(FormBuilderInterface $builder, array $options) + public function buildForm(FormBuilderInterface $builder, array $options): void { $builder ->add('password', RepeatedType::class, [ @@ -24,7 +24,7 @@ class PasswordResetType extends AbstractType ; } - public function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver): void { $resolver->setDefaults([ 'data_class' => PasswordResetData::class, diff --git a/src/Form/RegisterType.php b/src/Form/RegisterType.php index 5005d3e..8a99b1c 100644 --- a/src/Form/RegisterType.php +++ b/src/Form/RegisterType.php @@ -11,7 +11,7 @@ use Symfony\Component\OptionsResolver\OptionsResolver; class RegisterType extends AbstractType { - public function buildForm(FormBuilderInterface $builder, array $options) + public function buildForm(FormBuilderInterface $builder, array $options): void { $builder ->add('username', TextType::class, ['required' => true]) @@ -21,7 +21,7 @@ class RegisterType extends AbstractType ; } - public function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver): void { $resolver->setDefaults([ 'data_class' => RegisterData::class, diff --git a/src/User/UserManager.php b/src/User/UserManager.php index 9fab5a5..a4099af 100644 --- a/src/User/UserManager.php +++ b/src/User/UserManager.php @@ -34,11 +34,6 @@ class UserManager return $user; } - public function changePassword(User $user, string $rawPassword): void - { - $user->changePassword($this->hasherFactory->getPasswordHasher(User::class), $rawPassword); - } - public function createUserByInvite(string $username, string $password, string $email, Invite $invite, array $roles = self::DEFAULT_ROLES): User { if (null !== $invite->getUsedBy()) { diff --git a/templates/Account/password.html.twig b/templates/Account/password.html.twig new file mode 100644 index 0000000..0b939a9 --- /dev/null +++ b/templates/Account/password.html.twig @@ -0,0 +1,5 @@ +{% extends 'base.html.twig' %} + +{% block content %} +{{ form(form) }} +{% endblock %} \ No newline at end of file diff --git a/templates/Account/profile.html.twig b/templates/Account/profile.html.twig index 34a0c6d..f564296 100644 --- a/templates/Account/profile.html.twig +++ b/templates/Account/profile.html.twig @@ -10,7 +10,7 @@ Username - {{ user.username }} + {{ user.userIdentifier }} @@ -20,5 +20,12 @@ + + Password + ******** + + Change + + \ No newline at end of file