diff --git a/Api/ConfigInterface.php b/Api/ConfigInterface.php index c70d0fd..e6cde16 100644 --- a/Api/ConfigInterface.php +++ b/Api/ConfigInterface.php @@ -22,6 +22,7 @@ interface ConfigInterface public const CONFIG_WEBHOOK_URL = 'agentic_commerce/agentic_checkout/webhook_url'; public const CONFIG_WEBHOOK_SECRET = 'agentic_commerce/agentic_checkout/webhook_secret'; public const CONFIG_WEBHOOKS_ENABLED = 'agentic_commerce/agentic_checkout/enable_webhooks'; + public const CONFIG_API_TOKEN = 'agentic_commerce/agentic_checkout/api_token'; public const CONFIG_ORDER_STATUS_MAP = 'agentic_commerce/agentic_checkout/order_status_map'; public const CONFIG_GTIN_SOURCE = 'agentic_commerce/product_feed/gtin_source'; @@ -124,6 +125,12 @@ public function getIdempotencyTtl(?int $storeId = null): int; */ public function getIsWebhooksEnabled(?int $storeId = null): bool; + /** + * @param int|null $storeId + * @return string|null + */ + public function getApiToken(?int $storeId = null): ?string; + /** * Get order status mapping configuration * diff --git a/Controller/ApiController.php b/Controller/ApiController.php index e338aaf..0d95bb5 100644 --- a/Controller/ApiController.php +++ b/Controller/ApiController.php @@ -83,8 +83,14 @@ protected function createRequestObjectAndValidate(callable $factory): mixed public function makeErrorResponse(ErrorResponseInterface $errorResponse, int $statusCode = 400): ResultJson { /** @var ErrorResponse $errorResponse */ + if ($errorResponse->getData('_statusCode')) { + // @phpstan-ignore cast.int + $statusCode = (int) $errorResponse->getData('_statusCode'); + $errorResponse->unsetData('_statusCode'); + } + /** @var array $data */ - $data = $errorResponse->getData(); + $data = $errorResponse->toArray(); return $this->makeJsonResponse($data, $statusCode); } diff --git a/Model/Config.php b/Model/Config.php index b7eaa97..e5946cc 100644 --- a/Model/Config.php +++ b/Model/Config.php @@ -269,6 +269,22 @@ public function getIsWebhooksEnabled(?int $storeId = null): bool return $this->scopeConfig->isSetFlag(ConfigInterface::CONFIG_WEBHOOKS_ENABLED, ScopeInterface::SCOPE_STORE, $storeId); } + /** + * @param int|null $storeId + * @return string|null + */ + public function getApiToken(?int $storeId = null): ?string + { + /** @var string|null $token */ + $token = $this->scopeConfig->getValue( + ConfigInterface::CONFIG_API_TOKEN, + ScopeInterface::SCOPE_STORE, + $storeId + ); + + return $token ?: null; + } + /** * Get order status mapping configuration * diff --git a/Service/ComplianceService.php b/Service/ComplianceService.php index aada4ef..66eeba2 100644 --- a/Service/ComplianceService.php +++ b/Service/ComplianceService.php @@ -19,6 +19,7 @@ use Magebit\AgenticCommerce\Api\Data\Response\ErrorResponseInterfaceFactory; use Magebit\AgenticCommerce\Model\Idempotency\Management as IdempotencyManagement; use Magento\Framework\Encryption\EncryptorInterface; +use Magebit\AgenticCommerce\Api\ConfigInterface; class ComplianceService { @@ -29,12 +30,14 @@ class ComplianceService * @param IdempotencyManagement $idempotencyManagement * @param JsonFactory $resultJsonFactory * @param EncryptorInterface $encryptor + * @param ConfigInterface $config */ public function __construct( protected readonly ErrorResponseInterfaceFactory $errorResponseFactory, protected readonly IdempotencyManagement $idempotencyManagement, protected readonly JsonFactory $resultJsonFactory, - protected readonly EncryptorInterface $encryptor + protected readonly EncryptorInterface $encryptor, + protected readonly ConfigInterface $config ) { } @@ -47,6 +50,21 @@ public function validateApiVersion(Http $request): bool return $request->getHeader('API-Version') === self::API_VERSION; } + /** + * @param Http $request + * @return bool + */ + public function validateApiToken(Http $request): bool + { + $apiToken = $this->config->getApiToken(); + + if (!$apiToken) { + return true; + } + + return $request->getHeader('Authorization') === 'Bearer ' . $apiToken; + } + /** * @param Http $request * @return null|ErrorResponseInterface @@ -61,6 +79,15 @@ public function validateRequest(Http $request): ?ErrorResponseInterface ]]); } + if (!$this->validateApiToken($request)) { + return $this->errorResponseFactory->create(['data' => [ + 'type' => ErrorResponseInterface::TYPE_INVALID_REQUEST, + 'code' => 'invalid_api_token', + 'message' => 'Invalid API token', + '_statusCode' => 401, + ]]); + } + if ($idempotencyError = $this->validateIdempotency($request)) { return $idempotencyError; } diff --git a/etc/adminhtml/system.xml b/etc/adminhtml/system.xml index 566cf26..74dd46e 100644 --- a/etc/adminhtml/system.xml +++ b/etc/adminhtml/system.xml @@ -37,21 +37,26 @@ Base path to use for the router. Default is `checkout_sessions` required-entry - + + + Leave empty to skip Bearer token validation + Magento\Config\Model\Config\Backend\Encrypted + + required-entry - + List of links (e.g. ToS/privacy policy/etc.) to be displayed to the customer. List of links (e.g. ToS/privacy policy/etc.) to be displayed to the customer Magebit\AgenticCommerce\Block\Adminhtml\Form\Field\SessionLinks Magento\Config\Model\Config\Backend\Serialized\ArraySerialized - + Magento\Config\Model\Config\Source\Yesno - + URL to send webhooks to required-entry @@ -59,14 +64,15 @@ 1 - + Secret to use for the webhook + Magento\Config\Model\Config\Backend\Encrypted 1 - + Map Magento order statuses to Agentic Commerce order statuses for webhooks Magebit\AgenticCommerce\Block\Adminhtml\Form\Field\OrderStatusMap diff --git a/etc/config.xml b/etc/config.xml index d2abf7d..6474a13 100644 --- a/etc/config.xml +++ b/etc/config.xml @@ -19,6 +19,8 @@ checkout_sessions checkout/onepage/success 0 + + pending