mirror of
https://github.com/Sylius/Sylius.git
synced 2026-07-14 17:10:53 +00:00
refactor #14139 [Admin][API] Password reset (NoResponseMate)
This PR was merged into the 1.12 branch.
Discussion
----------
| Q | A |
|-----------------|--------------------------------------------------------------|
| Branch? | master |
| Bug fix? | no |
| New feature? | yes |
| BC breaks? | no |
| Deprecations? | no |
| Related tickets | - |
| License | MIT |
<br>
- Split the `ResetPassword` api resource in two, one focusing customer's password (`Sylius\Bundle\ApiBundle\Command\Account\ResetPassword`) and the other administrator's (`Sylius\Bundle\ApiBundle\Command\Admin\ResetPassword`)
- Add API endpoint for resetting administrator's password at `<api>/admin/reset-password-requests/{resetPasswordToken}`
- Extract password reset logic to a standalone service `Sylius\Bundle\CoreBundle\Security\UserPasswordResetter`
Commits
-------
b2c36834fd Add administrator password reset api tests
27248acd57 Change user transformer in admin context
3f2e7c787d Add basic handling of admin's password reset
4246bda8c6 Add admin's side password reset
01714dbe37 [API] Split ResetPassword resource
8b327372c7 [Core][API] Extract Password reset handling to a standalone service
707161a1ac [API] Update ResetPasswordHandler upgrade notice
3ff8920737 Fix coding style and minor issues
This commit is contained in:
commit
9fb6eb3ca3
26 changed files with 536 additions and 147 deletions
|
|
@ -70,7 +70,9 @@ Here is how the response looks like:
|
|||
Now when we do not provide parameters in response it returns all available `paymentMethods` in channel.
|
||||
Wrong parameters otherwise cause empty array `[]` in response and correct parameters return `paymentMethods` available for your `payment`.
|
||||
|
||||
1. The 2nd parameter `MetadataInterface` has been removed from `src/Sylius/Bundle/ApiBundle/CommandHandler/Account/ResetPasswordHandler` and replaced by `Sylius\Component\User\Security\PasswordUpdaterInterface` (previously 3rd parameter). From now on a token TTL value must be used instead as the 3rd parameter.
|
||||
1. All arguments of `src/Sylius/Bundle/ApiBundle/CommandHandler/Account/ResetPasswordHandler` have been removed and substituted with `Sylius\Bundle\CoreBundle\Security\UserPasswordResetter`.
|
||||
|
||||
1. The file `src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/ResetPassword.xml` has been renamed to `src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/AccountResetPassword.xml` and its short name has been changed from `ResetPasswordRequest` to `AccountResetPasswordRequest`.
|
||||
|
||||
1. Constructor of `Sylius\Bundle\ApiBundle\CommandHandler\Account\RequestResetPasswordTokenHandler` has been extended with `Sylius\Calendar\Provider\DateTimeProviderInterface` argument:
|
||||
|
||||
|
|
|
|||
|
|
@ -15,18 +15,21 @@ Feature: Resetting an administrator's password
|
|||
Then I should be notified that email with reset instruction has been sent
|
||||
And an email with instructions on how to reset the administrator's password should be sent to "sylius@example.com"
|
||||
|
||||
@todo
|
||||
@api
|
||||
Scenario: Changing my administrator's password
|
||||
Given I have already received an administrator's password resetting email
|
||||
When I reset my password using the received instructions
|
||||
Given I have already received a resetting password email
|
||||
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 "newp@ssw0rd"
|
||||
And I reset it
|
||||
Then I should be notified that my password has been successfully changed
|
||||
And I should be able to log in as "sylius@example.com" with "newp@ssw0rd" password
|
||||
And I should be able to log in as "sylius@example.com" authenticated by "newp@ssw0rd" password
|
||||
|
||||
@todo
|
||||
@api
|
||||
Scenario: Trying to change my administrator's password twice without sending a new password reset request
|
||||
Given I already reset my administrator's password
|
||||
When I try to reset my password again using the same email
|
||||
Then I should not be able to change it again 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
|
||||
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 not be able to change my password again with the same token
|
||||
|
|
|
|||
|
|
@ -18,6 +18,9 @@ use Sylius\Behat\Client\ApiClientInterface;
|
|||
use Sylius\Behat\Client\ApiSecurityClientInterface;
|
||||
use Sylius\Behat\Client\RequestFactoryInterface;
|
||||
use Sylius\Behat\Client\RequestInterface;
|
||||
use Sylius\Behat\Client\ResponseCheckerInterface;
|
||||
use Sylius\Component\Core\Model\AdminUserInterface;
|
||||
use Symfony\Component\HttpFoundation\Request as HTTPRequest;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Webmozart\Assert\Assert;
|
||||
|
||||
|
|
@ -29,6 +32,8 @@ final class LoginContext implements Context
|
|||
private ApiSecurityClientInterface $apiSecurityClient,
|
||||
private ApiClientInterface $client,
|
||||
private RequestFactoryInterface $requestFactory,
|
||||
private ResponseCheckerInterface $responseChecker,
|
||||
private string $apiUrlPrefix
|
||||
) {
|
||||
}
|
||||
|
||||
|
|
@ -89,6 +94,33 @@ final class LoginContext implements Context
|
|||
$this->client->executeCustomRequest($this->request);
|
||||
}
|
||||
|
||||
/**
|
||||
* @When /^(I) follow the instructions to reset my password$/
|
||||
*/
|
||||
public function iFollowTheInstructionsToResetMyPassword(AdminUserInterface $admin): void
|
||||
{
|
||||
$this->request = $this->requestFactory->custom(
|
||||
sprintf('%s/admin/reset-password-requests/%s', $this->apiUrlPrefix, $admin->getPasswordResetToken()),
|
||||
HttpRequest::METHOD_PATCH,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @When I specify my new password as :password
|
||||
*/
|
||||
public function iSpecifyMyNewPassword(string $password): void
|
||||
{
|
||||
$this->request->updateContent(['newPassword' => $password]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @When I confirm my new password as :password
|
||||
*/
|
||||
public function iConfirmMyNewPassword(string $password): void
|
||||
{
|
||||
$this->request->updateContent(['confirmNewPassword' => $password]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Then I should be logged in
|
||||
*/
|
||||
|
|
@ -139,6 +171,28 @@ final class LoginContext implements Context
|
|||
Assert::same($this->client->getLastResponse()->getStatusCode(), Response::HTTP_ACCEPTED);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Then I should be notified that my password has been successfully changed
|
||||
*/
|
||||
public function iShouldBeNotifiedThatMyPasswordHasBeenSuccessfullyChanged(): void
|
||||
{
|
||||
Assert::same($this->client->getLastResponse()->getStatusCode(), Response::HTTP_ACCEPTED);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Then I should not be able to change my password again with the same token
|
||||
*/
|
||||
public function iShouldNotBeAbleToChangeMyPasswordAgainWithTheSameToken(): void
|
||||
{
|
||||
$this->client->executeCustomRequest($this->request);
|
||||
|
||||
$lastResponse = $this->client->getLastResponse();
|
||||
|
||||
Assert::same($lastResponse->getStatusCode(), Response::HTTP_INTERNAL_SERVER_ERROR);
|
||||
$message = $this->responseChecker->getError($lastResponse);
|
||||
Assert::startsWith($message, 'No user found with reset token: ');
|
||||
}
|
||||
|
||||
private function logIn(string $username, string $password): void
|
||||
{
|
||||
$this->iWantToLogIn();
|
||||
|
|
|
|||
|
|
@ -98,4 +98,15 @@ final class AdminUserContext implements Context
|
|||
|
||||
$this->sharedStorage->set($avatarPath, $avatar->getPath());
|
||||
}
|
||||
|
||||
/**
|
||||
* @Given /^(I) have already received an administrator's password resetting email$/
|
||||
*/
|
||||
public function iHaveAlreadyReceivedAnAdministratorsPasswordResettingEmail(AdminUserInterface $administrator): void
|
||||
{
|
||||
$administrator->setPasswordResetToken('token');
|
||||
$administrator->setPasswordRequestedAt(new \DateTime());
|
||||
|
||||
$this->objectManager->flush();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@
|
|||
<argument type="service" id="sylius.behat.api_platform_client.shop" />
|
||||
<argument type="service" id="sylius.behat.request_factory" />
|
||||
<argument type="service" id="Sylius\Behat\Client\ResponseCheckerInterface" />
|
||||
<argument>%sylius.security.new_api_route%</argument>
|
||||
</service>
|
||||
|
||||
<service id="sylius.behat.context.api.admin.managing_administrators" class="Sylius\Behat\Context\Api\Admin\ManagingAdministratorsContext">
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -16,17 +16,10 @@ namespace Sylius\Bundle\ApiBundle\Command\Account;
|
|||
/** @experimental */
|
||||
class ResetPassword
|
||||
{
|
||||
/** @var string|null */
|
||||
public $newPassword;
|
||||
|
||||
/** @var string|null */
|
||||
public $confirmNewPassword;
|
||||
|
||||
/** @var string */
|
||||
public $resetPasswordToken;
|
||||
|
||||
public function __construct(string $resetPasswordToken)
|
||||
{
|
||||
$this->resetPasswordToken = $resetPasswordToken;
|
||||
public function __construct(
|
||||
public string $resetPasswordToken,
|
||||
public ?string $newPassword = null,
|
||||
public ?string $confirmNewPassword = null,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
|
|
|||
25
src/Sylius/Bundle/ApiBundle/Command/Admin/ResetPassword.php
Normal file
25
src/Sylius/Bundle/ApiBundle/Command/Admin/ResetPassword.php
Normal 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\Bundle\ApiBundle\Command\Admin;
|
||||
|
||||
/** @experimental */
|
||||
class ResetPassword
|
||||
{
|
||||
public function __construct(
|
||||
public string $resetPasswordToken,
|
||||
public ?string $newPassword = null,
|
||||
public ?string $confirmNewPassword = null,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
|
@ -14,38 +14,18 @@ declare(strict_types=1);
|
|||
namespace Sylius\Bundle\ApiBundle\CommandHandler\Account;
|
||||
|
||||
use Sylius\Bundle\ApiBundle\Command\Account\ResetPassword;
|
||||
use Sylius\Component\Core\Model\ShopUserInterface;
|
||||
use Sylius\Component\User\Repository\UserRepositoryInterface;
|
||||
use Sylius\Component\User\Security\PasswordUpdaterInterface;
|
||||
use Sylius\Bundle\CoreBundle\Security\UserPasswordResetterInterface;
|
||||
use Symfony\Component\Messenger\Handler\MessageHandlerInterface;
|
||||
use Webmozart\Assert\Assert;
|
||||
|
||||
/** @experimental */
|
||||
final class ResetPasswordHandler implements MessageHandlerInterface
|
||||
{
|
||||
public function __construct(
|
||||
private UserRepositoryInterface $userRepository,
|
||||
private PasswordUpdaterInterface $passwordUpdater,
|
||||
private string $tokenTtl
|
||||
) {
|
||||
public function __construct(private UserPasswordResetterInterface $userPasswordResetter)
|
||||
{
|
||||
}
|
||||
|
||||
public function __invoke(ResetPassword $command): void
|
||||
{
|
||||
/** @var ShopUserInterface|null $user */
|
||||
$user = $this->userRepository->findOneBy(['passwordResetToken' => $command->resetPasswordToken]);
|
||||
|
||||
Assert::notNull($user, 'No user found with reset token: ' . $command->resetPasswordToken);
|
||||
|
||||
$lifetime = new \DateInterval($this->tokenTtl);
|
||||
|
||||
if (!$user->isPasswordRequestNonExpired($lifetime)) {
|
||||
throw new \InvalidArgumentException('Password reset token has expired');
|
||||
}
|
||||
|
||||
$user->setPlainPassword($command->newPassword);
|
||||
|
||||
$this->passwordUpdater->updatePassword($user);
|
||||
$user->setPasswordResetToken(null);
|
||||
$this->userPasswordResetter->reset($command->resetPasswordToken, $command->newPassword);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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\CommandHandler\Admin;
|
||||
|
||||
use Sylius\Bundle\ApiBundle\Command\Admin\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)
|
||||
{
|
||||
}
|
||||
|
||||
public function __invoke(ResetPassword $command): void
|
||||
{
|
||||
$this->userPasswordResetter->reset($command->resetPasswordToken, $command->newPassword);
|
||||
}
|
||||
}
|
||||
|
|
@ -18,7 +18,7 @@ use ApiPlatform\Core\DataProvider\RestrictedDataProviderInterface;
|
|||
use Sylius\Bundle\ApiBundle\Command\Account\ResetPassword;
|
||||
|
||||
/** @experimental */
|
||||
final class ResetPasswordItemDataProvider implements RestrictedDataProviderInterface, ItemDataProviderInterface
|
||||
final class AccountResetPasswordItemDataProvider implements RestrictedDataProviderInterface, ItemDataProviderInterface
|
||||
{
|
||||
public function getItem(string $resourceClass, $id, string $operationName = null, array $context = [])
|
||||
{
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
<?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\DataProvider;
|
||||
|
||||
use ApiPlatform\Core\DataProvider\ItemDataProviderInterface;
|
||||
use ApiPlatform\Core\DataProvider\RestrictedDataProviderInterface;
|
||||
use Sylius\Bundle\ApiBundle\Command\Admin\ResetPassword;
|
||||
|
||||
/** @experimental */
|
||||
final class AdminResetPasswordItemDataProvider implements RestrictedDataProviderInterface, ItemDataProviderInterface
|
||||
{
|
||||
public function getItem(string $resourceClass, $id, string $operationName = null, array $context = [])
|
||||
{
|
||||
return new ResetPassword($id);
|
||||
}
|
||||
|
||||
public function supports(string $resourceClass, string $operationName = null, array $context = []): bool
|
||||
{
|
||||
return is_a($resourceClass, ResetPassword::class, true);
|
||||
}
|
||||
}
|
||||
|
|
@ -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\Account\ResetPassword" shortName="ResetPasswordRequest">
|
||||
<resource class="Sylius\Bundle\ApiBundle\Command\Account\ResetPassword" shortName="AccountResetPasswordRequest">
|
||||
<attribute name="validation_groups">sylius</attribute>
|
||||
<attribute name="messenger">input</attribute>
|
||||
|
||||
|
|
@ -33,20 +33,6 @@
|
|||
<attribute name="summary">Requests password reset</attribute>
|
||||
</attribute>
|
||||
</collectionOperation>
|
||||
|
||||
<collectionOperation name="admin_create_reset_password_request">
|
||||
<attribute name="method">POST</attribute>
|
||||
<attribute name="path">/admin/reset-password-requests</attribute>
|
||||
<attribute name="input">Sylius\Bundle\CoreBundle\Message\Admin\Account\RequestResetPasswordEmail</attribute>
|
||||
<attribute name="output">false</attribute>
|
||||
<attribute name="status">202</attribute>
|
||||
<attribute name="denormalization_context">
|
||||
<attribute name="groups">admin:reset_password:create</attribute>
|
||||
</attribute>
|
||||
<attribute name="openapi_context">
|
||||
<attribute name="summary">Requests administrator's password reset</attribute>
|
||||
</attribute>
|
||||
</collectionOperation>
|
||||
</collectionOperations>
|
||||
|
||||
<itemOperations>
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
<?xml version="1.0" ?>
|
||||
|
||||
<!--
|
||||
|
||||
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.
|
||||
|
||||
-->
|
||||
|
||||
<resources xmlns="https://api-platform.com/schema/metadata"
|
||||
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">
|
||||
<attribute name="validation_groups">sylius</attribute>
|
||||
<attribute name="messenger">input</attribute>
|
||||
|
||||
<collectionOperations>
|
||||
<collectionOperation name="admin_create_reset_password_request">
|
||||
<attribute name="method">POST</attribute>
|
||||
<attribute name="path">/admin/reset-password-requests</attribute>
|
||||
<attribute name="input">Sylius\Bundle\CoreBundle\Message\Admin\Account\RequestResetPasswordEmail</attribute>
|
||||
<attribute name="output">false</attribute>
|
||||
<attribute name="status">202</attribute>
|
||||
<attribute name="denormalization_context">
|
||||
<attribute name="groups">admin:reset_password:create</attribute>
|
||||
</attribute>
|
||||
<attribute name="openapi_context">
|
||||
<attribute name="summary">Requests administrator's password reset</attribute>
|
||||
</attribute>
|
||||
</collectionOperation>
|
||||
</collectionOperations>
|
||||
|
||||
<itemOperations>
|
||||
<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="output">false</attribute>
|
||||
<attribute name="status">202</attribute>
|
||||
<attribute name="denormalization_context">
|
||||
<attribute name="groups">admin:reset_password:update</attribute>
|
||||
</attribute>
|
||||
<attribute name="openapi_context">
|
||||
<attribute name="summary">Resets administrator's password</attribute>
|
||||
</attribute>
|
||||
</itemOperation>
|
||||
</itemOperations>
|
||||
|
||||
<property name="resetPasswordToken" identifier="true" />
|
||||
<property name="newPassword" required="true" />
|
||||
<property name="confirmNewPassword" required="true" />
|
||||
</resource>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
<?xml version="1.0" ?>
|
||||
|
||||
<!--
|
||||
|
||||
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.
|
||||
|
||||
-->
|
||||
|
||||
<serializer xmlns="http://symfony.com/schema/dic/serializer-mapping"
|
||||
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">
|
||||
<attribute name="newPassword">
|
||||
<group>admin:reset_password:update</group>
|
||||
</attribute>
|
||||
<attribute name="confirmNewPassword">
|
||||
<group>admin:reset_password:update</group>
|
||||
</attribute>
|
||||
</class>
|
||||
</serializer>
|
||||
|
|
@ -167,9 +167,7 @@
|
|||
</service>
|
||||
|
||||
<service id="Sylius\Bundle\ApiBundle\CommandHandler\Account\ResetPasswordHandler">
|
||||
<argument type="service" id="sylius.repository.shop_user" />
|
||||
<argument type="service" id="sylius.security.password_updater" />
|
||||
<argument type="string">%sylius.shop_user.token.password_reset.ttl%</argument>
|
||||
<argument type="service" id="sylius.user_password_resetter.shop" />
|
||||
<tag name="messenger.message_handler" bus="sylius.command_bus" />
|
||||
<tag name="messenger.message_handler" bus="sylius_default.bus" />
|
||||
</service>
|
||||
|
|
@ -190,6 +188,12 @@
|
|||
<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" />
|
||||
|
|
|
|||
|
|
@ -18,6 +18,14 @@
|
|||
<services>
|
||||
<defaults public="true" />
|
||||
|
||||
<service id="Sylius\Bundle\ApiBundle\DataProvider\AccountResetPasswordItemDataProvider">
|
||||
<tag name="api_platform.item_data_provider" priority="10" />
|
||||
</service>
|
||||
|
||||
<service id="Sylius\Bundle\ApiBundle\DataProvider\AdminResetPasswordItemDataProvider">
|
||||
<tag name="api_platform.item_data_provider" priority="10" />
|
||||
</service>
|
||||
|
||||
<service id="Sylius\Bundle\ApiBundle\DataProvider\OrderItemItemDataProvider">
|
||||
<argument type="service" id="sylius.repository.order_item" />
|
||||
<argument type="service" id="Sylius\Bundle\ApiBundle\Context\UserContextInterface" />
|
||||
|
|
@ -63,10 +71,6 @@
|
|||
<tag name="api_platform.item_data_provider" priority="10" />
|
||||
</service>
|
||||
|
||||
<service id="Sylius\Bundle\ApiBundle\DataProvider\ResetPasswordItemDataProvider">
|
||||
<tag name="api_platform.item_data_provider" priority="10" />
|
||||
</service>
|
||||
|
||||
<service id="Sylius\Bundle\ApiBundle\DataProvider\ShippingMethodsCollectionDataProvider">
|
||||
<argument type="service" id="sylius.repository.shipment" />
|
||||
<argument type="service" id="sylius.repository.shipping_method" />
|
||||
|
|
|
|||
|
|
@ -14,20 +14,15 @@ declare(strict_types=1);
|
|||
namespace spec\Sylius\Bundle\ApiBundle\CommandHandler\Account;
|
||||
|
||||
use PhpSpec\ObjectBehavior;
|
||||
use Prophecy\Argument;
|
||||
use Sylius\Bundle\ApiBundle\Command\Account\ResetPassword;
|
||||
use Sylius\Component\Core\Model\ShopUserInterface;
|
||||
use Sylius\Component\User\Repository\UserRepositoryInterface;
|
||||
use Sylius\Component\User\Security\PasswordUpdaterInterface;
|
||||
use Sylius\Bundle\CoreBundle\Security\UserPasswordResetterInterface;
|
||||
use Symfony\Component\Messenger\Handler\MessageHandlerInterface;
|
||||
|
||||
final class ResetPasswordHandlerSpec extends ObjectBehavior
|
||||
{
|
||||
function let(
|
||||
UserRepositoryInterface $userRepository,
|
||||
PasswordUpdaterInterface $passwordUpdater
|
||||
): void {
|
||||
$this->beConstructedWith($userRepository, $passwordUpdater, 'P5D');
|
||||
function let(UserPasswordResetterInterface $userPasswordResetter): void
|
||||
{
|
||||
$this->beConstructedWith($userPasswordResetter);
|
||||
}
|
||||
|
||||
function it_is_a_message_handler(): void
|
||||
|
|
@ -35,77 +30,13 @@ final class ResetPasswordHandlerSpec extends ObjectBehavior
|
|||
$this->shouldImplement(MessageHandlerInterface::class);
|
||||
}
|
||||
|
||||
function it_resets_password(
|
||||
UserRepositoryInterface $userRepository,
|
||||
ShopUserInterface $shopUser,
|
||||
PasswordUpdaterInterface $passwordUpdater
|
||||
): void {
|
||||
$userRepository->findOneBy(['passwordResetToken' => 'TOKEN'])->willReturn($shopUser);
|
||||
|
||||
$shopUser->isPasswordRequestNonExpired(Argument::that(function (\DateInterval $dateInterval) {
|
||||
return $dateInterval->format('%d') === '5';
|
||||
}))->willReturn(true);
|
||||
|
||||
$shopUser->getPasswordResetToken()->willReturn('TOKEN');
|
||||
|
||||
$shopUser->setPlainPassword('newPassword')->shouldBeCalled();
|
||||
$passwordUpdater->updatePassword($shopUser)->shouldBeCalled();
|
||||
$shopUser->setPasswordResetToken(null)->shouldBeCalled();
|
||||
|
||||
function it_delegates_password_resetting(UserPasswordResetterInterface $userPasswordResetter): void
|
||||
{
|
||||
$command = new ResetPassword('TOKEN');
|
||||
$command->newPassword = 'newPassword';
|
||||
$command->resetPasswordToken = 'TOKEN';
|
||||
|
||||
$userPasswordResetter->reset('TOKEN', 'newPassword')->shouldBeCalled();
|
||||
|
||||
$this->__invoke($command);
|
||||
}
|
||||
|
||||
function it_throws_exception_if_token_is_expired(
|
||||
UserRepositoryInterface $userRepository,
|
||||
ShopUserInterface $shopUser,
|
||||
PasswordUpdaterInterface $passwordUpdater
|
||||
): void {
|
||||
$userRepository->findOneBy(['passwordResetToken' => 'TOKEN'])->willReturn($shopUser);
|
||||
|
||||
$shopUser->isPasswordRequestNonExpired(Argument::that(function (\DateInterval $dateInterval) {
|
||||
return $dateInterval->format('%d') === '5';
|
||||
}))->willReturn(false);
|
||||
|
||||
$shopUser->getPasswordResetToken()->willReturn('TOKEN');
|
||||
$shopUser->setPlainPassword('newPassword')->shouldNotBeCalled();
|
||||
$passwordUpdater->updatePassword($shopUser)->shouldNotBeCalled();
|
||||
|
||||
$command = new ResetPassword('TOKEN');
|
||||
$command->newPassword = 'newPassword';
|
||||
$command->resetPasswordToken = 'TOKEN';
|
||||
|
||||
$this
|
||||
->shouldThrow(\InvalidArgumentException::class)
|
||||
->during('__invoke', [$command])
|
||||
;
|
||||
}
|
||||
|
||||
function it_throws_exception_if_tokens_are_not_exact(
|
||||
UserRepositoryInterface $userRepository,
|
||||
ShopUserInterface $shopUser,
|
||||
PasswordUpdaterInterface $passwordUpdater
|
||||
): void {
|
||||
$userRepository->findOneBy(['passwordResetToken' => 'TOKEN'])->willReturn($shopUser);
|
||||
|
||||
$shopUser->isPasswordRequestNonExpired(Argument::that(function (\DateInterval $dateInterval) {
|
||||
return $dateInterval->format('%d') === '5';
|
||||
}))->willReturn(false);
|
||||
|
||||
$shopUser->getPasswordResetToken()->willReturn('BADTOKEN');
|
||||
$shopUser->setPlainPassword('newPassword')->shouldNotBeCalled();
|
||||
$passwordUpdater->updatePassword($shopUser)->shouldNotBeCalled();
|
||||
|
||||
$command = new ResetPassword('TOKEN');
|
||||
$command->newPassword = 'newPassword';
|
||||
$command->resetPasswordToken = 'TOKEN';
|
||||
|
||||
$this
|
||||
->shouldThrow(\InvalidArgumentException::class)
|
||||
->during('__invoke', [$command])
|
||||
;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,42 @@
|
|||
<?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\CommandHandler\Admin;
|
||||
|
||||
use PhpSpec\ObjectBehavior;
|
||||
use Sylius\Bundle\ApiBundle\Command\Admin\ResetPassword;
|
||||
use Sylius\Bundle\CoreBundle\Security\UserPasswordResetterInterface;
|
||||
use Symfony\Component\Messenger\Handler\MessageHandlerInterface;
|
||||
|
||||
final class ResetPasswordHandlerSpec extends ObjectBehavior
|
||||
{
|
||||
function let(UserPasswordResetterInterface $userPasswordResetter): void
|
||||
{
|
||||
$this->beConstructedWith($userPasswordResetter);
|
||||
}
|
||||
|
||||
function it_is_a_message_handler(): void
|
||||
{
|
||||
$this->shouldImplement(MessageHandlerInterface::class);
|
||||
}
|
||||
|
||||
function it_delegates_password_resetting(UserPasswordResetterInterface $userPasswordResetter): void
|
||||
{
|
||||
$command = new ResetPassword('TOKEN');
|
||||
$command->newPassword = 'newPassword';
|
||||
|
||||
$userPasswordResetter->reset('TOKEN', 'newPassword')->shouldBeCalled();
|
||||
|
||||
$this->__invoke($command);
|
||||
}
|
||||
}
|
||||
|
|
@ -17,7 +17,7 @@ use PhpSpec\ObjectBehavior;
|
|||
use Sylius\Bundle\ApiBundle\Command\Account\ResetPassword;
|
||||
use Sylius\Component\Core\Model\AddressInterface;
|
||||
|
||||
final class ResetPasswordItemDataProviderSpec extends ObjectBehavior
|
||||
final class AccountResetPasswordItemDataProviderSpec extends ObjectBehavior
|
||||
{
|
||||
function it_supports_only_reset_password(): void
|
||||
{
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
<?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\DataProvider;
|
||||
|
||||
use PhpSpec\ObjectBehavior;
|
||||
use Sylius\Bundle\ApiBundle\Command\Admin\ResetPassword;
|
||||
use Sylius\Component\Core\Model\AddressInterface;
|
||||
|
||||
final class AdminResetPasswordItemDataProviderSpec extends ObjectBehavior
|
||||
{
|
||||
function it_supports_only_reset_password(): void
|
||||
{
|
||||
$this->supports(ResetPassword::class, 'post')->shouldReturn(true);
|
||||
$this->supports(AddressInterface::class, 'post')->shouldReturn(false);
|
||||
}
|
||||
|
||||
function it_provides_reset_password_class(): void
|
||||
{
|
||||
$this
|
||||
->getItem(ResetPassword::class, 'resetToken')
|
||||
->shouldBeLike(new ResetPassword('resetToken'))
|
||||
;
|
||||
}
|
||||
}
|
||||
|
|
@ -268,5 +268,17 @@
|
|||
<service id="Sylius\Bundle\CoreBundle\Order\Checker\OrderPromotionsIntegrityCheckerInterface" class="Sylius\Bundle\CoreBundle\Order\Checker\OrderPromotionsIntegrityChecker">
|
||||
<argument type="service" id="sylius.order_processing.order_processor" />
|
||||
</service>
|
||||
|
||||
<service id="sylius.user_password_resetter.admin" class="Sylius\Bundle\CoreBundle\Security\UserPasswordResetter">
|
||||
<argument type="service" id="sylius.repository.admin_user" />
|
||||
<argument type="service" id="Sylius\Component\User\Security\PasswordUpdaterInterface" />
|
||||
<argument type="string">%sylius.admin_user.token.password_reset.ttl%</argument>
|
||||
</service>
|
||||
|
||||
<service id="sylius.user_password_resetter.shop" class="Sylius\Bundle\CoreBundle\Security\UserPasswordResetter">
|
||||
<argument type="service" id="sylius.repository.shop_user" />
|
||||
<argument type="service" id="Sylius\Component\User\Security\PasswordUpdaterInterface" />
|
||||
<argument type="string">%sylius.shop_user.token.password_reset.ttl%</argument>
|
||||
</service>
|
||||
</services>
|
||||
</container>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,49 @@
|
|||
<?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\Security;
|
||||
|
||||
use Sylius\Component\User\Model\UserInterface;
|
||||
use Sylius\Component\User\Repository\UserRepositoryInterface;
|
||||
use Sylius\Component\User\Security\PasswordUpdaterInterface;
|
||||
use Webmozart\Assert\Assert;
|
||||
|
||||
final class UserPasswordResetter implements UserPasswordResetterInterface
|
||||
{
|
||||
public function __construct(
|
||||
private UserRepositoryInterface $userRepository,
|
||||
private PasswordUpdaterInterface $passwordUpdater,
|
||||
private string $tokenTtl,
|
||||
) {
|
||||
}
|
||||
|
||||
public function reset(string $token, string $password): void
|
||||
{
|
||||
/** @var UserInterface|null $user */
|
||||
$user = $this->userRepository->findOneBy(['passwordResetToken' => $token]);
|
||||
|
||||
Assert::notNull($user, 'No user found with reset token: ' . $token);
|
||||
|
||||
$lifetime = new \DateInterval($this->tokenTtl);
|
||||
|
||||
if (!$user->isPasswordRequestNonExpired($lifetime)) {
|
||||
throw new \InvalidArgumentException('Password reset token has expired');
|
||||
}
|
||||
|
||||
$user->setPlainPassword($password);
|
||||
|
||||
$this->passwordUpdater->updatePassword($user);
|
||||
$user->setPasswordResetToken(null);
|
||||
$user->setPasswordRequestedAt(null);
|
||||
}
|
||||
}
|
||||
|
|
@ -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\Security;
|
||||
|
||||
interface UserPasswordResetterInterface
|
||||
{
|
||||
public function reset(string $token, string $password): void;
|
||||
}
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
<?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\Security;
|
||||
|
||||
use PhpSpec\ObjectBehavior;
|
||||
use Prophecy\Argument;
|
||||
use Sylius\Bundle\CoreBundle\Security\UserPasswordResetterInterface;
|
||||
use Sylius\Component\User\Model\UserInterface;
|
||||
use Sylius\Component\User\Repository\UserRepositoryInterface;
|
||||
use Sylius\Component\User\Security\PasswordUpdaterInterface;
|
||||
|
||||
final class UserPasswordResetterSpec extends ObjectBehavior
|
||||
{
|
||||
function let(UserRepositoryInterface $userRepository, PasswordUpdaterInterface $passwordUpdater)
|
||||
{
|
||||
$this->beConstructedWith($userRepository, $passwordUpdater, 'P5D');
|
||||
}
|
||||
|
||||
function it_implements_user_password_resetter_interface(): void
|
||||
{
|
||||
$this->shouldImplement(UserPasswordResetterInterface::class);
|
||||
}
|
||||
|
||||
function it_resets_password(
|
||||
UserRepositoryInterface $userRepository,
|
||||
UserInterface $user,
|
||||
PasswordUpdaterInterface $passwordUpdater,
|
||||
): void {
|
||||
$userRepository->findOneBy(['passwordResetToken' => 'TOKEN'])->willReturn($user);
|
||||
|
||||
$user->isPasswordRequestNonExpired(
|
||||
Argument::that(static fn (\DateInterval $dateInterval) => $dateInterval->format('%d') === '5'),
|
||||
)->willReturn(true);
|
||||
|
||||
$user->getPasswordResetToken()->willReturn('TOKEN');
|
||||
|
||||
$user->setPlainPassword('newPassword')->shouldBeCalled();
|
||||
$passwordUpdater->updatePassword($user)->shouldBeCalled();
|
||||
$user->setPasswordResetToken(null)->shouldBeCalled();
|
||||
$user->setPasswordRequestedAt(null)->shouldBeCalled();
|
||||
|
||||
$this->reset('TOKEN', 'newPassword');
|
||||
}
|
||||
|
||||
function it_throws_exception_if_no_user_has_been_found_for_token(
|
||||
UserRepositoryInterface $userRepository,
|
||||
PasswordUpdaterInterface $passwordUpdater,
|
||||
): void {
|
||||
$userRepository->findOneBy(['passwordResetToken' => 'TOKEN'])->willReturn(null);
|
||||
|
||||
$passwordUpdater->updatePassword(Argument::any())->shouldNotBeCalled();
|
||||
|
||||
$this
|
||||
->shouldThrow(\InvalidArgumentException::class)
|
||||
->during('reset', ['TOKEN', 'newPassword'])
|
||||
;
|
||||
}
|
||||
|
||||
function it_throws_exception_if_token_is_expired(
|
||||
UserRepositoryInterface $userRepository,
|
||||
UserInterface $user,
|
||||
PasswordUpdaterInterface $passwordUpdater,
|
||||
): void {
|
||||
$userRepository->findOneBy(['passwordResetToken' => 'TOKEN'])->willReturn($user);
|
||||
|
||||
$user->isPasswordRequestNonExpired(
|
||||
Argument::that(static fn (\DateInterval $dateInterval) => $dateInterval->format('%d') === '5'),
|
||||
)->willReturn(false);
|
||||
|
||||
$user->getPasswordResetToken()->willReturn('TOKEN');
|
||||
$user->setPlainPassword('newPassword')->shouldNotBeCalled();
|
||||
$passwordUpdater->updatePassword($user)->shouldNotBeCalled();
|
||||
$user->setPasswordRequestedAt(null)->shouldNotBeCalled();
|
||||
|
||||
$this
|
||||
->shouldThrow(\InvalidArgumentException::class)
|
||||
->during('reset', ['TOKEN', 'newPassword'])
|
||||
;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue