diff --git a/src/Controller/ResetPasswordController.php b/src/Controller/ResetPasswordController.php index cbdc141..319faa6 100644 --- a/src/Controller/ResetPasswordController.php +++ b/src/Controller/ResetPasswordController.php @@ -12,8 +12,9 @@ use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Mailer\MailerInterface; use Symfony\Component\Mime\Address; +use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface; use Symfony\Component\Routing\Annotation\Route; -use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface; +use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface; use SymfonyCasts\Bundle\ResetPassword\Controller\ResetPasswordControllerTrait; use SymfonyCasts\Bundle\ResetPassword\Exception\ResetPasswordExceptionInterface; use SymfonyCasts\Bundle\ResetPassword\ResetPasswordHelperInterface; @@ -25,152 +26,154 @@ 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('security/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 - { - // Generate a fake token if the user does not exist or someone hit this page directly. - // This prevents exposing whether or not a user was found with the given email address or not - if (null === ($resetToken = $this->getTokenObjectFromSession())) { - $resetToken = $this->resetPasswordHelper->generateFakeResetToken(); - } - - return $this->render('security/check_email.html.twig', [ - 'resetToken' => $resetToken, - ]); - } - - /** - * 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 ($token === null) { - 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('blogs'); - } - - return $this->render('security/reset.html.twig', [ - 'resetForm' => $form->createView(), - ]); - } - - private function processSendingPasswordResetEmail(string $emailFormData, MailerInterface $mailer): RedirectResponse - { - $user = $this->getDoctrine()->getRepository(User::class)->findOneBy([ - 'email' => $emailFormData, - ]); - - // 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) { - // If you want to tell the user why a reset email was not sent, uncomment - // the lines below and change the redirect to 'app_forgot_password_request'. - // Caution: This may reveal if a user is registered or not. - // - // $this->addFlash('reset_password_error', sprintf( - // 'There was a problem handling your password reset request - %s', - // $e->getReason() - // )); - - return $this->redirectToRoute('app_check_email'); - } - - $email = (new TemplatedEmail()) - ->from(new Address('tracer@24unix.net', '24unix.net')) - ->to($user->getEmail()) - ->subject('Your password reset request') - ->htmlTemplate('security/email.html.twig') - ->context([ - 'resetToken' => $resetToken, - ]) - ; - - $mailer->send($email); - - // Store the token object in session for retrieval in check-email route. - $this->setTokenObjectInSession($resetToken); - - return $this->redirectToRoute('app_check_email'); - } + 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('security/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 + { + // Generate a fake token if the user does not exist or someone hit this page directly. + // This prevents exposing whether or not a user was found with the given email address or not + if (null === ($resetToken = $this->getTokenObjectFromSession())) { + $resetToken = $this->resetPasswordHelper->generateFakeResetToken(); + } + + return $this->render('security/check_email.html.twig', [ + 'resetToken' => $resetToken, + ]); + } + + /** + * 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, UserPasswordHasherInterface $passwordHasher, 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 ($token === null) { + 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); + + // Hash the plain password, and set it. + /* + ** @var PasswordAuthenticatedUserInterface $user + */ + $hashedPassword = $passwordHasher->hashPassword( + $user, + $form->get('plainPassword')->getData() + ); + + $user->setPassword($hashedPassword); + $this->getDoctrine()->getManager()->flush(); + + // The session is cleaned up after the password has been changed. + $this->cleanSessionAfterReset(); + + return $this->redirectToRoute('blogs'); + } + + return $this->render('security/reset.html.twig', [ + 'resetForm' => $form->createView(), + ]); + } + + private function processSendingPasswordResetEmail(string $emailFormData, MailerInterface $mailer): RedirectResponse + { + $user = $this->getDoctrine()->getRepository(User::class)->findOneBy([ + 'email' => $emailFormData, + ]); + + // 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) { + // If you want to tell the user why a reset email was not sent, uncomment + // the lines below and change the redirect to 'app_forgot_password_request'. + // Caution: This may reveal if a user is registered or not. + // + // $this->addFlash('reset_password_error', sprintf( + // 'There was a problem handling your password reset request - %s', + // $e->getReason() + // )); + + return $this->redirectToRoute('app_check_email'); + } + + $email = (new TemplatedEmail()) + ->from(new Address('tracer@24unix.net', '24unix.net')) + ->to($user->getEmail()) + ->subject('Your password reset request') + ->htmlTemplate('security/email.html.twig') + ->context([ + 'resetToken' => $resetToken, + ]); + + $mailer->send($email); + + // Store the token object in session for retrieval in check-email route. + $this->setTokenObjectInSession($resetToken); + + return $this->redirectToRoute('app_check_email'); + } }