diff --git a/README.md b/README.md
index f03fac776..0f07766b7 100644
--- a/README.md
+++ b/README.md
@@ -79,18 +79,39 @@ The latest documentation is available [here](https://app.swaggerhub.com/apis/Syl
# ...
sylius.security.shop_regex: "^/(?!admin|api/.*|api$|shop-api|media/.*)[^/]++" # shop-api has been added inside the brackets
- shop_api.security.regex: "^/shop-api"
+ sylius_shop_api.security.regex: "^/shop-api"
# ...
security:
firewalls:
// ...
+
+ sylius_shop_api_login:
+ pattern: "%sylius_shop_api.security.regex%/login"
+ stateless: true
+ anonymous: true
+ form_login:
+ provider: sylius_shop_user_provider
+ login_path: /shop-api/login_check
+ check_path: /shop-api/login_check
+ success_handler: lexik_jwt_authentication.handler.authentication_success
+ failure_handler: lexik_jwt_authentication.handler.authentication_failure
+ require_previous_session: false
- shop_api:
- pattern: "%shop_api.security.regex%"
- stateless: true
- anonymous: true
+ sylius_shop_api:
+ pattern: "%sylius_shop_api.security.regex%"
+ stateless: true
+ anonymous: true
+ guard:
+ provider: sylius_shop_user_provider
+ authenticators:
+ - lexik_jwt_authentication.jwt_token_authenticator
+
+ access_control:
+ - { path: "%sylius_shop_api.security.regex%/login", role: IS_AUTHENTICATED_ANONYMOUSLY }
+ - { path: "%sylius_shop_api.security.regex%/register", role: IS_AUTHENTICATED_ANONYMOUSLY }
+
```
6. (optional) if you have installed `nelmio/NelmioCorsBundle` for Support of Cross-Origin Ajax Request,
@@ -142,16 +163,8 @@ sylius_shop_api:
- "MUG_MATERIAL_CODE"
```
-### Authorization
-
-By default no authorization is provided together with this bundle. But it is tested to work along with [LexikJWTAuthenticationBundle](https://github.com/lexik/LexikJWTAuthenticationBundle)
-In order to check example configuration check
- - [security.yml](https://github.com/Sylius/SyliusShopApiPlugin/blob/master/tests/Application/app/config/security.yml)
- - [jwt parameters](https://github.com/Sylius/SyliusShopApiPlugin/blob/master/tests/Application/app/config/config.yml#L4-L7) and [jwt config](https://github.com/Sylius/SyliusShopApiPlugin/blob/master/tests/Application/app/config/config.yml#L55-L59) in config.yml
- - [example rsa keys](https://github.com/Sylius/SyliusShopApiPlugin/tree/master/tests/Application/app/config/jwt)
- - [login request](https://github.com/Sylius/SyliusShopApiPlugin/blob/master/tests/Controller/CustomerShopApiTest.php#L52-L68)
-
-From the test app.
+This plugin comes with an integration with [LexikJWTAuthenticationBundle](https://github.com/lexik/LexikJWTAuthenticationBundle/).
+More information about security customizations may be found there.
## Testing
diff --git a/composer.json b/composer.json
index b608b2cdd..7b3315e5f 100644
--- a/composer.json
+++ b/composer.json
@@ -7,11 +7,11 @@
"php": "^7.2",
"sylius/sylius": "^1.4",
+ "lexik/jwt-authentication-bundle": "^2.5",
"symfony/messenger": "~4.3.0"
},
"require-dev": {
"lchrusciel/api-test-case": "^4.0",
- "lexik/jwt-authentication-bundle": "^2.5",
"matthiasnoback/symfony-config-test": "^4.0",
"matthiasnoback/symfony-dependency-injection-test": "^4.0",
"phpspec/phpspec": "^5.0",
diff --git a/doc/swagger.yml b/doc/swagger.yml
index acdd0203f..72cee09c3 100644
--- a/doc/swagger.yml
+++ b/doc/swagger.yml
@@ -764,6 +764,25 @@ paths:
description: "There were validation errors"
500:
description: "Channel not found"
+
+ /login_check:
+ post:
+ tags:
+ - "users"
+ summary: "Log user in"
+ description: "This action allows to log existing user in"
+ operationId: "loginUser"
+ parameters:
+ - name: "content"
+ in: "body"
+ required: true
+ schema:
+ $ref: "#/definitions/LoginRequest"
+ responses:
+ 204:
+ description: "The user was successfully created"
+ 400:
+ description: "There were validation errors"
/orders:
parameters:
- $ref: "#/parameters/ChannelCode"
@@ -1569,6 +1588,15 @@ definitions:
channel:
type: "string"
example: "WEB_GB"
+ LoginRequest:
+ type: "object"
+ properties:
+ _username:
+ type: "string"
+ example: "test@example.com"
+ _password:
+ type: "string"
+ example: "test12334verysecure"
UpdateUserRequest:
type: "object"
properties:
diff --git a/src/EventListener/CartBlamerListener.php b/src/EventListener/CartBlamerListener.php
new file mode 100644
index 000000000..59489831e
--- /dev/null
+++ b/src/EventListener/CartBlamerListener.php
@@ -0,0 +1,91 @@
+cartManager = $cartManager;
+ $this->cartContext = $cartContext;
+ $this->cartRepository = $cartRepository;
+ $this->requestStack = $requestStack;
+ }
+
+ public function onJwtLogin(JWTCreatedEvent $interactiveLoginEvent): void
+ {
+ $user = $interactiveLoginEvent->getUser();
+ $request = $this->requestStack->getCurrentRequest();
+
+ Assert::notNull($request);
+
+ if (!$user instanceof ShopUserInterface) {
+ return;
+ }
+
+ $cart = $this->getCart($request->request->get('token'));
+
+ if (null === $cart) {
+ return;
+ }
+
+ $cart->setCustomer($user->getCustomer());
+ $this->cartManager->persist($cart);
+ $this->cartManager->flush();
+ }
+
+ private function getCart(?string $token): ?OrderInterface
+ {
+ if (null !== $token) {
+ /** @var OrderInterface $cart */
+ $cart = $this->cartRepository->findOneBy(['tokenValue' => $token]);
+
+ return $cart;
+ }
+
+ try {
+ /** @var OrderInterface $cart */
+ $cart = $this->cartContext->getCart();
+
+ return $cart;
+ } catch (CartNotFoundException $exception) {
+ return null;
+ }
+ }
+}
diff --git a/src/EventListener/UserCartRecalculationListener.php b/src/EventListener/UserCartRecalculationListener.php
new file mode 100644
index 000000000..acf8dead9
--- /dev/null
+++ b/src/EventListener/UserCartRecalculationListener.php
@@ -0,0 +1,49 @@
+cartContext = $cartContext;
+ $this->orderProcessor = $orderProcessor;
+ $this->cartManager = $cartManager;
+ }
+
+ public function recalculateCartWhileLogin(): void
+ {
+ try {
+ $cart = $this->cartContext->getCart();
+ } catch (CartNotFoundException $exception) {
+ return;
+ }
+
+ Assert::isInstanceOf($cart, OrderInterface::class);
+
+ $this->orderProcessor->process($cart);
+
+ $this->cartManager->flush();
+ }
+}
diff --git a/src/Resources/config/routing.yml b/src/Resources/config/routing.yml
index 64c97eee0..5b134d5c7 100644
--- a/src/Resources/config/routing.yml
+++ b/src/Resources/config/routing.yml
@@ -37,3 +37,7 @@ sylius_shop_api_address_book:
sylius_shop_api_order:
resource: "@ShopApiPlugin/Resources/config/routing/order.yml"
prefix: /shop-api
+
+sylius_shop_api_login_check:
+ methods: [POST]
+ path: /shop-api/login_check
diff --git a/src/Resources/config/services.xml b/src/Resources/config/services.xml
index 43953a561..828794a13 100644
--- a/src/Resources/config/services.xml
+++ b/src/Resources/config/services.xml
@@ -72,6 +72,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/Application/config/packages/security.yaml b/tests/Application/config/packages/security.yaml
index ac16053ca..93739f1f8 100644
--- a/tests/Application/config/packages/security.yaml
+++ b/tests/Application/config/packages/security.yaml
@@ -2,7 +2,7 @@ parameters:
sylius.security.admin_regex: "^/admin"
sylius.security.api_regex: "^/api"
sylius.security.shop_regex: "^/(?!admin|api/.*|api$|media/.*)[^/]++"
- shop_api.security.regex: "^/shop-api"
+ sylius_shop_api.security.regex: "^/shop-api"
security:
providers:
@@ -52,7 +52,7 @@ security:
anonymous: true
sylius_shop_api_login:
- pattern: "%shop_api.security.regex%/login"
+ pattern: "%sylius_shop_api.security.regex%/login"
stateless: true
anonymous: true
form_login:
@@ -63,8 +63,8 @@ security:
failure_handler: lexik_jwt_authentication.handler.authentication_failure
require_previous_session: false
- shop_api:
- pattern: "%shop_api.security.regex%"
+ sylius_shop_api:
+ pattern: "%sylius_shop_api.security.regex%"
stateless: true
anonymous: true
guard:
@@ -115,11 +115,11 @@ security:
- { path: "%sylius.security.admin_regex%/login", role: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: "%sylius.security.api_regex%/login", role: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: "%sylius.security.shop_regex%/login", role: IS_AUTHENTICATED_ANONYMOUSLY }
- - { path: "%shop_api.security.regex%/login", role: IS_AUTHENTICATED_ANONYMOUSLY }
+ - { path: "%sylius_shop_api.security.regex%/login", role: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: "%sylius.security.shop_regex%/register", role: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: "%sylius.security.shop_regex%/verify", role: IS_AUTHENTICATED_ANONYMOUSLY }
- - { path: "%shop_api.security.regex%/register", role: IS_AUTHENTICATED_ANONYMOUSLY }
+ - { path: "%sylius_shop_api.security.regex%/register", role: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: "%sylius.security.admin_regex%", role: ROLE_ADMINISTRATION_ACCESS }
- { path: "%sylius.security.api_regex%/.*", role: ROLE_API_ACCESS }
diff --git a/tests/Application/config/routes.yaml b/tests/Application/config/routes.yaml
index 9645b82d5..05107a37b 100644
--- a/tests/Application/config/routes.yaml
+++ b/tests/Application/config/routes.yaml
@@ -5,7 +5,3 @@ sylius:
sylius_shop_api:
resource: "@ShopApiPlugin/Resources/config/routing.yml"
-
-sylius_shop_api_login_check:
- methods: [POST]
- path: /shop-api/login_check
diff --git a/tests/Controller/Cart/CartPickupApiTest.php b/tests/Controller/Cart/CartPickupApiTest.php
index ec9730e0a..c6fe27580 100644
--- a/tests/Controller/Cart/CartPickupApiTest.php
+++ b/tests/Controller/Cart/CartPickupApiTest.php
@@ -4,8 +4,11 @@
namespace Tests\Sylius\ShopApiPlugin\Controller\Cart;
+use Sylius\Component\Core\Model\OrderInterface;
use Sylius\Component\Core\Repository\OrderRepositoryInterface;
+use Sylius\ShopApiPlugin\Command\Cart\PickupCart;
use Symfony\Component\HttpFoundation\Response;
+use Symfony\Component\Messenger\MessageBusInterface;
use Tests\Sylius\ShopApiPlugin\Controller\JsonApiTestCase;
use Tests\Sylius\ShopApiPlugin\Controller\Utils\ShopUserLoginTrait;
@@ -51,4 +54,32 @@ public function it_only_creates_one_cart_if_user_is_logged_in(): void
$this->assertCount(1, $orders, 'Only one cart should be created');
}
+
+ /**
+ * @test
+ */
+ public function it_does_not_create_a_new_cart_if_cart_was_picked_up_before_logging_in(): void
+ {
+ $this->loadFixturesFromFiles(['shop.yml', 'customer.yml']);
+
+ $token = 'SDAOSLEFNWU35H3QLI5325';
+
+ /** @var MessageBusInterface $bus */
+ $bus = $this->get('sylius_shop_api_plugin.command_bus');
+ $bus->dispatch(new PickupCart($token, 'WEB_GB'));
+
+ $this->logInUserWithCart('oliver@queen.com', '123password', $token);
+
+ /** @var OrderRepositoryInterface $orderRepository */
+ $orderRepository = $this->get('sylius.repository.order');
+ $orders = $orderRepository->findAll();
+
+ $this->assertCount(1, $orders, 'Only one cart should be created');
+
+ /** @var OrderInterface $order */
+ $order = $orders[0];
+ $customer = $order->getCustomer();
+ $this->assertNotNull($customer, 'Cart should have customer assigned, but it has not.');
+ $this->assertSame('oliver@queen.com', $customer->getEmail());
+ }
}
diff --git a/tests/Controller/Cart/CartPutItemToCartApiTest.php b/tests/Controller/Cart/CartPutItemToCartApiTest.php
index a3c6ce4df..735310e72 100644
--- a/tests/Controller/Cart/CartPutItemToCartApiTest.php
+++ b/tests/Controller/Cart/CartPutItemToCartApiTest.php
@@ -15,9 +15,12 @@
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Messenger\MessageBusInterface;
use Tests\Sylius\ShopApiPlugin\Controller\JsonApiTestCase;
+use Tests\Sylius\ShopApiPlugin\Controller\Utils\ShopUserLoginTrait;
final class CartPutItemToCartApiTest extends JsonApiTestCase
{
+ use ShopUserLoginTrait;
+
/**
* @test
*/
@@ -44,6 +47,28 @@ public function it_adds_a_product_to_the_cart(): void
$this->assertResponse($response, 'cart/add_simple_product_to_cart_response', Response::HTTP_CREATED);
}
+ /**
+ * @test
+ */
+ public function it_recalculates_cart_when_customer_log_in(): void
+ {
+ $this->loadFixturesFromFiles(['shop.yml', 'customer.yml', 'promotion.yml']);
+
+ $token = 'SDAOSLEFNWU35H3QLI5325';
+
+ /** @var MessageBusInterface $bus */
+ $bus = $this->get('sylius_shop_api_plugin.command_bus');
+ $bus->dispatch(new PickupCart($token, 'WEB_GB'));
+ $bus->dispatch(new PutSimpleItemToCart($token, 'LOGAN_MUG_CODE', 1));
+
+ $this->logInUserWithCart('oliver@queen.com', '123password', $token);
+
+ $this->client->request('GET', '/shop-api/carts/' . $token, [], [], self::CONTENT_TYPE_HEADER);
+ $response = $this->client->getResponse();
+
+ $this->assertResponse($response, 'cart/recalculated_cart_after_log_in', Response::HTTP_OK);
+ }
+
/**
* @test
*/
diff --git a/tests/Controller/Utils/ShopUserLoginTrait.php b/tests/Controller/Utils/ShopUserLoginTrait.php
index 5a846a513..81817cafc 100644
--- a/tests/Controller/Utils/ShopUserLoginTrait.php
+++ b/tests/Controller/Utils/ShopUserLoginTrait.php
@@ -23,8 +23,29 @@ protected function logInUser(string $username, string $password): void
}
EOT;
+ $this->sendLogInRequest($data);
+ }
+
+ protected function logInUserWithCart(string $username, string $password, string $token): void
+ {
+ $data =
+<<sendLogInRequest($data);
+ }
+
+ private function sendLogInRequest(string $data): void
+ {
$this->client->request('POST', '/shop-api/login_check', [], [], ['CONTENT_TYPE' => 'application/json', 'ACCEPT' => 'application/json'], $data);
+
Assert::assertSame($this->client->getResponse()->getStatusCode(), Response::HTTP_OK);
+
$response = json_decode($this->client->getResponse()->getContent(), true);
$this->client->setServerParameter('HTTP_Authorization', sprintf('Bearer %s', $response['token']));
}
diff --git a/tests/DataFixtures/ORM/customer.yml b/tests/DataFixtures/ORM/customer.yml
index ffce3fa78..f6aca3428 100644
--- a/tests/DataFixtures/ORM/customer.yml
+++ b/tests/DataFixtures/ORM/customer.yml
@@ -22,6 +22,7 @@ Sylius\Component\Core\Model\Customer:
email: "oliver@queen.com"
emailCanonical: "oliver@queen.com"
gender: "m"
+ group: "@retail"
phoneNumber: "0212115512"
hater:
@@ -29,3 +30,8 @@ Sylius\Component\Core\Model\Customer:
lastName: "Wilson"
email: "hater@queen.com"
emailCanonical: "slade@queen.com"
+
+Sylius\Component\Customer\Model\CustomerGroup:
+ retail:
+ name: "Retail"
+ code: "retail"
diff --git a/tests/DataFixtures/ORM/promotion.yml b/tests/DataFixtures/ORM/promotion.yml
index 94d1846ec..426f6a90b 100644
--- a/tests/DataFixtures/ORM/promotion.yml
+++ b/tests/DataFixtures/ORM/promotion.yml
@@ -6,12 +6,24 @@ Sylius\Component\Core\Model\Promotion:
actions: ["@10_percent_order_discount"]
rules: ["@over_50_amount_rule"]
+ customer_promotion:
+ code: "CUSTOMER_PROMOTION"
+ name: "Holiday promotion"
+ channels: ["@gb_web_channel", "@de_web_channel"]
+ actions: ["@20_percent_order_discount"]
+ rules: ["@customer_group_rule"]
+
Sylius\Component\Promotion\Model\PromotionAction:
10_percent_order_discount:
type: "order_percentage_discount"
promotion: "@holiday_promotion"
configuration:
percentage: 0.1
+ 20_percent_order_discount:
+ type: "order_percentage_discount"
+ promotion: "@customer_promotion"
+ configuration:
+ percentage: 0.2
Sylius\Component\Promotion\Model\PromotionRule:
over_50_amount_rule:
@@ -22,3 +34,8 @@ Sylius\Component\Promotion\Model\PromotionRule:
amount: 5000
WEB_DE:
amount: 5000
+ customer_group_rule:
+ type: "customer_group"
+ promotion: "@customer_promotion"
+ configuration:
+ group_code: 'retail'
diff --git a/tests/Responses/Expected/cart/recalculated_cart_after_log_in.json b/tests/Responses/Expected/cart/recalculated_cart_after_log_in.json
new file mode 100644
index 000000000..2df518c57
--- /dev/null
+++ b/tests/Responses/Expected/cart/recalculated_cart_after_log_in.json
@@ -0,0 +1,79 @@
+{
+ "tokenValue": "SDAOSLEFNWU35H3QLI5325",
+ "channel": "WEB_GB",
+ "currency": "GBP",
+ "locale": "en_GB",
+ "checkoutState": "cart",
+ "items": [
+ {
+ "id": @integer@,
+ "quantity": 1,
+ "total": 1599,
+ "product": {
+ "code": "LOGAN_MUG_CODE",
+ "name": "Logan Mug",
+ "slug": "logan-mug",
+ "channelCode": "WEB_GB",
+ "description": "Some description Lorem ipsum dolor sit amet.",
+ "averageRating": 0,
+ "taxons": {
+ "main": "MUG",
+ "others": [
+ "MUG",
+ "BRAND"
+ ]
+ },
+ "variants": [
+ {
+ "code": "LOGAN_MUG_CODE",
+ "name": "Logan Mug",
+ "axis": [],
+ "nameAxis": {},
+ "price": {
+ "current": 1999,
+ "currency": "GBP"
+ },
+ "images": []
+ }
+ ],
+ "attributes": [
+ {
+ "code": "MUG_MATERIAL_CODE",
+ "name": "Mug material",
+ "value": "Wood"
+ }
+ ],
+ "associations": [],
+ "images": [
+ {
+ "code": "thumbnail",
+ "path": "\/uo\/mug.jpg"
+ }
+ ],
+ "_links": {
+ "self": {
+ "href": "\/shop-api\/products\/by-slug\/logan-mug"
+ }
+ }
+ }
+ }
+ ],
+ "totals": {
+ "total": 1599,
+ "items": 1599,
+ "taxes": 0,
+ "shipping": 0,
+ "promotion": -400
+ },
+ "payments": [],
+ "shipments": [],
+ "cartDiscounts": {
+ "CUSTOMER_PROMOTION": {
+ "name": "Holiday promotion",
+ "amount": {
+ "current": -400,
+ "currency": "GBP"
+ }
+ }
+ }
+}