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/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/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/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 f2e5748..f564296 100644 --- a/templates/Account/profile.html.twig +++ b/templates/Account/profile.html.twig @@ -20,5 +20,12 @@ + + Password + ******** + + Change + + \ No newline at end of file