diff --git a/build.xml b/build.xml index 022d435bd3..fe126c2e05 100644 --- a/build.xml +++ b/build.xml @@ -1313,7 +1313,9 @@ - + + + @@ -1323,7 +1325,7 @@ diff --git a/src/Migrations/Version20200327080840.php b/src/Migrations/Version20200327080840.php new file mode 100644 index 0000000000..6f6661cb9a --- /dev/null +++ b/src/Migrations/Version20200327080840.php @@ -0,0 +1,39 @@ +sql(' + CREATE TABLE customer_user_refresh_token_chain ( + uuid UUID NOT NULL, + customer_user_id INT NOT NULL, + token_chain VARCHAR(255) NOT NULL, + expired_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, + PRIMARY KEY(uuid) + )'); + $this->sql('CREATE INDEX IDX_DA9A5BFDBBB3772B ON customer_user_refresh_token_chain (customer_user_id)'); + $this->sql(' + ALTER TABLE + customer_user_refresh_token_chain + ADD + CONSTRAINT FK_DA9A5BFDBBB3772B FOREIGN KEY (customer_user_id) REFERENCES customer_users (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + } + + /** + * @param \Doctrine\DBAL\Schema\Schema $schema + */ + public function down(Schema $schema): void + { + } +} diff --git a/src/Model/Customer/User/CurrentCustomerUser.php b/src/Model/Customer/User/CurrentCustomerUser.php index 2dd59826f4..a8b21f1cca 100644 --- a/src/Model/Customer/User/CurrentCustomerUser.php +++ b/src/Model/Customer/User/CurrentCustomerUser.php @@ -3,6 +3,7 @@ namespace Shopsys\FrameworkBundle\Model\Customer\User; use Shopsys\FrameworkBundle\Model\Pricing\Group\PricingGroupSettingFacade; +use Shopsys\FrontendApiBundle\Model\User\FrontendApiUser; use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; class CurrentCustomerUser @@ -17,16 +18,24 @@ class CurrentCustomerUser */ protected $pricingGroupSettingFacade; + /** + * @var \Shopsys\FrameworkBundle\Model\Customer\User\CustomerUserFacade + */ + protected $customerUserFacade; + /** * @param \Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface $tokenStorage * @param \Shopsys\FrameworkBundle\Model\Pricing\Group\PricingGroupSettingFacade $pricingGroupSettingFacade + * @param \Shopsys\FrameworkBundle\Model\Customer\User\CustomerUserFacade $customerUserFacade */ public function __construct( TokenStorageInterface $tokenStorage, - PricingGroupSettingFacade $pricingGroupSettingFacade + PricingGroupSettingFacade $pricingGroupSettingFacade, + CustomerUserFacade $customerUserFacade ) { $this->tokenStorage = $tokenStorage; $this->pricingGroupSettingFacade = $pricingGroupSettingFacade; + $this->customerUserFacade = $customerUserFacade; } /** @@ -48,15 +57,21 @@ public function getPricingGroup() public function findCurrentCustomerUser() { $token = $this->tokenStorage->getToken(); + if ($token === null) { return null; } - $customerUser = $token->getUser(); - if (!$customerUser instanceof CustomerUser) { - return null; + $user = $token->getUser(); + + if ($user instanceof FrontendApiUser) { + return $this->customerUserFacade->getByUuid($user->getUuid()); + } + + if ($user instanceof CustomerUser) { + return $user; } - return $customerUser; + return null; } } diff --git a/src/Model/Customer/User/CustomerUser.php b/src/Model/Customer/User/CustomerUser.php index 34ade06623..25fa7dcc51 100644 --- a/src/Model/Customer/User/CustomerUser.php +++ b/src/Model/Customer/User/CustomerUser.php @@ -3,6 +3,7 @@ namespace Shopsys\FrameworkBundle\Model\Customer\User; use DateTime; +use Doctrine\Common\Collections\ArrayCollection; use Doctrine\ORM\Mapping as ORM; use Ramsey\Uuid\Uuid; use Serializable; @@ -124,6 +125,13 @@ class CustomerUser implements UserInterface, TimelimitLoginInterface, Serializab */ protected $uuid; + /** + * @var \Shopsys\FrameworkBundle\Model\Customer\User\CustomerUserRefreshTokenChain[]|\Doctrine\Common\Collections\Collection + * + * @ORM\OneToMany(targetEntity="CustomerUserRefreshTokenChain", mappedBy="customerUser", cascade={"persist"}) + */ + protected $refreshTokenChain; + /** * @param \Shopsys\FrameworkBundle\Model\Customer\User\CustomerUserData $customerUserData */ @@ -143,6 +151,7 @@ public function __construct(CustomerUserData $customerUserData) $this->customer = $customerUserData->customer; $this->defaultDeliveryAddress = $customerUserData->defaultDeliveryAddress; $this->uuid = $customerUserData->uuid ?: Uuid::uuid4()->toString(); + $this->refreshTokenChain = new ArrayCollection(); } /** @@ -412,4 +421,12 @@ public function getUuid(): string { return $this->uuid; } + + /** + * @param \Shopsys\FrameworkBundle\Model\Customer\User\CustomerUserRefreshTokenChain $customerUserRefreshTokenChain + */ + public function addRefreshTokenChain(CustomerUserRefreshTokenChain $customerUserRefreshTokenChain): void + { + $this->refreshTokenChain->add($customerUserRefreshTokenChain); + } } diff --git a/src/Model/Customer/User/CustomerUserFacade.php b/src/Model/Customer/User/CustomerUserFacade.php index 22d518869f..8e2060504a 100644 --- a/src/Model/Customer/User/CustomerUserFacade.php +++ b/src/Model/Customer/User/CustomerUserFacade.php @@ -79,6 +79,11 @@ class CustomerUserFacade */ protected $billingAddressFacade; + /** + * @var \Shopsys\FrameworkBundle\Model\Customer\User\CustomerUserRefreshTokenChainFacade + */ + protected $customerUserRefreshTokenChainFacade; + /** * @param \Doctrine\ORM\EntityManagerInterface $em * @param \Shopsys\FrameworkBundle\Model\Customer\User\CustomerUserRepository $customerUserRepository @@ -92,6 +97,7 @@ class CustomerUserFacade * @param \Shopsys\FrameworkBundle\Model\Customer\DeliveryAddressFacade $deliveryAddressFacade * @param \Shopsys\FrameworkBundle\Model\Customer\CustomerDataFactoryInterface $customerDataFactory * @param \Shopsys\FrameworkBundle\Model\Customer\BillingAddressFacade $billingAddressFacade + * @param \Shopsys\FrameworkBundle\Model\Customer\User\CustomerUserRefreshTokenChainFacade $customerUserRefreshTokenChainFacade */ public function __construct( EntityManagerInterface $em, @@ -105,7 +111,8 @@ public function __construct( CustomerFacade $customerFacade, DeliveryAddressFacade $deliveryAddressFacade, CustomerDataFactoryInterface $customerDataFactory, - BillingAddressFacade $billingAddressFacade + BillingAddressFacade $billingAddressFacade, + CustomerUserRefreshTokenChainFacade $customerUserRefreshTokenChainFacade ) { $this->em = $em; $this->customerUserRepository = $customerUserRepository; @@ -119,6 +126,7 @@ public function __construct( $this->deliveryAddressFacade = $deliveryAddressFacade; $this->customerDataFactory = $customerDataFactory; $this->billingAddressFacade = $billingAddressFacade; + $this->customerUserRefreshTokenChainFacade = $customerUserRefreshTokenChainFacade; } /** @@ -329,4 +337,25 @@ protected function createCustomerWithBillingAddress(int $domainId, BillingAddres return $customer; } + + /** + * @param \Shopsys\FrameworkBundle\Model\Customer\User\CustomerUser $customerUser + * @param string $refreshTokenChain + * @param \DateTime $tokenExpiration + */ + public function addRefreshTokenChain(CustomerUser $customerUser, string $refreshTokenChain, \DateTime $tokenExpiration): void + { + $refreshTokenChain = $this->customerUserRefreshTokenChainFacade->createCustomerUserRefreshTokenChain($customerUser, $refreshTokenChain, $tokenExpiration); + $customerUser->addRefreshTokenChain($refreshTokenChain); + $this->em->flush(); + } + + /** + * @param string $uuid + * @return \Shopsys\FrameworkBundle\Model\Customer\User\CustomerUser + */ + public function getByUuid(string $uuid): CustomerUser + { + return $this->customerUserRepository->getOneByUuid($uuid); + } } diff --git a/src/Model/Customer/User/CustomerUserRefreshTokenChain.php b/src/Model/Customer/User/CustomerUserRefreshTokenChain.php new file mode 100644 index 0000000000..9c6e53020c --- /dev/null +++ b/src/Model/Customer/User/CustomerUserRefreshTokenChain.php @@ -0,0 +1,64 @@ +uuid = $customerUserRefreshTokenChainData->uuid ?: Uuid::uuid4()->toString(); + $this->customerUser = $customerUserRefreshTokenChainData->customerUser; + $this->tokenChain = $customerUserRefreshTokenChainData->tokenChain; + $this->expiredAt = $customerUserRefreshTokenChainData->expiredAt; + } + + /** + * @return string + */ + public function getTokenChain(): string + { + return $this->tokenChain; + } +} diff --git a/src/Model/Customer/User/CustomerUserRefreshTokenChainData.php b/src/Model/Customer/User/CustomerUserRefreshTokenChainData.php new file mode 100644 index 0000000000..9f74636312 --- /dev/null +++ b/src/Model/Customer/User/CustomerUserRefreshTokenChainData.php @@ -0,0 +1,28 @@ +customerUserRefreshTokenChainDataFactory = $customerUserRefreshTokenChainDataFactory; + $this->customerUserRefreshTokenChainFactory = $customerUserRefreshTokenChainFactory; + $this->encoderFactory = $encoderFactory; + $this->customerUserRefreshTokenChainRepository = $customerUserRefreshTokenChainRepository; + } + + /** + * @param \Shopsys\FrameworkBundle\Model\Customer\User\CustomerUser $customerUser + * @param string $tokenChain + * @param \DateTime $tokenExpiration + * @return \Shopsys\FrameworkBundle\Model\Customer\User\CustomerUserRefreshTokenChain + */ + public function createCustomerUserRefreshTokenChain(CustomerUser $customerUser, string $tokenChain, \DateTime $tokenExpiration): CustomerUserRefreshTokenChain + { + $encoder = $this->encoderFactory->getEncoder($customerUser); + + $customerUserRefreshTokenChainData = $this->customerUserRefreshTokenChainDataFactory->create(); + $customerUserRefreshTokenChainData->customerUser = $customerUser; + $customerUserRefreshTokenChainData->tokenChain = $encoder->encodePassword($tokenChain, null); + $customerUserRefreshTokenChainData->expiredAt = $tokenExpiration; + + return $this->customerUserRefreshTokenChainFactory->create($customerUserRefreshTokenChainData); + } + + /** + * @param \Shopsys\FrameworkBundle\Model\Customer\User\CustomerUser $customerUser + * @param string $secretChain + * @return bool + */ + public function isValidSecretChainForUser(CustomerUser $customerUser, string $secretChain): bool + { + $encoder = $this->encoderFactory->getEncoder($customerUser); + $customersTokenChains = $this->customerUserRefreshTokenChainRepository->getCustomersTokenChains($customerUser); + + foreach ($customersTokenChains as $customersTokenChain) { + if ($encoder->isPasswordValid($customersTokenChain->getTokenChain(), $secretChain, null)) { + return true; + } + } + + return false; + } +} diff --git a/src/Model/Customer/User/CustomerUserRefreshTokenChainFactory.php b/src/Model/Customer/User/CustomerUserRefreshTokenChainFactory.php new file mode 100644 index 0000000000..eab52295bc --- /dev/null +++ b/src/Model/Customer/User/CustomerUserRefreshTokenChainFactory.php @@ -0,0 +1,34 @@ +entityNameResolver = $entityNameResolver; + } + + /** + * @param \Shopsys\FrameworkBundle\Model\Customer\User\CustomerUserRefreshTokenChainData $customerUserRefreshTokenChainData + * @return \Shopsys\FrameworkBundle\Model\Customer\User\CustomerUserRefreshTokenChain + */ + public function create(CustomerUserRefreshTokenChainData $customerUserRefreshTokenChainData): CustomerUserRefreshTokenChain + { + $classData = $this->entityNameResolver->resolve(CustomerUserRefreshTokenChain::class); + + return new $classData($customerUserRefreshTokenChainData); + } +} diff --git a/src/Model/Customer/User/CustomerUserRefreshTokenChainFactoryInterface.php b/src/Model/Customer/User/CustomerUserRefreshTokenChainFactoryInterface.php new file mode 100644 index 0000000000..a3d1af42e0 --- /dev/null +++ b/src/Model/Customer/User/CustomerUserRefreshTokenChainFactoryInterface.php @@ -0,0 +1,13 @@ +em = $entityManager; + } + + /** + * @return \Doctrine\ORM\EntityRepository + */ + protected function getCustomerUserRefreshTokenChainRepository(): ObjectRepository + { + return $this->em->getRepository(CustomerUserRefreshTokenChain::class); + } + + /** + * @param \Shopsys\FrameworkBundle\Model\Customer\User\CustomerUser $customerUser + * @return \Shopsys\FrameworkBundle\Model\Customer\User\CustomerUserRefreshTokenChain[] + */ + public function getCustomersTokenChains(CustomerUser $customerUser): array + { + return $this->getCustomerUserRefreshTokenChainRepository()->findBy(['customerUser' => $customerUser]); + } +} diff --git a/src/Model/Customer/User/CustomerUserRepository.php b/src/Model/Customer/User/CustomerUserRepository.php index 392aa9ea35..d43c3b7e16 100644 --- a/src/Model/Customer/User/CustomerUserRepository.php +++ b/src/Model/Customer/User/CustomerUserRepository.php @@ -6,6 +6,7 @@ use Shopsys\FrameworkBundle\Component\String\DatabaseSearching; use Shopsys\FrameworkBundle\Form\Admin\QuickSearch\QuickSearchFormData; use Shopsys\FrameworkBundle\Model\Customer\BillingAddress; +use Shopsys\FrameworkBundle\Model\Customer\Exception\CustomerUserNotFoundException; use Shopsys\FrameworkBundle\Model\Order\Order; use Shopsys\FrameworkBundle\Model\Pricing\Group\PricingGroup; @@ -168,4 +169,20 @@ public function replaceCustomerUsersPricingGroup(PricingGroup $oldPricingGroup, ->where('u.pricingGroup = :oldPricingGroup')->setParameter('oldPricingGroup', $oldPricingGroup) ->getQuery()->execute(); } + + /** + * @param string $uuid + * @return \Shopsys\FrameworkBundle\Model\Customer\User\CustomerUser + */ + public function getOneByUuid(string $uuid): CustomerUser + { + /** @var \Shopsys\FrameworkBundle\Model\Customer\User\CustomerUser|null $customerUser */ + $customerUser = $this->getCustomerUserRepository()->findOneBy(['uuid' => $uuid]); + + if ($customerUser === null) { + throw new CustomerUserNotFoundException('Customer with UUID ' . $uuid . ' does not exist.'); + } + + return $customerUser; + } } diff --git a/src/Resources/config/services.yml b/src/Resources/config/services.yml index d15e73da39..e569941017 100644 --- a/src/Resources/config/services.yml +++ b/src/Resources/config/services.yml @@ -979,3 +979,9 @@ services: Shopsys\FrameworkBundle\Component\ClassExtension\FileContentsReplacer: ~ Shopsys\FrameworkBundle\Model\Pricing\PriceConverter: ~ + + Shopsys\FrameworkBundle\Model\Customer\User\CustomerUserRefreshTokenChainDataFactoryInterface: + alias: Shopsys\FrameworkBundle\Model\Customer\User\CustomerUserRefreshTokenChainDataFactory + + Shopsys\FrameworkBundle\Model\Customer\User\CustomerUserRefreshTokenChainFactoryInterface: + alias: Shopsys\FrameworkBundle\Model\Customer\User\CustomerUserRefreshTokenChainFactory diff --git a/tests/Unit/Model/Customer/CurrentCustomerUserTest.php b/tests/Unit/Model/Customer/CurrentCustomerUserTest.php index 35f7cfcac0..4a3fd97705 100644 --- a/tests/Unit/Model/Customer/CurrentCustomerUserTest.php +++ b/tests/Unit/Model/Customer/CurrentCustomerUserTest.php @@ -7,6 +7,7 @@ use Shopsys\FrameworkBundle\Model\Customer\User\CurrentCustomerUser; use Shopsys\FrameworkBundle\Model\Customer\User\CustomerUser; use Shopsys\FrameworkBundle\Model\Customer\User\CustomerUserData; +use Shopsys\FrameworkBundle\Model\Customer\User\CustomerUserFacade; use Shopsys\FrameworkBundle\Model\Pricing\Group\PricingGroup; use Shopsys\FrameworkBundle\Model\Pricing\Group\PricingGroupData; use Shopsys\FrameworkBundle\Model\Pricing\Group\PricingGroupSettingFacade; @@ -23,8 +24,9 @@ public function testGetPricingGroupForUnregisteredCustomerReturnsDefaultPricingG $tokenStorageMock = $this->createMock(TokenStorage::class); $pricingGroupSettingFacadeMock = $this->getPricingGroupSettingFacadeMockReturningDefaultPricingGroup($expectedPricingGroup); + $customerUserFacadeMock = $this->createMock(CustomerUserFacade::class); - $currentCustomerUser = new CurrentCustomerUser($tokenStorageMock, $pricingGroupSettingFacadeMock); + $currentCustomerUser = new CurrentCustomerUser($tokenStorageMock, $pricingGroupSettingFacadeMock, $customerUserFacadeMock); $pricingGroup = $currentCustomerUser->getPricingGroup(); $this->assertSame($expectedPricingGroup, $pricingGroup); @@ -39,8 +41,9 @@ public function testGetPricingGroupForRegisteredCustomerReturnsHisPricingGroup() $tokenStorageMock = $this->getTokenStorageMockForCustomerUser($customerUser); $pricingGroupFacadeMock = $this->createMock(PricingGroupSettingFacade::class); + $customerUserFacadeMock = $this->createMock(CustomerUserFacade::class); - $currentCustomerUser = new CurrentCustomerUser($tokenStorageMock, $pricingGroupFacadeMock); + $currentCustomerUser = new CurrentCustomerUser($tokenStorageMock, $pricingGroupFacadeMock, $customerUserFacadeMock); $pricingGroup = $currentCustomerUser->getPricingGroup(); $this->assertSame($expectedPricingGroup, $pricingGroup);