[API] Resend verification email refactor to take current user from the security context

This commit is contained in:
Łukasz Chruściel 2021-08-18 14:10:31 +02:00
parent 40960da969
commit f247e3e581
No known key found for this signature in database
GPG key ID: 049A1D51AA3B039C
15 changed files with 192 additions and 227 deletions

View file

@ -99,6 +99,12 @@
}
```
1. Constructor of `Sylius\Bundle\ApiBundle\Command\Account\ResendVerificationEmail` has been removed. Relation to the current
customer is set through `setShopUserId()`
1. The `Sylius\Bundle\ApiBundle\Validator\Constraints\ShopUserExists` constraint and the `Sylius\Bundle\ApiBundle\Validator\Constraints\ShopUserExistsValidator`
have been removed
1. Constructor of `ApiBundle/Serializer/ProductVariantNormalizer.php` has been extended with `SectionProviderInterface`
argument:
@ -131,6 +137,15 @@
}
```
1. Request body of `POST` `api/v2/shop/account-verification-requests` endpoint has been removed:
```diff
{
- "email": "string",
- "localeCode": "string"
}
```
1. The service `Sylius\Bundle\ApiBundle\Converter\ItemIriToIdentifierConverter` has changed its name to `Sylius\Bundle\ApiBundle\Converter\IriToIdentifierConverter`
and its service definition from `Sylius\Bundle\ApiBundle\Converter\ItemIriToIdentifierConverter` to `Sylius\Bundle\ApiBundle\Converter\IriToIdentifierConverterInterface`
@ -230,7 +245,6 @@
* `sylius.api.validator.confirm_reset_password` => `Sylius\Bundle\ApiBundle\Validator\Constraints\ConfirmResetPasswordValidator`
* `sylius.api.validator.promotion_coupon_eligibility` => `Sylius\Bundle\ApiBundle\Validator\Constraints\PromotionCouponEligibilityValidator`
* `sylius.api.validator.shipment_already_shipped` => `Sylius\Bundle\ApiBundle\Validator\Constraints\ShipmentAlreadyShippedValidator`
* `sylius.api.validator.shop_user_exists` => `Sylius\Bundle\ApiBundle\Validator\Constraints\ShopUserExistsValidator`
* `sylius.api.validator.shop_user_not_verified` => `Sylius\Bundle\ApiBundle\Validator\Constraints\ShopUserNotVerifiedValidator`
* `sylius.api.validator.account_verification_token_eligibility` => `Sylius\Bundle\ApiBundle\Validator\Constraints\AccountVerificationTokenEligibilityValidator`
* `sylius.api.validator.unique_reviewer_email` => `Sylius\Bundle\ApiBundle\Validator\Constraints\UniqueReviewerEmailValidator`

View file

@ -16,29 +16,23 @@ namespace Sylius\Bundle\ApiBundle\Command\Account;
use Sylius\Bundle\ApiBundle\Command\ChannelCodeAwareInterface;
use Sylius\Bundle\ApiBundle\Command\IriToIdentifierConversionAwareInterface;
use Sylius\Bundle\ApiBundle\Command\LocaleCodeAwareInterface;
use Sylius\Bundle\ApiBundle\Command\ShopUserIdAwareInterface;
/** @experimental */
class ResendVerificationEmail implements ChannelCodeAwareInterface, LocaleCodeAwareInterface, IriToIdentifierConversionAwareInterface
class ResendVerificationEmail implements ShopUserIdAwareInterface, ChannelCodeAwareInterface, LocaleCodeAwareInterface, IriToIdentifierConversionAwareInterface
{
/** @var string */
public $email;
/**
* @var string
* @psalm-immutable
*/
public $channelCode;
/** @var string|int|null */
public $shopUserId = null;
/**
* @var string|null
* @psalm-immutable
*/
public $localeCode;
public ?string $channelCode = null;
public function __construct(string $email)
{
$this->email = $email;
}
/**
* @var string|null
*/
public ?string $localeCode = null;
public function getChannelCode(): string
{
@ -59,4 +53,14 @@ class ResendVerificationEmail implements ChannelCodeAwareInterface, LocaleCodeAw
{
$this->localeCode = $localeCode;
}
public function getShopUserId()
{
return $this->shopUserId;
}
public function setShopUserId($shopUserId): void
{
$this->shopUserId = $shopUserId;
}
}

View file

@ -15,6 +15,8 @@ namespace Sylius\Bundle\ApiBundle\CommandHandler\Account;
use Sylius\Bundle\ApiBundle\Command\Account\ResendVerificationEmail;
use Sylius\Bundle\ApiBundle\Command\Account\SendAccountVerificationEmail;
use Sylius\Component\Core\Model\CustomerInterface;
use Sylius\Component\Core\Model\ShopUserInterface;
use Sylius\Component\User\Model\UserInterface;
use Sylius\Component\User\Repository\UserRepositoryInterface;
use Sylius\Component\User\Security\Generator\GeneratorInterface;
@ -47,16 +49,18 @@ final class ResendVerificationEmailHandler implements MessageHandlerInterface
public function __invoke(ResendVerificationEmail $command): void
{
/** @var UserInterface|null $user */
Assert::notNull($user = $this->shopUserRepository->findOneByEmail($command->email));
/** @var ShopUserInterface|null $user */
Assert::notNull($user = $this->shopUserRepository->find($command->getShopUserId()));
$token = $this->tokenGenerator->generate();
$user->setEmailVerificationToken($token);
/** @var CustomerInterface $customer */
$customer = $user->getCustomer();
$this->commandBus->dispatch(new SendAccountVerificationEmail(
$command->email,
$command->localeCode,
$command->channelCode
$customer->getEmail(),
$command->getLocaleCode(),
$command->getChannelCode()
), [new DispatchAfterCurrentBusStamp()]);
}
}

View file

@ -33,6 +33,7 @@
<attribute name="openapi_context">
<attribute name="summary">Resends verification email</attribute>
</attribute>
<attribute name="security">is_granted("ROLE_USER")</attribute>
</collectionOperation>
</collectionOperations>

View file

@ -16,11 +16,5 @@
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\Account\ResendVerificationEmail">
<attribute name="email">
<group>shop:resend_verification_email:create</group>
</attribute>
<attribute name="localeCode" serialized-name="locale">
<group>shop:resend_verification_email:create</group>
</attribute>
</class>
</serializer>

View file

@ -102,11 +102,6 @@
<tag name="validator.constraint_validator" alias="sylius_api_shipment_already_shipped" />
</service>
<service id="Sylius\Bundle\ApiBundle\Validator\Constraints\ShopUserExistsValidator">
<argument type="service" id="sylius.repository.shop_user" />
<tag name="validator.constraint_validator" alias="sylius_api_shop_user_exists" />
</service>
<service id="Sylius\Bundle\ApiBundle\Validator\Constraints\ShopUserNotVerifiedValidator">
<argument type="service" id="sylius.repository.shop_user" />
<tag name="validator.constraint_validator" alias="sylius_api_shop_user_not_verified" />

View file

@ -13,11 +13,6 @@
<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 http://symfony.com/schema/dic/services/constraint-mapping-1.0.xsd">
<class name="Sylius\Bundle\ApiBundle\Command\Account\ResendVerificationEmail">
<constraint name="Sylius\Bundle\ApiBundle\Validator\Constraints\ShopUserExists">
<option name="groups">
<value>sylius</value>
</option>
</constraint>
<constraint name="Sylius\Bundle\ApiBundle\Validator\Constraints\ShopUserNotVerified">
<option name="groups">
<value>sylius</value>

View file

@ -1,6 +1,5 @@
sylius:
account:
invalid_email: 'There is no shop user with %email% email.'
invalid_verification_token: 'There is no shop user with %verificationToken% email verification token.'
is_verified: 'Account with email %email% is currently verified.'
address:

View file

@ -1,33 +0,0 @@
<?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;
/** @experimental */
final class ShopUserExists extends Constraint
{
/** @var string */
public $message = 'sylius.account.invalid_email';
public function validatedBy(): string
{
return 'sylius_api_shop_user_exists';
}
public function getTargets(): string
{
return self::CLASS_CONSTRAINT;
}
}

View file

@ -1,48 +0,0 @@
<?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\ApiBundle\Command\Account\ResendVerificationEmail;
use Sylius\Component\User\Repository\UserRepositoryInterface;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
use Webmozart\Assert\Assert;
/** @experimental */
final class ShopUserExistsValidator extends ConstraintValidator
{
/** @var UserRepositoryInterface */
private $shopUserRepository;
public function __construct(UserRepositoryInterface $shopUserRepository)
{
$this->shopUserRepository = $shopUserRepository;
}
public function validate($value, Constraint $constraint): void
{
Assert::isInstanceOf($value, ResendVerificationEmail::class);
/** @var ShopUserExists $constraint */
Assert::isInstanceOf($constraint, ShopUserExists::class);
$shopUser = $this->shopUserRepository->findOneByEmail($value->email);
if ($shopUser !== null) {
return;
}
$this->context->addViolation($constraint->message, ['%email%' => $value->email]);
}
}

View file

@ -13,7 +13,8 @@ declare(strict_types=1);
namespace Sylius\Bundle\ApiBundle\Validator\Constraints;
use Sylius\Bundle\ApiBundle\Command\Account\ResendVerificationEmail;
use Sylius\Bundle\ApiBundle\Command\ShopUserIdAwareInterface;
use Sylius\Component\Core\Model\ShopUserInterface;
use Sylius\Component\User\Repository\UserRepositoryInterface;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
@ -32,12 +33,13 @@ final class ShopUserNotVerifiedValidator extends ConstraintValidator
public function validate($value, Constraint $constraint): void
{
Assert::isInstanceOf($value, ResendVerificationEmail::class);
Assert::isInstanceOf($value, ShopUserIdAwareInterface::class);
/** @var ShopUserNotVerified $constraint */
Assert::isInstanceOf($constraint, ShopUserNotVerified::class);
$shopUser = $this->shopUserRepository->findOneByEmail($value->email);
/** @var ShopUserInterface $shopUser */
$shopUser = $this->shopUserRepository->find($value->getShopUserId());
Assert::notNull($shopUser);
@ -45,6 +47,6 @@ final class ShopUserNotVerifiedValidator extends ConstraintValidator
return;
}
$this->context->addViolation($constraint->message, ['%email%' => $value->email]);
$this->context->addViolation($constraint->message, ['%email%' => $shopUser->getEmail()]);
}
}

View file

@ -16,7 +16,7 @@ namespace spec\Sylius\Bundle\ApiBundle\CommandHandler\Account;
use PhpSpec\ObjectBehavior;
use Sylius\Bundle\ApiBundle\Command\Account\ResendVerificationEmail;
use Sylius\Bundle\ApiBundle\Command\Account\SendAccountVerificationEmail;
use Sylius\Component\Core\Model\ChannelInterface;
use Sylius\Component\Core\Model\CustomerInterface;
use Sylius\Component\Core\Model\ShopUserInterface;
use Sylius\Component\User\Repository\UserRepositoryInterface;
use Sylius\Component\User\Security\Generator\GeneratorInterface;
@ -42,11 +42,12 @@ final class ResendVerificationEmailHandlerSpec extends ObjectBehavior
function it_throws_exception_if_shop_user_does_not_exist(UserRepositoryInterface $userRepository): void
{
$userRepository->findOneByEmail('test@email.com')->willReturn(null);
$userRepository->find(42)->willReturn(null);
$resendVerificationEmail = new ResendVerificationEmail('test@email.com');
$resendVerificationEmail = new ResendVerificationEmail();
$resendVerificationEmail->setChannelCode('WEB');
$resendVerificationEmail->setLocaleCode('en_US');
$resendVerificationEmail->setShopUserId(42);
$this
->shouldThrow(\InvalidArgumentException::class)
@ -59,11 +60,11 @@ final class ResendVerificationEmailHandlerSpec extends ObjectBehavior
ShopUserInterface $shopUser,
GeneratorInterface $generator,
MessageBusInterface $messageBus,
ChannelInterface $channel
CustomerInterface $customer
): void {
$userRepository->findOneByEmail('test@email.com')->willReturn($shopUser);
$channel->isAccountVerificationRequired()->willReturn(true);
$userRepository->find(42)->willReturn($shopUser);
$shopUser->getCustomer()->willReturn($customer);
$customer->getEmail()->willReturn('test@email.com');
$generator->generate()->willReturn('TOKEN');
$shopUser->setEmailVerificationToken('TOKEN')->shouldBeCalled();
@ -75,9 +76,10 @@ final class ResendVerificationEmailHandlerSpec extends ObjectBehavior
[new DispatchAfterCurrentBusStamp()]
)->willReturn(new Envelope($sendAccountVerificationEmail))->shouldBeCalled();
$resendVerificationEmail = new ResendVerificationEmail('test@email.com');
$resendVerificationEmail = new ResendVerificationEmail();
$resendVerificationEmail->setChannelCode('WEB');
$resendVerificationEmail->setLocaleCode('en_US');
$resendVerificationEmail->setShopUserId(42);
$this($resendVerificationEmail);
}

View file

@ -1,90 +0,0 @@
<?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 Sylius\Bundle\ApiBundle\Command\Account\ResendVerificationEmail;
use Sylius\Bundle\ApiBundle\Command\Checkout\CompleteOrder;
use Sylius\Bundle\ApiBundle\Validator\Constraints\ShopUserExists;
use Sylius\Component\Core\Model\ShopUserInterface;
use Sylius\Component\User\Repository\UserRepositoryInterface;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidatorInterface;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
final class ShopUserExistsValidatorSpec extends ObjectBehavior
{
function let(UserRepositoryInterface $userRepository): void
{
$this->beConstructedWith($userRepository);
}
function it_is_a_constraint_validator(): void
{
$this->shouldImplement(ConstraintValidatorInterface::class);
}
function it_throws_an_exception_if_value_is_not_an_instance_of_resend_verification_email_class(): void
{
$this
->shouldThrow(\InvalidArgumentException::class)
->during('validate', [new CompleteOrder(), new class() extends Constraint {
}])
;
}
function it_throws_an_exception_if_constraint_is_not_an_instance_of_shop_user_exists(): void
{
$this
->shouldThrow(\InvalidArgumentException::class)
->during('validate', ['', new class() extends Constraint {
}])
;
}
function it_adds_violation_if_shop_user_does_not_exist(
UserRepositoryInterface $userRepository,
ExecutionContextInterface $executionContext
): void {
$this->initialize($executionContext);
$value = new ResendVerificationEmail('test@sylius.com');
$userRepository->findOneByEmail('test@sylius.com')->willReturn(null);
$executionContext
->addViolation('sylius.account.invalid_email', ['%email%' => 'test@sylius.com'])
->shouldBeCalled();
$this->validate($value, new ShopUserExists());
}
function it_does_not_add_violation_if_shop_user_exists(
UserRepositoryInterface $userRepository,
ExecutionContextInterface $executionContext,
ShopUserInterface $shopUser
): void {
$this->initialize($executionContext);
$value = new ResendVerificationEmail('test@sylius.com');
$userRepository->findOneByEmail('test@sylius.com')->willReturn($shopUser);
$executionContext
->addViolation('sylius.account.invalid_email', ['%email%' => 'test@sylius.com'])
->shouldNotBeCalled();
$this->validate($value, new ShopUserExists());
}
}

View file

@ -16,6 +16,7 @@ namespace spec\Sylius\Bundle\ApiBundle\Validator\Constraints;
use PhpSpec\ObjectBehavior;
use Sylius\Bundle\ApiBundle\Command\Account\ResendVerificationEmail;
use Sylius\Bundle\ApiBundle\Command\Checkout\CompleteOrder;
use Sylius\Bundle\ApiBundle\Command\ShopUserIdAwareInterface;
use Sylius\Bundle\ApiBundle\Validator\Constraints\ShopUserNotVerified;
use Sylius\Component\Core\Model\ShopUserInterface;
use Sylius\Component\User\Repository\UserRepositoryInterface;
@ -55,9 +56,20 @@ final class ShopUserNotVerifiedValidatorSpec extends ObjectBehavior
function it_throws_an_exception_if_shop_user_does_not_exist(UserRepositoryInterface $userRepository): void
{
$value = new ResendVerificationEmail('test@sylius.com');
$value = new class() implements ShopUserIdAwareInterface {
$userRepository->findOneByEmail('test@sylius.com')->willReturn(null);
public function getShopUserId()
{
return 42;
}
public function setShopUserId($shopUserId): void
{
// Intentionally left blank
}
};
$userRepository->find(42)->willReturn(null);
$this
->shouldThrow(\InvalidArgumentException::class)
@ -72,11 +84,23 @@ final class ShopUserNotVerifiedValidatorSpec extends ObjectBehavior
): void {
$this->initialize($executionContext);
$value = new ResendVerificationEmail('test@sylius.com');
$value = new class() implements ShopUserIdAwareInterface {
$userRepository->findOneByEmail('test@sylius.com')->willReturn($shopUser);
public function getShopUserId()
{
return 42;
}
public function setShopUserId($shopUserId): void
{
// Intentionally left blank
}
};
$userRepository->find(42)->willReturn($shopUser);
$shopUser->isVerified()->willReturn(true);
$shopUser->getEmail()->willReturn('test@sylius.com');
$executionContext
->addViolation('sylius.account.is_verified', ['%email%' => 'test@sylius.com'])
@ -92,9 +116,20 @@ final class ShopUserNotVerifiedValidatorSpec extends ObjectBehavior
): void {
$this->initialize($executionContext);
$value = new ResendVerificationEmail('test@sylius.com');
$value = new class() implements ShopUserIdAwareInterface {
$userRepository->findOneByEmail('test@sylius.com')->willReturn($shopUser);
public function getShopUserId()
{
return 42;
}
public function setShopUserId($shopUserId): void
{
// Intentionally left blank
}
};
$userRepository->find(42)->willReturn($shopUser);
$shopUser->isVerified()->willReturn(false);

View file

@ -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.
*/
namespace Sylius\Tests\Api\Shop;
use Sylius\Component\Core\Test\Services\EmailChecker;
use Sylius\Tests\Api\JsonApiTestCase;
use Sylius\Tests\Api\Utils\ShopUserLoginTrait;
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\HttpFoundation\Response;
final class VerifyCustomerAccountsTest extends JsonApiTestCase
{
use ShopUserLoginTrait;
/** @test */
public function it_resends_account_verification_token(): void
{
$container = self::bootKernel()->getContainer();
/** @var Filesystem $filesystem */
$filesystem = $container->get('filesystem');
/** @var EmailChecker $emailChecker */
$emailChecker = $container->get('sylius.behat.email_checker');
$filesystem->remove($emailChecker->getSpoolDirectory());
$loadedData = $this->loadFixturesFromFiles(['cart.yaml', 'authentication/customer.yaml']);
$token = $this->logInShopUser('oliver@doe.com');
$this->client->request(
'POST',
'/api/v2/shop/account-verification-requests',
[],
[],
[
'CONTENT_TYPE' => 'application/ld+json',
'HTTP_ACCEPT' => 'application/ld+json',
'HTTP_Authorization' => sprintf('Bearer %s', $token)
],
'{}'
);
$response = $this->client->getResponse();
$this->assertResponseCode($response, Response::HTTP_ACCEPTED);
self::assertSame(1, $emailChecker->countMessagesTo('oliver@doe.com'));
}
/** @test */
public function it_does_not_allow_to_resend_token_for_not_logged_in_users(): void
{
$container = self::bootKernel()->getContainer();
/** @var Filesystem $filesystem */
$filesystem = $container->get('filesystem');
/** @var EmailChecker $emailChecker */
$emailChecker = $container->get('sylius.behat.email_checker');
$filesystem->remove($emailChecker->getSpoolDirectory());
$this->loadFixturesFromFiles(['cart.yaml', 'authentication/customer.yaml']);
$this->client->request(
'POST',
'/api/v2/shop/account-verification-requests',
[],
[],
[
'CONTENT_TYPE' => 'application/ld+json',
'HTTP_ACCEPT' => 'application/ld+json',
],
'{}'
);
$response = $this->client->getResponse();
$this->assertResponseCode($response, Response::HTTP_UNAUTHORIZED);
self::assertFalse($filesystem->exists($emailChecker->getSpoolDirectory()));
}
}