feature #14153 [Admin][UI][API] Reset password validation (NoResponseMate)

This PR was merged into the 1.12 branch.

Discussion
----------

| Q               | A                                                            |
|-----------------|--------------------------------------------------------------|
| Branch?         | 1.12          |
| Bug fix?        | no                                                       |
| New feature?    | yes                                                       |
| BC breaks?      | no                                                       |
| Deprecations?   | no <!-- don't forget to update the UPGRADE-*.md file --> |
| Related tickets | -                      |
| License         | MIT                                                          |

Based on #14147

Extracted it to not mix the concepts as the base PR only touches UI, but the validation is for both UI and API.

Commits
-------

6c1336a886 Add administrator password reset ui tests
ad5cf70619 [Admin][UI] Rendering the reset password page
5f845b1c83 Move ResetPassword command and handler into Core
99dd0b2eca [Admin][UI] Password resetting
f430bfb8c2 [Behat][Admin] Better elements' names in ResetPasswordPage
c37d52f999 [Admin][Core] Extract ResetPassword message dispatching to a standalone service
ea77ea005c [Behat] Fix admin's logging in logic
23ef23e223 [Behat][Admin] Fix and cleanup of password resetting
f6ce7838cb Fix sylius.admin.login.before_form event's priorities
53542d27db [Core] Remove "experimental" tag from ResetPassword related classes
ddbfa821e8 [Behat] More precise password changed notification check
0c596f6d41 [Admin][UI] Rework password reset template events
3ede1e5532 [Admin][UI] Add completely forgotten password reset email link
5c320f0ca3 [API][UI] Add validation for expired admin password reset token
fd24b11ee7 [API][UI] Add password validation to admin's password reset workflow
This commit is contained in:
Mateusz Zalewski 2022-07-21 16:15:41 +02:00 committed by GitHub
commit 6193a33da8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
54 changed files with 1064 additions and 59 deletions

View file

@ -5,7 +5,8 @@ Feature: Resetting an administrator's password
I want to be able to reset my password
Background:
Given there is an administrator "sylius@example.com" identified by "sylius"
Given the store operates on a single channel in "United States"
And there is an administrator "sylius@example.com" identified by "sylius"
@email @api @ui
Scenario: Sending an administrator's password reset request
@ -23,7 +24,7 @@ Feature: Resetting an administrator's password
Then I should be notified that email with reset instruction has been sent
But "does-not-exist@example.com" should receive no emails
@api
@ui @api
Scenario: Changing my administrator's password
Given I have already received a resetting password email
When I follow the instructions to reset my password
@ -33,7 +34,7 @@ Feature: Resetting an administrator's password
Then I should be notified that my password has been successfully changed
And I should be able to log in as "sylius@example.com" authenticated by "newp@ssw0rd" password
@api
@ui @api
Scenario: Trying to change my administrator's password twice without sending a new password reset request
Given I have already received an administrator's password resetting email
When I follow the instructions to reset my password
@ -41,3 +42,20 @@ Feature: Resetting an administrator's password
And I confirm my new password as "newp@ssw0rd"
And I reset it
Then I should not be able to change my password again with the same token
@api
Scenario: Trying to change my administrator's password using an expired reset token
Given I have already received an administrator's password resetting email
But my password reset token has already expired
When I try to follow the instructions to reset my password
And I specify my new password as "newp@ssw0rd"
And I confirm my new password as "newp@ssw0rd"
And I reset it
Then I should be notified that the password reset token has expired
@ui @no-api
Scenario: Trying to change my administrator's password using an expired reset token
Given I have already received an administrator's password resetting email
But my password reset token has already expired
When I try to follow the instructions to reset my password
Then I should be notified that the password reset token has expired

View file

@ -14,6 +14,15 @@ Feature: Resetting an administrator's password validation
And I try to reset it
Then I should be notified that the email is required
@ui @api
Scenario: Trying to reset my administrator's password with an empty value
Given I have already received an administrator's password resetting email
When I follow the instructions to reset my password
And I do not specify my new password
And I do not confirm my new password
And I try to reset it
Then I should be notified that the new password is required
@ui @api
Scenario: Trying to reset my administrator's password with an invalid email
When I want to reset password
@ -21,19 +30,19 @@ Feature: Resetting an administrator's password validation
And I try to reset it
Then I should be notified that the email is not valid
@todo
@ui @api
Scenario: Trying to reset my administrator's password with a wrong confirmation password
Given I have already received an administrator's password resetting email
When I follow link on my email to reset my password
When I follow the instructions to reset my password
And I specify my new password as "newp@ssw0rd"
And I confirm my new password as "wrongp@ssw0rd"
And I try to reset it
Then I should be notified that the entered passwords do not match
@todo
@ui @api
Scenario: Trying to reset my administrator's password with a too short password
Given I have already received an administrator's password resetting email
When I follow link on my email to reset my password
When I follow the instructions to reset my password
And I specify my new password as "fu"
And I confirm my new password as "fu"
And I try to reset it

View file

@ -62,7 +62,7 @@ final class ResettingPasswordContext implements Context
}
/**
* @When /^(I) follow the instructions to reset my password$/
* @When /^(I)(?:| try to) follow the instructions to reset my password$/
*/
public function iFollowTheInstructionsToResetMyPassword(AdminUserInterface $admin): void
{
@ -74,16 +74,18 @@ final class ResettingPasswordContext implements Context
/**
* @When I specify my new password as :password
* @When I do not specify my new password
*/
public function iSpecifyMyNewPassword(string $password): void
public function iSpecifyMyNewPassword(string $password = ''): void
{
$this->request->updateContent(['newPassword' => $password]);
}
/**
* @When I confirm my new password as :password
* @When I do not confirm my new password
*/
public function iConfirmMyNewPassword(string $password): void
public function iConfirmMyNewPassword(string $password = ''): void
{
$this->request->updateContent(['confirmNewPassword' => $password]);
}
@ -139,4 +141,47 @@ final class ResettingPasswordContext implements Context
'This email is not valid.'
);
}
/**
* @Then I should be notified that the password reset token has expired
*/
public function iShouldBeNotifiedThatThePasswordResetTokenHasExpired(): void
{
$message = $this->responseChecker->getError($this->client->getLastResponse());
Assert::same($message, 'The password reset token has expired.');
}
/**
* @Then I should be notified that the new password is required
*/
public function iShouldBeNotifiedThatTheNewPasswordIsRequired(): void
{
$this->assertResponseHasValidationMessageForNewPassword('Please enter the password.');
}
/**
* @Then I should be notified that the entered passwords do not match
*/
public function iShouldBeNotifiedThatTheEnteredPasswordsDoNotMatch(): void
{
$this->assertResponseHasValidationMessageForNewPassword('The entered passwords do not match.');
}
/**
* @Then /^I should be notified that the password should be ([^"]+)$/
*/
public function iShouldBeNotifiedThatThePasswordShouldBe(string $validationMessage): void
{
$this->assertResponseHasValidationMessageForNewPassword(sprintf('Password must be %s.', $validationMessage));
}
private function assertResponseHasValidationMessageForNewPassword(string $message): void
{
$lastResponse = $this->client->getLastResponse();
Assert::true(
$this->responseChecker->hasViolationWithMessage($lastResponse, $message, 'newPassword'),
$lastResponse->getContent(),
);
}
}

View file

@ -109,4 +109,14 @@ final class AdminUserContext implements Context
$this->objectManager->flush();
}
/**
* @Given /^(my) password reset token has already expired$/
*/
public function myPasswordResetTokenHasAlreadyExpired(AdminUserInterface $administrator): void
{
$administrator->setPasswordRequestedAt(new \DateTime('-1 year'));
$this->objectManager->flush();
}
}

View file

@ -14,11 +14,8 @@ declare(strict_types=1);
namespace Sylius\Behat\Context\Ui\Admin;
use Behat\Behat\Context\Context;
use Sylius\Behat\NotificationType;
use Sylius\Behat\Page\Admin\Account\LoginPageInterface;
use Sylius\Behat\Page\Admin\Account\RequestPasswordResetPage;
use Sylius\Behat\Page\Admin\DashboardPageInterface;
use Sylius\Behat\Service\NotificationCheckerInterface;
use Sylius\Component\Core\Model\AdminUserInterface;
use Webmozart\Assert\Assert;
@ -55,6 +52,14 @@ final class LoginContext implements Context
$this->loginPage->specifyPassword($password);
}
/**
* @When /^(this administrator) logs in using "([^"]+)" password$/
*/
public function theyLogIn(AdminUserInterface $adminUser, $password)
{
$this->logInAgain($adminUser->getUsername(), $password);
}
/**
* @When I log in
*/
@ -101,16 +106,7 @@ final class LoginContext implements Context
public function iShouldBeAbleToLogInAsAuthenticatedByPassword($username, $password)
{
$this->logInAgain($username, $password);
$this->dashboardPage->verify();
}
/**
* @When /^(this administrator) logs in using "([^"]+)" password$/
*/
public function theyLogIn(AdminUserInterface $adminUser, $password)
{
$this->logInAgain($adminUser->getUsername(), $password);
$this->iShouldBeLoggedIn();
}
/**
@ -132,14 +128,12 @@ final class LoginContext implements Context
Assert::true($this->loginPage->isOpen());
}
/**
* @param string $username
* @param string $password
*/
private function logInAgain($username, $password)
private function logInAgain(string $username, string $password): void
{
$this->dashboardPage->open();
$this->dashboardPage->logOut();
$this->dashboardPage->tryToOpen();
if ($this->dashboardPage->isOpen()) {
$this->dashboardPage->logOut();
}
$this->loginPage->open();
$this->loginPage->specifyUsername($username);

View file

@ -14,15 +14,20 @@ declare(strict_types=1);
namespace Sylius\Behat\Context\Ui\Admin;
use Behat\Behat\Context\Context;
use Sylius\Behat\Element\Admin\Account\ResetElementInterface;
use Sylius\Behat\NotificationType;
use Sylius\Behat\Page\Admin\Account\RequestPasswordResetPage;
use Sylius\Behat\Page\Admin\Account\ResetPasswordPageInterface;
use Sylius\Behat\Service\NotificationCheckerInterface;
use Sylius\Component\Core\Model\AdminUserInterface;
use Webmozart\Assert\Assert;
final class ResettingPasswordContext implements Context
{
public function __construct(
private RequestPasswordResetPage $requestPasswordResetPage,
private ResetPasswordPageInterface $resetPasswordPage,
private ResetElementInterface $resetElement,
private NotificationCheckerInterface $notificationChecker,
) {
}
@ -49,7 +54,33 @@ final class ResettingPasswordContext implements Context
*/
public function iResetIt(): void
{
$this->requestPasswordResetPage->resetPassword();
$this->resetElement->reset();
}
/**
* @When /^(I)(?:| try to) follow the instructions to reset my password$/
*/
public function iFollowTheInstructionsToResetMyPassword(AdminUserInterface $admin): void
{
$this->resetPasswordPage->tryToOpen(['token' => $admin->getPasswordResetToken()]);
}
/**
* @When I specify my new password as :password
* @When I do not specify my new password
*/
public function iSpecifyMyNewPassword(string $password = ''): void
{
$this->resetPasswordPage->specifyNewPassword($password);
}
/**
* @When I confirm my new password as :password
* @When I do not confirm my new password
*/
public function iConfirmMyNewPassword(string $password = ''): void
{
$this->resetPasswordPage->specifyPasswordConfirmation($password);
}
/**
@ -84,4 +115,63 @@ final class ResettingPasswordContext implements Context
'This email is not valid.',
);
}
/**
* @Then I should be notified that my password has been successfully changed
*/
public function iShouldBeNotifiedThatMyPasswordHasBeenSuccessfullyChanged(): void
{
$this->notificationChecker->checkNotification('has been changed successfully!', NotificationType::success());
}
/**
* @Then I should not be able to change my password again with the same token
*/
public function iShouldNotBeAbleToChangeMyPasswordAgainWithTheSameToken(): void
{
$this->resetPasswordPage->tryToOpen(['token' => 'itotallyforgotmypassword']);
Assert::false($this->resetPasswordPage->isOpen(), 'User should not be on the forgotten password page');
}
/**
* @Then I should be notified that the password reset token has expired
*/
public function iShouldBeNotifiedThatThePasswordResetTokenHasExpired(): void
{
$this->notificationChecker->checkNotification('has expired', NotificationType::failure());
}
/**
* @Then I should be notified that the new password is required
*/
public function iShouldBeNotifiedThatTheNewPasswordIsRequired(): void
{
Assert::contains(
$this->resetPasswordPage->getValidationMessageFor('new_password'),
'Please enter the password.',
);
}
/**
* @Then I should be notified that the entered passwords do not match
*/
public function iShouldBeNotifiedThatTheEnteredPasswordsDoNotMatch(): void
{
Assert::contains(
$this->resetPasswordPage->getValidationMessageFor('new_password'),
'The entered passwords do not match.',
);
}
/**
* @Then /^I should be notified that the password should be ([^"]+)$/
*/
public function iShouldBeNotifiedThatThePasswordShouldBe(string $validationMessage): void
{
Assert::contains(
$this->resetPasswordPage->getValidationMessageFor('new_password'),
$validationMessage,
);
}
}

View file

@ -0,0 +1,24 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Paweł Jędrzejewski
*
* 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\Behat\Element\Admin\Account;
use FriendsOfBehat\PageObjectExtension\Element\Element;
final class ResetElement extends Element implements ResetElementInterface
{
public function reset(): void
{
$this->getDocument()->find('css', 'button[type="submit"]:contains("Reset")')->click();
}
}

View file

@ -0,0 +1,19 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Paweł Jędrzejewski
*
* 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\Behat\Element\Admin\Account;
interface ResetElementInterface
{
public function reset(): void;
}

View file

@ -20,7 +20,7 @@ class RequestPasswordResetPage extends SymfonyPage implements RequestPasswordRes
{
public function getRouteName(): string
{
return 'sylius_admin_request_password_reset';
return 'sylius_admin_render_reset_password_page';
}
public function specifyEmail(string $email): void
@ -28,11 +28,6 @@ class RequestPasswordResetPage extends SymfonyPage implements RequestPasswordRes
$this->getElement('email')->setValue($email);
}
public function resetPassword(): void
{
$this->getDocument()->pressButton('Reset password');
}
public function getEmailValidationMessage(): string
{
$errorLabel = $this->getElement('email')->getParent()->find('css', '[data-test-validation-error]');

View file

@ -19,7 +19,5 @@ interface RequestPasswordResetPageInterface extends SymfonyPageInterface
{
public function specifyEmail(string $email): void;
public function resetPassword(): void;
public function getEmailValidationMessage(): string;
}

View file

@ -0,0 +1,54 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Paweł Jędrzejewski
*
* 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\Behat\Page\Admin\Account;
use Behat\Mink\Exception\ElementNotFoundException;
use FriendsOfBehat\PageObjectExtension\Page\SymfonyPage;
class ResetPasswordPage extends SymfonyPage implements ResetPasswordPageInterface
{
public function specifyNewPassword(string $password): void
{
$this->getElement('new_password')->setValue($password);
}
public function specifyPasswordConfirmation(string $password): void
{
$this->getElement('confirm_new_password')->setValue($password);
}
public function getValidationMessageFor(string $element): string
{
$errorLabel = $this->getElement($element)->getParent()->find('css', '[data-test-validation-error]');
if (null === $errorLabel) {
throw new ElementNotFoundException($this->getSession(), 'Validation message', 'css', '[data-test-validation-error]');
}
return $errorLabel->getText();
}
public function getRouteName(): string
{
return 'sylius_admin_render_password_reset';
}
protected function getDefinedElements(): array
{
return array_merge(parent::getDefinedElements(), [
'confirm_new_password' => '[data-test-confirm-new-password]',
'new_password' => '[data-test-new-password]',
]);
}
}

View file

@ -0,0 +1,25 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Paweł Jędrzejewski
*
* 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\Behat\Page\Admin\Account;
use FriendsOfBehat\PageObjectExtension\Page\SymfonyPageInterface;
interface ResetPasswordPageInterface extends SymfonyPageInterface
{
public function specifyNewPassword(string $password): void;
public function specifyPasswordConfirmation(string $password): void;
public function getValidationMessageFor(string $element): string;
}

View file

@ -319,6 +319,8 @@
<service id="sylius.behat.context.ui.admin.resetting_password" class="Sylius\Behat\Context\Ui\Admin\ResettingPasswordContext">
<argument type="service" id="sylius.behat.page.admin.request_password_reset" />
<argument type="service" id="sylius.behat.page.admin.reset_password" />
<argument type="service" id="sylius.behat.element.admin.account.reset" />
<argument type="service" id="sylius.behat.notification_checker" />
</service>

View file

@ -16,6 +16,8 @@
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd"
>
<services>
<service id="sylius.behat.element.admin.account.reset" class="Sylius\Behat\Element\Admin\Account\ResetElement" parent="sylius.behat.element" />
<service id="sylius.behat.element.admin.channel.shop_billing_data" class="Sylius\Behat\Element\Admin\Channel\ShopBillingDataElement" parent="sylius.behat.element" />
<service id="sylius.behat.element.admin.top_bar" class="Sylius\Behat\Element\Admin\TopBarElement" parent="sylius.behat.element" />

View file

@ -12,11 +12,17 @@
-->
<container xmlns="http://symfony.com/schema/dic/services" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
<parameters>
<parameter key="sylius.behat.page.admin.reset_password">Sylius\Behat\Page\Admin\Account\ResetPasswordPage</parameter>
</parameters>
<services>
<defaults public="true" />
<service id="sylius.behat.page.admin.login" class="Sylius\Behat\Page\Admin\Account\LoginPage" parent="sylius.behat.symfony_page" public="false" />
<service id="sylius.behat.page.admin.request_password_reset" class="Sylius\Behat\Page\Admin\Account\RequestPasswordResetPage" parent="sylius.behat.symfony_page" public="false" />
<service id="sylius.behat.page.admin.reset_password" class="%sylius.behat.page.admin.reset_password%" parent="sylius.behat.symfony_page" public="false" />
</services>
</container>

View file

@ -7,7 +7,7 @@ default:
contexts:
- sylius.behat.context.hook.doctrine_orm
- sylius.behat.context.transform.user
- sylius.behat.context.transform.admin
- sylius.behat.context.setup.channel
- sylius.behat.context.setup.admin_security

View file

@ -0,0 +1,74 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Paweł Jędrzejewski
*
* 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\AdminBundle\Action\Account;
use Sylius\Bundle\AdminBundle\Form\Type\ResetPasswordType;
use Sylius\Component\Core\Model\AdminUserInterface;
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\Response;
use Symfony\Component\HttpFoundation\Session\Flash\FlashBagInterface;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\Routing\RouterInterface;
use Twig\Environment;
final class RenderResetPasswordPageAction
{
public function __construct(
private UserRepositoryInterface $userRepository,
private FormFactoryInterface $formFactory,
private FlashBagInterface $flashBag,
private RouterInterface $router,
private Environment $twig,
private string $tokenTtl,
) {
}
public function __invoke(Request $request, string $token): Response
{
/** @var AdminUserInterface|null $admin */
$admin = $this->userRepository->findOneBy(['passwordResetToken' => $token]);
if (null === $admin) {
throw new NotFoundHttpException('Token not found');
}
$lifetime = new \DateInterval($this->tokenTtl);
if (!$admin->isPasswordRequestNonExpired($lifetime)) {
$this->flashBag->add('error', 'sylius.admin.password_reset.token_expired');
$attributes = $request->attributes->get('_sylius');
$redirect = $attributes['redirect'] ?? 'sylius_admin_login';
if (is_array($redirect)) {
return new RedirectResponse($this->router->generate(
$redirect['route'] ?? 'sylius_admin_login',
$redirect['params'] ?? [],
));
}
return new RedirectResponse($this->router->generate($redirect));
}
$form = $this->formFactory->create(ResetPasswordType::class);
return new Response(
$this->twig->render('@SyliusAdmin/Security/resetPassword.html.twig', [
'form' => $form->createView(),
]),
);
}
}

View file

@ -0,0 +1,70 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Paweł Jędrzejewski
*
* 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\AdminBundle\Action\Account;
use Sylius\Bundle\AdminBundle\Form\Model\PasswordReset;
use Sylius\Bundle\AdminBundle\Form\Type\ResetPasswordType;
use Sylius\Bundle\CoreBundle\MessageDispatcher\ResetPasswordDispatcherInterface;
use Symfony\Component\Form\FormFactoryInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Session\Flash\FlashBagInterface;
use Symfony\Component\Routing\RouterInterface;
use Twig\Environment;
final class ResetPasswordAction
{
public function __construct(
private FormFactoryInterface $formFactory,
private ResetPasswordDispatcherInterface $resetPasswordDispatcher,
private FlashBagInterface $flashBag,
private RouterInterface $router,
private Environment $twig,
) {
}
public function __invoke(Request $request, string $token): Response
{
$form = $this->formFactory->create(ResetPasswordType::class);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
/** @var PasswordReset $passwordReset */
$passwordReset = $form->getData();
$this->resetPasswordDispatcher->dispatch($token, $passwordReset->getPassword());
$this->flashBag->add('success', 'sylius.admin.password_reset.success');
$attributes = $request->attributes->get('_sylius');
$redirect = $attributes['redirect'] ?? 'sylius_admin_login';
if (is_array($redirect)) {
return new RedirectResponse(
$redirect['route'] ?? 'sylius_admin_login',
$redirect['params'] ?? [],
);
}
return new RedirectResponse($this->router->generate($redirect));
}
return new Response(
$this->twig->render('@SyliusAdmin/Security/resetPassword.html.twig', [
'form' => $form->createView(),
]),
);
}
}

View file

@ -0,0 +1,29 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Paweł Jędrzejewski
*
* 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\AdminBundle\Form\Model;
class PasswordReset
{
private ?string $password = null;
public function getPassword(): ?string
{
return $this->password;
}
public function setPassword(?string $password): void
{
$this->password = $password;
}
}

View file

@ -0,0 +1,55 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Paweł Jędrzejewski
*
* 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\AdminBundle\Form\Type;
use Sylius\Bundle\AdminBundle\Form\Model\PasswordReset;
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;
final class ResetPasswordType extends AbstractType
{
public function __construct(private array $validationGroups = [])
{
}
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('password', RepeatedType::class, [
'type' => PasswordType::class,
'first_name' => 'newPassword',
'first_options' => ['label' => 'sylius.form.admin.reset_password.password.label'],
'second_name' => 'confirmNewPassword',
'second_options' => ['label' => 'sylius.form.admin.reset_password.password.confirmation'],
'invalid_message' => 'sylius.admin.reset_password.mismatch',
])
;
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => PasswordReset::class,
'validation_groups' => $this->validationGroups,
]);
}
public function getBlockPrefix(): string
{
return 'sylius_admin_reset_password';
}
}

View file

@ -1311,3 +1311,27 @@ sylius_ui:
submit:
template: '@SyliusAdmin/Security/RequestPasswordReset/_submit.html.twig'
priority: 10
sylius.admin.reset_password.content:
blocks:
form:
template: '@SyliusAdmin/Security/ResetPassword/_content.html.twig'
priority: 10
sylius.admin.reset_password.form:
blocks:
logo:
template: '@SyliusAdmin/Security/_logo.html.twig'
priority: 20
content:
template: '@SyliusAdmin/Security/ResetPassword/_form.html.twig'
priority: 10
sylius.admin.reset_password.form.content:
blocks:
passwords:
template: '@SyliusAdmin/Security/ResetPassword/Form/_passwords.html.twig'
priority: 20
submit:
template: '@SyliusAdmin/Security/ResetPassword/Form/_submit.html.twig'
priority: 10

View file

@ -27,3 +27,15 @@ sylius_admin_request_password_reset:
path: /forgotten-password
methods: [POST]
controller: Sylius\Bundle\AdminBundle\Action\Account\RequestPasswordResetAction
sylius_admin_render_password_reset:
path: /forgotten-password/{token}
methods: [GET]
defaults:
_controller: Sylius\Bundle\AdminBundle\Action\Account\RenderResetPasswordPageAction
sylius_admin_password_reset:
path: /forgotten-password/{token}
methods: [POST]
defaults:
_controller: Sylius\Bundle\AdminBundle\Action\Account\ResetPasswordAction

View file

@ -19,6 +19,23 @@
<services>
<defaults public="true" />
<service id="Sylius\Bundle\AdminBundle\Action\Account\RenderResetPasswordPageAction" public="true">
<argument type="service" id="sylius.repository.admin_user" />
<argument type="service" id="form.factory" />
<argument type="service" id="session.flash_bag" />
<argument type="service" id="router" />
<argument type="service" id="twig" />
<argument type="string">%sylius.admin_user.token.password_reset.ttl%</argument>
</service>
<service id="Sylius\Bundle\AdminBundle\Action\Account\ResetPasswordAction">
<argument type="service" id="form.factory" />
<argument type="service" id="Sylius\Bundle\CoreBundle\MessageDispatcher\ResetPasswordDispatcher" />
<argument type="service" id="session.flash_bag" />
<argument type="service" id="router" />
<argument type="service" id="twig" />
</service>
<service id="Sylius\Bundle\AdminBundle\Action\RemoveAvatarAction" public="true">
<argument type='service' id="sylius.repository.avatar_image" />
<argument type="service" id="router" />

View file

@ -19,6 +19,9 @@
<parameter key="sylius.form.type.admin.password_reset_request.validation_groups" type="collection">
<parameter>sylius</parameter>
</parameter>
<parameter key="sylius.form.type.admin.reset_password.validation_groups" type="collection">
<parameter>sylius</parameter>
</parameter>
</parameters>
<services>
<service id="Sylius\Bundle\AdminBundle\Form\Extension\CatalogPromotionActionTypeExtension">
@ -37,5 +40,10 @@
<argument>%sylius.form.type.admin.password_reset_request.validation_groups%</argument>
<tag name="form.type" />
</service>
<service id="Sylius\Bundle\AdminBundle\Form\Type\ResetPasswordType">
<argument>%sylius.form.type.admin.reset_password.validation_groups%</argument>
<tag name="form.type" />
</service>
</services>
</container>

View file

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!--
~ This file is part of the Sylius package.
~
~ (c) Paweł Jędrzejewski
~
~ For the full copyright and license information, please view the LICENSE
~ file that was distributed with this source code.
-->
<constraint-mapping xmlns="http://symfony.com/schema/dic/constraint-mapping"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/constraint-mapping https://symfony.com/schema/dic/constraint-mapping/constraint-mapping-1.0.xsd">
<class name="Sylius\Bundle\AdminBundle\Form\Model\PasswordReset">
<property name="password">
<constraint name="NotBlank">
<option name="message">sylius.admin.reset_password.password.not_blank</option>
<option name="groups">sylius</option>
</constraint>
<constraint name="Length">
<option name="min">4</option>
<option name="minMessage">sylius.admin.reset_password.password.min</option>
<option name="max">254</option>
<option name="maxMessage">sylius.admin.reset_password.password.max</option>
<option name="groups">sylius</option>
</constraint>
</property>
</class>
</constraint-mapping>

View file

@ -5,6 +5,9 @@ sylius:
admin:
request_reset_password:
success: 'If the email you have specified exists in our system, we have sent there an instruction on how to reset your password.'
password_reset:
success: 'Your password has been changed successfully!'
token_expired: 'The password reset token has expired.'
email:
order_confirmation_resent: 'Order confirmation has been successfully resent to the customer.'
shipment_confirmation_resent: 'Shipment confirmation has been successfully resent to the customer.'

View file

@ -6,6 +6,12 @@ sylius:
thank_you_for_transaction: 'Thank you for a successful transaction.'
you_can_check_its_location_with_the_tracking_code: 'You can check it''s location with the %tracking_code% tracking code.'
your_order_with_number: 'Your order with number'
form:
admin:
reset_password:
password:
label: New password
confirmation: Confirm new password
menu:
admin:
main:

View file

@ -0,0 +1,2 @@
{{ form_row(form.password.newPassword, sylius_test_form_attribute('new-password')) }}
{{ form_row(form.password.confirmNewPassword, sylius_test_form_attribute('confirm-new-password')) }}

View file

@ -0,0 +1 @@
<button type="submit" class="ui fluid large primary submit button">{{ 'sylius.ui.reset'|trans }}</button>

View file

@ -0,0 +1,5 @@
<div class="ui middle aligned center aligned grid">
<div class="column">
{{ sylius_template_event('sylius.admin.reset_password.form', _context) }}
</div>
</div>

View file

@ -0,0 +1,8 @@
{% form_theme form '@SyliusAdmin/Form/theme.html.twig' %}
{{ form_start(form, {action: path('sylius_admin_password_reset', {'token': app.request.attributes.get('token')}), 'attr': {'class': 'ui large loadable form', 'novalidate': 'novalidate'}}) }}
<div class="ui left aligned very padded segment">
{{ sylius_template_event('sylius.admin.reset_password.form.content', _context) }}
</div>
{{ form_row(form._token) }}
{{ form_end(form, {'render_rest': false}) }}

View file

@ -0,0 +1,15 @@
{% extends '@SyliusUi/Layout/centered.html.twig' %}
{% block title %}Sylius | {{ 'sylius.ui.administration_reset_password'|trans }}{% endblock %}
{% block stylesheets %}
{{ sylius_template_event('sylius.admin.layout.stylesheets') }}
{% endblock %}
{% block content %}
{{ sylius_template_event('sylius.admin.reset_password.content', _context) }}
{% endblock %}
{% block javascripts %}
{{ sylius_template_event('sylius.admin.layout.javascripts') }}
{% endblock %}

View file

@ -182,6 +182,7 @@ final class TestKernel extends BaseKernel
*/
protected function configureRoutes(object $routes): void
{
$routes->import('@SyliusAdminBundle/Resources/config/routing.yml');
}
public function getCacheDir(): string

View file

@ -15,7 +15,7 @@ namespace Sylius\Bundle\ApiBundle\DataProvider;
use ApiPlatform\Core\DataProvider\ItemDataProviderInterface;
use ApiPlatform\Core\DataProvider\RestrictedDataProviderInterface;
use Sylius\Bundle\ApiBundle\Command\Admin\ResetPassword;
use Sylius\Bundle\CoreBundle\Message\Admin\Account\ResetPassword;
/** @experimental */
final class AdminResetPasswordItemDataProvider implements RestrictedDataProviderInterface, ItemDataProviderInterface

View file

@ -15,7 +15,7 @@
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://api-platform.com/schema/metadata https://api-platform.com/schema/metadata/metadata-2.0.xsd"
>
<resource class="Sylius\Bundle\ApiBundle\Command\Admin\ResetPassword" shortName="AdminResetPasswordRequest">
<resource class="Sylius\Bundle\CoreBundle\Message\Admin\Account\ResetPassword" shortName="AdminResetPasswordRequest">
<attribute name="validation_groups">sylius</attribute>
<attribute name="messenger">input</attribute>
@ -39,7 +39,7 @@
<itemOperation name="admin_update_reset_password_request">
<attribute name="method">PATCH</attribute>
<attribute name="path">/admin/reset-password-requests/{resetPasswordToken}</attribute>
<attribute name="input">Sylius\Bundle\ApiBundle\Command\Admin\ResetPassword</attribute>
<attribute name="input">Sylius\Bundle\CoreBundle\Message\Admin\Account\ResetPassword</attribute>
<attribute name="output">false</attribute>
<attribute name="status">202</attribute>
<attribute name="denormalization_context">

View file

@ -188,12 +188,6 @@
<tag name="messenger.message_handler" bus="sylius_default.bus" />
</service>
<service id="Sylius\Bundle\ApiBundle\CommandHandler\Admin\ResetPasswordHandler">
<argument type="service" id="sylius.user_password_resetter.admin" />
<tag name="messenger.message_handler" bus="sylius.command_bus" />
<tag name="messenger.message_handler" bus="sylius_default.bus" />
</service>
<service id="Sylius\Bundle\ApiBundle\CommandHandler\Checkout\SendOrderConfirmationHandler">
<argument type="service" id="sylius.email_sender" />
<argument type="service" id="sylius.repository.order" />

View file

@ -120,6 +120,12 @@
<tag name="validator.constraint_validator" alias="sylius_api_unique_reviewer_email_validator" />
</service>
<service id="Sylius\Bundle\ApiBundle\Validator\Constraints\AdminResetPasswordTokenNonExpiredValidator">
<argument type="service" id="sylius.repository.admin_user" />
<argument type="string">%sylius.admin_user.token.password_reset.ttl%</argument>
<tag name="validator.constraint_validator" alias="sylius_api_validator_admin_non_expired_password_reset_token" />
</service>
<service id="Sylius\Bundle\ApiBundle\Validator\CatalogPromotion\PercentageDiscountActionValidator">
<argument type="service" id="Sylius\Bundle\CoreBundle\SectionResolver\SectionProviderInterface" />
<tag name="sylius.catalog_promotion.action_validator" key="percentage_discount"/>

View file

@ -0,0 +1,53 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!--
~ This file is part of the Sylius package.
~
~ (c) Paweł Jędrzejewski
~
~ For the full copyright and license information, please view the LICENSE
~ file that was distributed with this source code.
-->
<constraint-mapping xmlns="http://symfony.com/schema/dic/constraint-mapping"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/constraint-mapping https://symfony.com/schema/dic/constraint-mapping/constraint-mapping-1.0.xsd">
<class name="Sylius\Bundle\CoreBundle\Message\Admin\Account\ResetPassword">
<constraint name="Sylius\Bundle\ApiBundle\Validator\Constraints\AdminResetPasswordTokenNonExpired">
<option name="groups">
<value>sylius</value>
</option>
</constraint>
<property name="newPassword">
<constraint name="NotBlank">
<option name="message">sylius.admin.reset_password.password.not_blank</option>
<option name="groups">sylius</option>
</constraint>
<constraint name="Length">
<option name="min">4</option>
<option name="minMessage">sylius.admin.reset_password.password.min</option>
<option name="max">254</option>
<option name="maxMessage">sylius.admin.reset_password.password.max</option>
<option name="groups">sylius</option>
</constraint>
<constraint name="Expression">
<option name="expression">this.newPassword === this.confirmNewPassword</option>
<option name="message">sylius.admin.reset_password.mismatch</option>
<option name="groups">sylius</option>
</constraint>
</property>
<property name="confirmNewPassword">
<constraint name="NotBlank">
<option name="message">sylius.admin.reset_password.password.not_blank</option>
<option name="groups">sylius</option>
</constraint>
<constraint name="Length">
<option name="min">4</option>
<option name="minMessage">sylius.admin.reset_password.password.min</option>
<option name="max">254</option>
<option name="maxMessage">sylius.admin.reset_password.password.max</option>
<option name="groups">sylius</option>
</constraint>
</property>
</class>
</constraint-mapping>

View file

@ -1,4 +1,6 @@
sylius:
admin:
expired_password_reset_token: 'The password reset token has expired.'
account:
invalid_verification_token: 'There is no shop user with %verificationToken% email verification token.'
is_verified: 'Account with email %email% is currently verified.'

View file

@ -0,0 +1,31 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Paweł Jędrzejewski
*
* 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\ApiBundle\Validator\Constraints;
use Symfony\Component\Validator\Constraint;
final class AdminResetPasswordTokenNonExpired extends Constraint
{
public string $message = 'sylius.admin.expired_password_reset_token';
public function validatedBy(): string
{
return 'sylius_api_validator_admin_non_expired_password_reset_token';
}
public function getTargets(): string
{
return self::CLASS_CONSTRAINT;
}
}

View file

@ -0,0 +1,53 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Paweł Jędrzejewski
*
* 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\ApiBundle\Validator\Constraints;
use Sylius\Bundle\CoreBundle\Message\Admin\Account\ResetPassword;
use Sylius\Component\Core\Model\AdminUserInterface;
use Sylius\Component\User\Repository\UserRepositoryInterface;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
use Webmozart\Assert\Assert;
final class AdminResetPasswordTokenNonExpiredValidator extends ConstraintValidator
{
public function __construct(
private UserRepositoryInterface $adminUserRepository,
private string $tokenTtl,
) {
}
/** @param ResetPassword|mixed $value */
public function validate($value, Constraint $constraint): void
{
Assert::isInstanceOf($value, ResetPassword::class);
/** @var AdminResetPasswordTokenNonExpired $constraint */
Assert::isInstanceOf($constraint, AdminResetPasswordTokenNonExpired::class);
/** @var AdminUserInterface|null $user */
$user = $this->adminUserRepository->findOneBy(['passwordResetToken' => $value->resetPasswordToken]);
if (null === $user) {
return;
}
$lifetime = new \DateInterval($this->tokenTtl);
if (!$user->isPasswordRequestNonExpired($lifetime)) {
$this->context->addViolation(
$constraint->message,
);
}
}
}

View file

@ -14,7 +14,7 @@ declare(strict_types=1);
namespace spec\Sylius\Bundle\ApiBundle\DataProvider;
use PhpSpec\ObjectBehavior;
use Sylius\Bundle\ApiBundle\Command\Admin\ResetPassword;
use Sylius\Bundle\CoreBundle\Message\Admin\Account\ResetPassword;
use Sylius\Component\Core\Model\AddressInterface;
final class AdminResetPasswordItemDataProviderSpec extends ObjectBehavior

View file

@ -0,0 +1,114 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Paweł Jędrzejewski
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace spec\Sylius\Bundle\ApiBundle\Validator\Constraints;
use PhpSpec\ObjectBehavior;
use Prophecy\Argument;
use Sylius\Bundle\ApiBundle\Validator\Constraints\AdminResetPasswordTokenNonExpired;
use Sylius\Bundle\CoreBundle\Message\Admin\Account\ResetPassword;
use Sylius\Component\Core\Model\AdminUserInterface;
use Sylius\Component\User\Repository\UserRepositoryInterface;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidatorInterface;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
final class AdminResetPasswordTokenNonExpiredValidatorSpec extends ObjectBehavior
{
public function let(UserRepositoryInterface $userRepository): void
{
$this->beConstructedWith($userRepository, 'P5D');
}
function it_is_a_constraint_validator(): void
{
$this->shouldImplement(ConstraintValidatorInterface::class);
}
public function it_throws_exception_when_value_is_not_a_reset_password(): void
{
$constraint = new AdminResetPasswordTokenNonExpired();
$this
->shouldThrow(\InvalidArgumentException::class)
->during('validate', ['', $constraint])
;
}
public function it_throws_exception_when_constraint_is_not_admin_reset_password_token_non_expired(
Constraint $constraint,
): void {
$value = new ResetPassword('token');
$this
->shouldThrow(\InvalidArgumentException::class)
->during('validate', [$value, $constraint])
;
}
public function it_does_nothing_when_a_user_for_given_token_does_not_exist(
UserRepositoryInterface $userRepository,
ExecutionContextInterface $executionContext,
): void {
$value = new ResetPassword('token');
$constraint = new AdminResetPasswordTokenNonExpired();
$this->initialize($executionContext);
$userRepository->findOneBy(['passwordResetToken' => 'token'])->willReturn(null);
$executionContext->addViolation(Argument::any())->shouldNotBeCalled();
$this->validate($value, $constraint);
}
public function it_does_nothing_when_user_password_reset_token_is_non_expired(
UserRepositoryInterface $userRepository,
AdminUserInterface $adminUser,
ExecutionContextInterface $executionContext,
): void {
$value = new ResetPassword('token');
$constraint = new AdminResetPasswordTokenNonExpired();
$this->initialize($executionContext);
$adminUser->isPasswordRequestNonExpired(
Argument::that(static fn (\DateInterval $dateInterval) => $dateInterval->format('%d') === '5'),
)->willReturn(true);
$userRepository->findOneBy(['passwordResetToken' => 'token'])->willReturn($adminUser);
$executionContext->addViolation(Argument::any())->shouldNotBeCalled();
$this->validate($value, $constraint);
}
public function it_adds_a_violation_when_user_password_reset_token_is_expired(
UserRepositoryInterface $userRepository,
AdminUserInterface $adminUser,
ExecutionContextInterface $executionContext,
): void {
$value = new ResetPassword('token');
$constraint = new AdminResetPasswordTokenNonExpired();
$this->initialize($executionContext);
$adminUser->isPasswordRequestNonExpired(
Argument::that(static fn (\DateInterval $dateInterval) => $dateInterval->format('%d') === '5'),
)->willReturn(false);
$userRepository->findOneBy(['passwordResetToken' => 'token'])->willReturn($adminUser);
$executionContext->addViolation($constraint->message)->shouldBeCalled();
$this->validate($value, $constraint);
}
}

View file

@ -11,9 +11,8 @@
declare(strict_types=1);
namespace Sylius\Bundle\ApiBundle\Command\Admin;
namespace Sylius\Bundle\CoreBundle\Message\Admin\Account;
/** @experimental */
class ResetPassword
{
public function __construct(

View file

@ -0,0 +1,29 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Paweł Jędrzejewski
*
* 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\MessageDispatcher;
use Sylius\Bundle\CoreBundle\Message\Admin\Account\ResetPassword;
use Symfony\Component\Messenger\MessageBusInterface;
final class ResetPasswordDispatcher 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,19 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Paweł Jędrzejewski
*
* 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\MessageDispatcher;
interface ResetPasswordDispatcherInterface
{
public function dispatch(string $token, string $password): void;
}

View file

@ -11,13 +11,12 @@
declare(strict_types=1);
namespace Sylius\Bundle\ApiBundle\CommandHandler\Admin;
namespace Sylius\Bundle\CoreBundle\MessageHandler\Admin\Account;
use Sylius\Bundle\ApiBundle\Command\Admin\ResetPassword;
use Sylius\Bundle\CoreBundle\Message\Admin\Account\ResetPassword;
use Sylius\Bundle\CoreBundle\Security\UserPasswordResetterInterface;
use Symfony\Component\Messenger\Handler\MessageHandlerInterface;
/** @experimental */
final class ResetPasswordHandler implements MessageHandlerInterface
{
public function __construct(private UserPasswordResetterInterface $userPasswordResetter)

View file

@ -15,7 +15,7 @@
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/serializer-mapping https://symfony.com/schema/dic/serializer-mapping/serializer-mapping-1.0.xsd"
>
<class name="Sylius\Bundle\ApiBundle\Command\Admin\ResetPassword">
<class name="Sylius\Bundle\CoreBundle\Message\Admin\Account\ResetPassword">
<attribute name="newPassword">
<group>admin:reset_password:update</group>
</attribute>

View file

@ -280,5 +280,9 @@
<argument type="service" id="Sylius\Component\User\Security\PasswordUpdaterInterface" />
<argument type="string">%sylius.shop_user.token.password_reset.ttl%</argument>
</service>
<service id="Sylius\Bundle\CoreBundle\MessageDispatcher\ResetPasswordDispatcher">
<argument type="service" id="sylius.command_bus" />
</service>
</services>
</container>

View file

@ -28,6 +28,13 @@
<tag name="messenger.message_handler" bus="sylius_default.bus" />
</service>
<service id="Sylius\Bundle\CoreBundle\MessageHandler\Admin\Account\ResetPasswordHandler">
<argument type="service" id="sylius.user_password_resetter.admin" />
<tag name="messenger.message_handler" bus="sylius.command_bus" />
<tag name="messenger.message_handler" bus="sylius_default.bus" />
</service>
<service id="Sylius\Bundle\CoreBundle\MessageHandler\Admin\Account\SendResetPasswordEmailHandler">
<argument type="service" id="sylius.repository.admin_user"/>
<argument type="service" id="sylius.email_sender"/>

View file

@ -6,6 +6,12 @@ sylius:
max: Email must be at most {{ limit }} characters long.
min: Email must be at least {{ limit }} characters long.
not_blank: Please enter an email.
reset_password:
mismatch: The entered passwords do not match.
password:
min: Password must be at least {{ limit }} characters long.
max: Password must be at most {{ limit }} characters long.
not_blank: Please enter the password.
avatar_image:
file:
max_size: The image is too big - {{ size }}{{ suffix }}. Maximum allowed size is {{ limit }}{{ suffix }}.

View file

@ -38,7 +38,8 @@
<div style="text-align: center;">
{% if sylius_bundle_loaded_checker('SyliusAdminBundle') %}
<a href="#" style="display: inline-block; text-align: center; background: #1abb9c; padding: 18px 28px; color: #fff; text-decoration: none; border-radius: 3px;">
{% set url = url('sylius_admin_render_password_reset', {'token': adminUser.passwordResetToken}) %}
<a href="{{ url|raw }}" style="display: inline-block; text-align: center; background: #1abb9c; padding: 18px 28px; color: #fff; text-decoration: none; border-radius: 3px;">
{{ 'sylius.email.admin_password_reset.reset_your_password'|trans({}, null, localeCode) }}
</a>
{% else %}

View file

@ -0,0 +1,37 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Paweł Jędrzejewski
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace spec\Sylius\Bundle\CoreBundle\MessageDispatcher;
use PhpSpec\ObjectBehavior;
use Sylius\Bundle\CoreBundle\Message\Admin\Account\ResetPassword;
use Sylius\Bundle\CoreBundle\MessageDispatcher\ResetPasswordDispatcher;
use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\MessageBusInterface;
final class ResetPasswordDispatcherSpec extends ObjectBehavior
{
function let(MessageBusInterface $messageBus): void
{
$this->beConstructedWith($messageBus);
}
function it_dispatches_a_reset_password_message(MessageBusInterface $messageBus): void
{
$message = new ResetPassword('token', 'password');
$messageBus->dispatch($message)->willReturn(new Envelope($message))->shouldBeCalled();
$this->dispatch('token', 'password');
}
}

View file

@ -11,10 +11,10 @@
declare(strict_types=1);
namespace spec\Sylius\Bundle\ApiBundle\CommandHandler\Admin;
namespace spec\Sylius\Bundle\CoreBundle\MessageHandler\Admin\Account;
use PhpSpec\ObjectBehavior;
use Sylius\Bundle\ApiBundle\Command\Admin\ResetPassword;
use Sylius\Bundle\CoreBundle\Message\Admin\Account\ResetPassword;
use Sylius\Bundle\CoreBundle\Security\UserPasswordResetterInterface;
use Symfony\Component\Messenger\Handler\MessageHandlerInterface;