[New routing] Reset password

This commit is contained in:
Loïc Frémont 2026-02-23 16:28:25 +01:00
parent ec5097d1a5
commit a59acfdf79
11 changed files with 227 additions and 2 deletions

View file

@ -59,6 +59,7 @@ sylius_core:
shop_product: false
shop_product_review: false
shop_register: false
shop_reset_password: false
sylius_api:
enabled: true

View file

@ -0,0 +1,26 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Sylius Sp. z o.o.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Sylius\Bundle\CoreBundle\Command\Shop\Account;
class ResetPassword
{
public function __construct(
public readonly string $token,
#[\SensitiveParameter]
public readonly string $newPassword,
#[\SensitiveParameter]
public readonly ?string $confirmNewPassword = null,
) {
}
}

View file

@ -0,0 +1,30 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Sylius Sp. z o.o.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Sylius\Bundle\CoreBundle\CommandDispatcher\Shop;
use Sylius\Bundle\CoreBundle\Command\Shop\Account\ResetPassword;
use Sylius\Bundle\CoreBundle\CommandDispatcher\ResetPasswordDispatcherInterface;
use Symfony\Component\Messenger\MessageBusInterface;
final class ShopResetPasswordDispatcher implements ResetPasswordDispatcherInterface
{
public function __construct(private MessageBusInterface $messageBus)
{
}
public function dispatch(string $token, string $password): void
{
$this->messageBus->dispatch(new ResetPassword($token, $password));
}
}

View file

@ -0,0 +1,31 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Sylius Sp. z o.o.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Sylius\Bundle\CoreBundle\CommandHandler\Shop\Account;
use Sylius\Bundle\CoreBundle\Command\Shop\Account\ResetPassword;
use Sylius\Bundle\CoreBundle\Security\UserPasswordResetterInterface;
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
#[AsMessageHandler]
final class ResetPasswordHandler
{
public function __construct(private UserPasswordResetterInterface $userPasswordResetter)
{
}
public function __invoke(ResetPassword $command): void
{
$this->userPasswordResetter->reset($command->token, $command->newPassword);
}
}

View file

@ -27,6 +27,8 @@ class OrderController extends BaseOrderController
/** @deprecated This method is deprecated and will be removed in Sylius 3.0 */
public function summaryAction(Request $request): Response
{
trigger_deprecation('sylius/shop-bundle', '2.3', '"%s" method is deprecated and will be removed in Sylius 3.0', __METHOD__);
$configuration = $this->requestConfigurationFactory->create($this->metadata, $request);
$cart = $this->getCurrentCart();

View file

@ -19,6 +19,7 @@ use Sylius\Bundle\CoreBundle\CommandDispatcher\ResendShipmentConfirmationEmailDi
use Sylius\Bundle\CoreBundle\CommandDispatcher\ResendShipmentConfirmationEmailDispatcherInterface;
use Sylius\Bundle\CoreBundle\CommandDispatcher\ResetPasswordDispatcher;
use Sylius\Bundle\CoreBundle\CommandDispatcher\ResetPasswordDispatcherInterface;
use Sylius\Bundle\CoreBundle\CommandDispatcher\Shop\ShopResetPasswordDispatcher;
return static function (ContainerConfigurator $container) {
$services = $container->services();
@ -40,4 +41,9 @@ return static function (ContainerConfigurator $container) {
->args([service('sylius.command_bus')])
;
$services->alias(ResetPasswordDispatcherInterface::class, 'sylius.command_dispatcher.reset_password');
$services
->set('sylius.command_dispatcher.reset_password.shop', ShopResetPasswordDispatcher::class)
->args([service('sylius.command_bus')])
;
};

View file

@ -14,10 +14,11 @@ declare(strict_types=1);
namespace Symfony\Component\DependencyInjection\Loader\Configurator;
use Sylius\Bundle\CoreBundle\CommandHandler\Admin\Account\RequestResetPasswordEmailHandler;
use Sylius\Bundle\CoreBundle\CommandHandler\Admin\Account\ResetPasswordHandler;
use Sylius\Bundle\CoreBundle\CommandHandler\Admin\Account\ResetPasswordHandler as AdminResetPasswordEmailHandler;
use Sylius\Bundle\CoreBundle\CommandHandler\Admin\Account\SendResetPasswordEmailHandler;
use Sylius\Bundle\CoreBundle\CommandHandler\ResendOrderConfirmationEmailHandler;
use Sylius\Bundle\CoreBundle\CommandHandler\ResendShipmentConfirmationEmailHandler;
use Sylius\Bundle\CoreBundle\CommandHandler\Shop\Account\ResetPasswordHandler as ShopResetPasswordEmailHandler;
return static function (ContainerConfigurator $container) {
$services = $container->services();
@ -43,11 +44,17 @@ return static function (ContainerConfigurator $container) {
;
$services
->set('sylius.command_handler.admin.account.reset_password', ResetPasswordHandler::class)
->set('sylius.command_handler.admin.account.reset_password', AdminResetPasswordEmailHandler::class)
->args([service('sylius.resetter.user_password.admin')])
->tag('messenger.message_handler', ['bus' => 'sylius.command_bus'])
;
$services
->set('sylius.command_handler.shop.account.reset_password', ShopResetPasswordEmailHandler::class)
->args([service('sylius.resetter.user_password.shop')])
->tag('messenger.message_handler', ['bus' => 'sylius.command_bus'])
;
$services
->set('sylius.command_handler.resend_order_confirmation_email', ResendOrderConfirmationEmailHandler::class)
->args([

View file

@ -0,0 +1,96 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Sylius Sp. z o.o.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Sylius\Bundle\ShopBundle\Controller;
use Sylius\Bundle\CoreBundle\CommandDispatcher\ResetPasswordDispatcherInterface;
use Sylius\Bundle\CoreBundle\Provider\FlashBagProvider;
use Sylius\Bundle\UserBundle\Form\Model\PasswordReset;
use Sylius\Bundle\UserBundle\Form\Type\UserResetPasswordType;
use Sylius\Component\User\Model\UserInterface;
use Sylius\Component\User\Repository\UserRepositoryInterface;
use Symfony\Component\Form\FormFactoryInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
use Twig\Environment;
/**
* @experimental
*/
final readonly class ResetPasswordAction
{
public function __construct(
private Environment $twig,
private RouterInterface $router,
private FormFactoryInterface $formFactory,
private UserRepositoryInterface $userRepository,
private TranslatorInterface $translator,
private RequestStack $requestStack,
private ResetPasswordDispatcherInterface $resetPasswordDispatcher,
private string $tokenTtl,
) {
}
public function __invoke(Request $request, string $token, ?string $formType, ?string $template, ?string $redirect): Response
{
$formType ??= UserResetPasswordType::class;
$redirect ??= 'sylius_shop_login';
$template ??= '@SyliusShop/account/reset_password.html.twig';
/** @var UserInterface|null $user */
$user = $this->userRepository->findOneBy(['passwordResetToken' => $token]);
if (null === $user) {
throw new NotFoundHttpException('Token not found.');
}
$lifetime = new \DateInterval($this->tokenTtl);
if (!$user->isPasswordRequestNonExpired($lifetime)) {
$this->addErrorNotification();
return new RedirectResponse($this->router->generate($redirect));
}
$passwordReset = new PasswordReset();
$form = $this->formFactory->create($formType, $passwordReset);
if (in_array($request->getMethod(), ['POST', 'PUT', 'PATCH'], true) && $form->handleRequest($request)->isSubmitted() && $form->isValid()) {
$this->resetPasswordDispatcher->dispatch($token, $passwordReset->getPassword());
$this->addSuccessNotification();
return new RedirectResponse($this->router->generate($redirect));
}
return new Response($this->twig->render($template, [
'form' => $form->createView(),
'user' => $user,
]));
}
private function addSuccessNotification(): void
{
FlashBagProvider::getFlashBag($this->requestStack)->add('success', $this->translator->trans('sylius.user.reset_password', [], 'flashes'));
}
private function addErrorNotification(): void
{
FlashBagProvider::getFlashBag($this->requestStack)->add('error', $this->translator->trans('sylius.user.expire_password_reset_token', [], 'flashes'));
}
}

View file

@ -76,8 +76,15 @@ sylius_shop_request_password_reset_token:
sylius_shop_password_reset:
path: /forgotten-password/{token}
methods: [GET, POST]
condition: "context.isSyliusRoutingBcLayerEnabled('shop_reset_password')"
defaults:
_controller: sylius.controller.shop_user::resetPasswordAction
_sylius:
template: "@SyliusShop/account/reset_password.html.twig"
redirect: sylius_shop_login
_sylius_shop_password_reset:
path: /forgotten-password/{token}
methods: [GET, POST]
controller: sylius_shop.controller.reset_password
condition: "not context.isSyliusRoutingBcLayerEnabled('shop_reset_password')"

View file

@ -20,6 +20,7 @@ use Sylius\Bundle\ShopBundle\Controller\CurrencySwitchController;
use Sylius\Bundle\ShopBundle\Controller\LocaleSwitchController;
use Sylius\Bundle\ShopBundle\Controller\OrderThankYouAction;
use Sylius\Bundle\ShopBundle\Controller\RegistrationThankYouController;
use Sylius\Bundle\ShopBundle\Controller\ResetPasswordAction;
return static function (ContainerConfigurator $container) {
$services = $container->services();
@ -102,4 +103,19 @@ return static function (ContainerConfigurator $container) {
])
->tag('controller.service_arguments')
;
$services
->set('sylius_shop.controller.reset_password', ResetPasswordAction::class)
->args([
service('twig'),
service('router.default'),
service('form.factory'),
service('sylius.repository.shop_user'),
service('translator'),
service('request_stack'),
service('sylius.command_dispatcher.reset_password.shop'),
param('sylius.shop_user.token.password_reset.ttl'),
])
->tag('controller.service_arguments')
;
};

View file

@ -72,8 +72,11 @@ class UserController extends ResourceController
return $this->prepareResetPasswordRequest($request, $generator, UserEvents::REQUEST_RESET_PASSWORD_TOKEN);
}
/** @deprecated This method is deprecated and will be removed in Sylius 3.0 */
public function resetPasswordAction(Request $request, string $token): Response
{
trigger_deprecation('sylius/user-bundle', '2.3', '"%s" method is deprecated and will be removed in Sylius 3.0', __METHOD__);
$configuration = $this->requestConfigurationFactory->create($this->metadata, $request);
/** @var UserInterface|null $user */
$user = $this->repository->findOneBy(['passwordResetToken' => $token]);