From fd75389f93adee6469160cf7ac272196b7d08681 Mon Sep 17 00:00:00 2001 From: Vincent Chalamon Date: Mon, 12 Dec 2022 17:26:54 +0100 Subject: [PATCH] Add API Platform compatibility --- .github/workflows/ci.yml | 4 + DependencyInjection/Configuration.php | 9 ++ .../LexikJWTAuthenticationExtension.php | 13 ++ OpenApi/OpenApiFactory.php | 92 ++++++++++++++ README.md | 2 +- Resources/config/api_platform.xml | 14 ++ Resources/doc/index.rst | 14 ++ .../ApiPlatformOpenApiExportCommandTest.php | 120 ++++++++++++++++++ Tests/Functional/app/AppKernel.php | 36 +++++- .../app/config/routing_api_platform.yml | 6 + 10 files changed, 302 insertions(+), 8 deletions(-) create mode 100644 OpenApi/OpenApiFactory.php create mode 100644 Resources/config/api_platform.xml create mode 100644 Tests/Functional/Command/ApiPlatformOpenApiExportCommandTest.php create mode 100644 Tests/Functional/app/config/routing_api_platform.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index edcb46bf..ddaf0b70 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -70,6 +70,10 @@ jobs: if: "matrix.symfony == '6.0.*' || matrix.symfony == '6.1.*' || matrix.symfony == '6.2.*' || matrix.symfony == '6.3.*@dev'" run: "composer remove --dev --no-update symfony/security-guard" + - name: "Install api-platform/core" + if: "matrix.symfony == '6.1.*' || matrix.symfony == '6.2.*' || matrix.symfony == '6.3.*@dev'" + run: "composer require --dev --no-update api-platform/core:^3.0" + - name: "Install dependencies" run: "composer update ${{ matrix.composer-flags }} --prefer-dist" env: diff --git a/DependencyInjection/Configuration.php b/DependencyInjection/Configuration.php index f635f985..66aa1a71 100644 --- a/DependencyInjection/Configuration.php +++ b/DependencyInjection/Configuration.php @@ -115,6 +115,15 @@ public function getConfigTreeBuilder(): TreeBuilder ->end() ->end() ->end() + ->arrayNode('api_platform') + ->info('API Platform compatibility: add check_path in OpenApi documentation.') + ->children() + ->scalarNode('check_path') + ->defaultNull() + ->info('The login check path to document on OpenApi.') + ->end() + ->end() + ->end() ->end(); return $treeBuilder; diff --git a/DependencyInjection/LexikJWTAuthenticationExtension.php b/DependencyInjection/LexikJWTAuthenticationExtension.php index 2df275b7..0ad7ba87 100644 --- a/DependencyInjection/LexikJWTAuthenticationExtension.php +++ b/DependencyInjection/LexikJWTAuthenticationExtension.php @@ -2,6 +2,7 @@ namespace Lexik\Bundle\JWTAuthenticationBundle\DependencyInjection; +use ApiPlatform\Symfony\Bundle\ApiPlatformBundle; use Lexik\Bundle\JWTAuthenticationBundle\Encoder\JWTEncoderInterface; use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException; use Symfony\Component\Config\FileLocator; @@ -10,6 +11,7 @@ use Symfony\Component\DependencyInjection\Argument\IteratorArgument; use Symfony\Component\DependencyInjection\ChildDefinition; use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Exception\LogicException; use Symfony\Component\DependencyInjection\Loader\XmlFileLoader; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\HttpKernel\DependencyInjection\Extension; @@ -141,6 +143,17 @@ public function load(array $configs, ContainerBuilder $container) ->replaceArgument(3, $config['pass_phrase']) ->replaceArgument(4, $encoderConfig['signature_algorithm']); } + + if (isset($config['api_platform']['check_path'])) { + if (!class_exists(ApiPlatformBundle::class)) { + throw new LogicException('API Platform cannot be detected. Try running "composer require api-platform/core".'); + } + + $loader->load('api_platform.xml'); + $container + ->getDefinition('lexik_jwt_authentication.api_platform.openapi.factory') + ->replaceArgument(1, $config['api_platform']['check_path']); + } } private static function createTokenExtractors(ContainerBuilder $container, array $tokenExtractorsConfig) diff --git a/OpenApi/OpenApiFactory.php b/OpenApi/OpenApiFactory.php new file mode 100644 index 00000000..ed0386fb --- /dev/null +++ b/OpenApi/OpenApiFactory.php @@ -0,0 +1,92 @@ + + * + * @final + */ +class OpenApiFactory implements OpenApiFactoryInterface +{ + /** + * @var OpenApiFactoryInterface + */ + private $decorated; + + private $checkPath; + + public function __construct(OpenApiFactoryInterface $decorated, string $checkPath) + { + $this->decorated = $decorated; + $this->checkPath = $checkPath; + } + + /** + * {@inheritdoc} + */ + public function __invoke(array $context = []): OpenApi + { + $openApi = ($this->decorated)($context); + + $openApi + ->getPaths() + ->addPath($this->checkPath, (new PathItem())->withPost((new Operation()) + ->withOperationId('login_check_post') + ->withTags(['Login Check']) + ->withResponses([ + Response::HTTP_OK => [ + 'description' => 'User token created', + 'content' => [ + 'application/json' => [ + 'schema' => [ + 'type' => 'object', + 'properties' => [ + 'token' => [ + 'readOnly' => true, + 'type' => 'string', + 'nullable' => false, + ], + ], + 'required' => ['token'], + ], + ], + ], + ], + ]) + ->withSummary('Creates a user token.') + ->withRequestBody((new RequestBody()) + ->withDescription('The login data') + ->withContent(new \ArrayObject([ + 'application/json' => new MediaType(new \ArrayObject(new \ArrayObject([ + 'type' => 'object', + 'properties' => [ + '_username' => [ + 'type' => 'string', + 'nullable' => false, + ], + '_password' => [ + 'type' => 'string', + 'nullable' => false, + ], + ], + 'required' => ['_username', '_password'], + ]))), + ])) + ->withRequired(true) + ) + )); + + return $openApi; + } +} diff --git a/README.md b/README.md index b5a2896b..6a6a1ba3 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ Github Issues are dedicated to bug reports and feature requests. Contributing ------------ -See the [CONTRIBUTING](CONTRIBUTING.rst) file. +See the [CONTRIBUTING](CONTRIBUTING.md) file. Sponsoring diff --git a/Resources/config/api_platform.xml b/Resources/config/api_platform.xml new file mode 100644 index 00000000..80119029 --- /dev/null +++ b/Resources/config/api_platform.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + diff --git a/Resources/doc/index.rst b/Resources/doc/index.rst index eaf84df6..8b636593 100644 --- a/Resources/doc/index.rst +++ b/Resources/doc/index.rst @@ -145,6 +145,20 @@ Configure application routing api_login_check: path: /api/login_check +Enable API Platform compatibility +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +To enable the `API Platform `__ compatibility, add the +``lexik_jwt_authentication.api_platform.check_path`` configuration option as following: + +.. code-block:: yaml + + # config/packages/lexik_jwt_authentication.yaml + lexik_jwt_authentication: + # ... + api_platform: + check_path: /api/login_check + Usage ----- diff --git a/Tests/Functional/Command/ApiPlatformOpenApiExportCommandTest.php b/Tests/Functional/Command/ApiPlatformOpenApiExportCommandTest.php new file mode 100644 index 00000000..ea29ece8 --- /dev/null +++ b/Tests/Functional/Command/ApiPlatformOpenApiExportCommandTest.php @@ -0,0 +1,120 @@ + + * + * @requires function ApiPlatformBundle::build + */ +class ApiPlatformOpenApiExportCommandTest extends TestCase +{ + /** + * Test command. + */ + public function testCheckOpenApiExportCommand() + { + $kernel = $this->bootKernel(); + $app = new Application($kernel); + $tester = new CommandTester($app->find('api:openapi:export')); + + $this->assertSame(0, $tester->execute([])); + $this->assertJsonStringEqualsJsonString(<<getDisplay()); + } +} diff --git a/Tests/Functional/app/AppKernel.php b/Tests/Functional/app/AppKernel.php index b68cc384..b2ff4e31 100644 --- a/Tests/Functional/app/AppKernel.php +++ b/Tests/Functional/app/AppKernel.php @@ -2,13 +2,13 @@ namespace Lexik\Bundle\JWTAuthenticationBundle\Tests\Functional; +use ApiPlatform\Symfony\Bundle\ApiPlatformBundle; use Lexik\Bundle\JWTAuthenticationBundle\LexikJWTAuthenticationBundle; use Lexik\Bundle\JWTAuthenticationBundle\Tests\Functional\Bundle\Bundle; use Psr\Log\NullLogger; use Symfony\Bundle\FrameworkBundle\FrameworkBundle; use Symfony\Bundle\SecurityBundle\SecurityBundle; use Symfony\Bundle\SecurityBundle\SecurityBundle\Security; -use Symfony\Component\Config\Definition\Builder\TreeBuilder; use Symfony\Component\Config\Loader\LoaderInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\HttpKernel\Kernel; @@ -42,12 +42,17 @@ public function __construct($environment, $debug, $testCase = null) */ public function registerBundles(): array { - return [ + $bundles = [ new FrameworkBundle(), new SecurityBundle(), new LexikJWTAuthenticationBundle(), new Bundle(), ]; + if (class_exists(ApiPlatformBundle::class)) { + $bundles[] = new ApiPlatformBundle(); + } + + return $bundles; } public function getRootDir() @@ -100,12 +105,29 @@ public function registerContainerConfiguration(LoaderInterface $loader) }); } - $loader->load(function (ContainerBuilder $container) use ($sessionConfig) { + $router = [ + 'resource' => '%kernel.root_dir%/config/routing.yml', + 'utf8' => true, + ]; + if (class_exists(ApiPlatformBundle::class)) { + $loader->load(function (ContainerBuilder $container) use (&$router) { + $container->prependExtensionConfig('api_platform', [ + 'title' => 'LexikJWTAuthenticationBundle', + 'description' => 'API Platform integration in LexikJWTAuthenticationBundle', + 'version' => '1.0.0', + ]); + $container->prependExtensionConfig('lexik_jwt_authentication', [ + 'api_platform' => [ + 'check_path' => '/login_check', + ], + ]); + $router['resource'] = '%kernel.root_dir%/config/routing_api_platform.yml'; + }); + } + + $loader->load(function (ContainerBuilder $container) use ($router, $sessionConfig) { $container->prependExtensionConfig('framework', [ - 'router' => [ - 'resource' => '%kernel.root_dir%/config/routing.yml', - 'utf8' => true, - ], + 'router' => $router, 'session' => $sessionConfig ]); }); diff --git a/Tests/Functional/app/config/routing_api_platform.yml b/Tests/Functional/app/config/routing_api_platform.yml new file mode 100644 index 00000000..f254b79a --- /dev/null +++ b/Tests/Functional/app/config/routing_api_platform.yml @@ -0,0 +1,6 @@ +default: + resource: routing.yml + +api_platform: + resource: . + type: api_platform