Skip to content
This repository was archived by the owner on Oct 8, 2025. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 72 additions & 0 deletions src/Authenticator/JWTGuestOrderAuthenticator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<?php

declare(strict_types=1);

namespace Sylius\ShopApiPlugin\Authenticator;

use Lexik\Bundle\JWTAuthenticationBundle\Exception\JWTDecodeFailureException;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Guard\AuthenticatorInterface;
use Symfony\Component\Security\Guard\Token\GuardTokenInterface;
use Symfony\Component\Security\Guard\Token\PostAuthenticationGuardToken;

final class JWTGuestOrderAuthenticator implements AuthenticatorInterface
{
private const TOKEN_HEADER = 'Sylius-Guest-Token';

public function supports(Request $request): bool
{
return $request->headers->has(self::TOKEN_HEADER);
}

public function getCredentials(Request $request): string
{
return $request->headers->get(self::TOKEN_HEADER);
}

public function getUser($credentials, UserProviderInterface $userProvider): UserInterface
{
try {
return $userProvider->loadUserByUsername($credentials);
} catch (JWTDecodeFailureException $decodeFailureException) {
throw new AuthenticationException($decodeFailureException->getMessage());
}
}

public function checkCredentials($credentials, UserInterface $user): bool
{
return true;
}

public function onAuthenticationFailure(Request $request, AuthenticationException $exception): Response
{
return new JsonResponse(['message' => $exception->getMessage()], Response::HTTP_UNAUTHORIZED);
}

public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey): ?Response
{
// The request continues
return null;
}

public function supportsRememberMe(): bool
{
return false;
}

public function start(Request $request, AuthenticationException $authException = null): Response
{
return new JsonResponse(['message' => 'Authentication required'], Response::HTTP_UNAUTHORIZED);
}

public function createAuthenticatedToken(UserInterface $user, $providerKey): GuardTokenInterface
{
return new PostAuthenticationGuardToken($user, $providerKey, []);
}
}
99 changes: 99 additions & 0 deletions src/Controller/Customer/GuestLoginAction.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
<?php

declare(strict_types=1);

namespace Sylius\ShopApiPlugin\Controller\Customer;

use FOS\RestBundle\View\View;
use FOS\RestBundle\View\ViewHandlerInterface;
use Sylius\Component\Core\Model\CustomerInterface;
use Sylius\Component\Core\Model\OrderInterface;
use Sylius\Component\Core\OrderCheckoutStates;
use Sylius\Component\Core\Repository\OrderRepositoryInterface;
use Sylius\ShopApiPlugin\Encoder\GuestOrderJWTEncoderInterface;
use Sylius\ShopApiPlugin\Factory\ValidationErrorViewFactoryInterface;
use Sylius\ShopApiPlugin\Request\Customer\GuestLoginRequest;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Validator\Validator\ValidatorInterface;

final class GuestLoginAction
{
/** @var ViewHandlerInterface */
private $viewHandler;

/** @var ValidatorInterface */
private $validator;

/** @var ValidationErrorViewFactoryInterface */
private $validationErrorViewFactory;

/** @var OrderRepositoryInterface */
private $orderRepository;

/** @var GuestOrderJWTEncoderInterface */
private $guestOrderJWTEncoder;

/**
* GuestLoginAction constructor.
*/
public function __construct(
ViewHandlerInterface $viewHandler,
ValidatorInterface $validator,
ValidationErrorViewFactoryInterface $validationErrorViewFactory,
OrderRepositoryInterface $orderRepository,
GuestOrderJWTEncoderInterface $guestOrderJWTEncoder
) {
$this->viewHandler = $viewHandler;
$this->validator = $validator;
$this->validationErrorViewFactory = $validationErrorViewFactory;
$this->orderRepository = $orderRepository;
$this->guestOrderJWTEncoder = $guestOrderJWTEncoder;
}

public function __invoke(Request $request): Response
{
// This is just to validate that all necessary fields are present.
$loginRequest = new GuestLoginRequest($request);
$validationErrors = $this->validator->validate($loginRequest);
if (0 < count($validationErrors)) {
return $this->viewHandler->handle(
View::create(
$this->validationErrorViewFactory->create($validationErrors),
Response::HTTP_BAD_REQUEST
)
);
}

// Actual login logic
$success = false;

/** @var OrderInterface $order */
$order = $this->orderRepository->findOneByNumber($loginRequest->getOrderNumber());

// The order has to exist and must be placed
if (null !== $order && $order->getCheckoutState() !== OrderCheckoutStates::STATE_CART) {
/** @var CustomerInterface $customer */
$customer = $order->getCustomer();
$paymentMethod = $order->getLastPayment()->getMethod();

// The order must be a guest order. Also the provided email & payment method must match.
if (
null === $customer->getUser() &&
$loginRequest->getEmail() === $customer->getEmail() &&
$paymentMethod->getCode() === $loginRequest->getPaymentMethodCode()
) {
$success = true;
}
}

// Return the jwt on success
if (true === $success) {
$jwt = $this->guestOrderJWTEncoder->encode($order);

return $this->viewHandler->handle(View::create(['jwt' => $jwt], Response::HTTP_OK));
}

return $this->viewHandler->handle(View::create(['message' => 'Bad credentials.'], Response::HTTP_UNAUTHORIZED));
}
}
59 changes: 59 additions & 0 deletions src/Controller/Order/ShowGuestOrderDetailsAction.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<?php

declare(strict_types=1);

namespace Sylius\ShopApiPlugin\Controller\Order;

use FOS\RestBundle\View\View;
use FOS\RestBundle\View\ViewHandlerInterface;
use Sylius\Component\Core\Model\CustomerInterface;
use Sylius\ShopApiPlugin\Traits\CustomerGuestAuthenticationInterface;
use Sylius\ShopApiPlugin\ViewRepository\Order\PlacedOrderViewRepositoryInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Webmozart\Assert\Assert;

final class ShowGuestOrderDetailsAction
{
/** @var ViewHandlerInterface */
private $viewHandler;

/** @var TokenStorageInterface */
private $tokenStorage;

/** @var PlacedOrderViewRepositoryInterface */
private $placedOrderViewRepository;

public function __construct(
ViewHandlerInterface $viewHandler,
TokenStorageInterface $tokenStorage,
PlacedOrderViewRepositoryInterface $placedOrderViewRepository
) {
$this->viewHandler = $viewHandler;
$this->tokenStorage = $tokenStorage;
$this->placedOrderViewRepository = $placedOrderViewRepository;
}

public function __invoke(Request $request): Response
{
try {
$token = $this->tokenStorage->getToken();

Assert::notNull($token);

/** @var CustomerGuestAuthenticationInterface|CustomerInterface $customer */
$customer = $token->getUser();

Assert::isInstanceOf($customer, CustomerInterface::class);
Assert::isInstanceOf($customer, CustomerGuestAuthenticationInterface::class);
Assert::null($customer->getUser());

$order = $this->placedOrderViewRepository->getOneCompletedByCustomerEmailAndToken($customer->getEmail(), $customer->getAuthorizedOrder()->getTokenValue());
} catch (\InvalidArgumentException $exception) {
return $this->viewHandler->handle(View::create(null, Response::HTTP_UNAUTHORIZED));
}

return $this->viewHandler->handle(View::create($order, Response::HTTP_OK));
}
}
41 changes: 41 additions & 0 deletions src/Encoder/GuestOrderJWTEncoder.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php

declare(strict_types=1);

namespace Sylius\ShopApiPlugin\Encoder;

use Lexik\Bundle\JWTAuthenticationBundle\Encoder\JWTEncoderInterface;
use Sylius\Component\Core\Model\OrderInterface;
use Sylius\Component\Core\Repository\OrderRepositoryInterface;

class GuestOrderJWTEncoder implements GuestOrderJWTEncoderInterface
{
/** @var JWTEncoderInterface */
protected $JWTEncoder;

/** @var OrderRepositoryInterface */
protected $orderRepository;

public function __construct(JWTEncoderInterface $JWTEncoder, OrderRepositoryInterface $orderRepository)
{
$this->JWTEncoder = $JWTEncoder;
$this->orderRepository = $orderRepository;
}

public function encode(OrderInterface $order): string
{
$data = ['orderToken' => $order->getTokenValue()];

return $this->JWTEncoder->encode($data);
}

public function decode(string $jwt): OrderInterface
{
$data = $this->JWTEncoder->decode($jwt);

/** @var OrderInterface $order */
$order = $this->orderRepository->findOneByTokenValue($data['orderToken']);

return $order;
}
}
14 changes: 14 additions & 0 deletions src/Encoder/GuestOrderJWTEncoderInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

declare(strict_types=1);

namespace Sylius\ShopApiPlugin\Encoder;

use Sylius\Component\Core\Model\OrderInterface;

interface GuestOrderJWTEncoderInterface
{
public function encode(OrderInterface $order): string;

public function decode(string $jwt): OrderInterface;
}
45 changes: 45 additions & 0 deletions src/Provider/GuestUserProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php

declare(strict_types=1);

namespace Sylius\ShopApiPlugin\Provider;

use ReflectionClass;
use Sylius\ShopApiPlugin\Encoder\GuestOrderJWTEncoderInterface;
use Sylius\ShopApiPlugin\Traits\CustomerGuestAuthenticationInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Webmozart\Assert\Assert;

final class GuestUserProvider implements UserProviderInterface
{
/** @var GuestOrderJWTEncoderInterface */
private $encoder;

public function __construct(GuestOrderJWTEncoderInterface $encoder)
{
$this->encoder = $encoder;
}

public function loadUserByUsername($jwt): UserInterface
{
$order = $this->encoder->decode($jwt);

/** @var CustomerGuestAuthenticationInterface $customer */
$customer = $order->getCustomer();
Assert::implementsInterface($customer, CustomerGuestAuthenticationInterface::class);

$customer->setAuthorizedOrder($order);

return $customer;
}

public function refreshUser(UserInterface $user)
{
}

public function supportsClass($class): bool
{
return (new ReflectionClass($class))->implementsInterface(CustomerGuestAuthenticationInterface::class);
}
}
41 changes: 41 additions & 0 deletions src/Request/Customer/GuestLoginRequest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php

declare(strict_types=1);

namespace Sylius\ShopApiPlugin\Request\Customer;

use Symfony\Component\HttpFoundation\Request;

class GuestLoginRequest
{
/** @var string */
protected $email;

/** @var string */
protected $orderNumber;

/** @var string */
protected $paymentMethodCode;

public function __construct(Request $request)
{
$this->email = $request->request->get('email');
$this->orderNumber = $request->request->get('orderNumber');
$this->paymentMethodCode = $request->request->get('paymentMethod');
}

public function getEmail(): string
{
return $this->email;
}

public function getOrderNumber(): string
{
return $this->orderNumber;
}

public function getPaymentMethodCode(): string
{
return $this->paymentMethodCode;
}
}
6 changes: 6 additions & 0 deletions src/Resources/config/routing/order.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,9 @@ sylius_shop_api_order_details:
methods: [GET]
defaults:
_controller: sylius.shop_api_plugin.controller.order.show_order_details_action

sylius_shop_api_guest_order_details:
path: /guest/order
methods: [GET]
defaults:
_controller: sylius.shop_api_plugin.controller.order.show_guest_order_details_action
2 changes: 2 additions & 0 deletions src/Resources/config/services.xml
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@

<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">
<imports>
<import resource="services/authenticator.xml"/>
<import resource="services/command_providers.xml"/>
<import resource="services/controllers.xml"/>
<import resource="services/encoder.xml"/>
<import resource="services/factories.xml"/>
<import resource="services/handlers.xml"/>
<import resource="services/http.xml"/>
Expand Down
Loading