From 2ffbf03cf81fbd891e458f49a605ea81d4e29940 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya <alejandro@alejandrocelaya.com> Date: Sun, 12 Apr 2020 13:59:10 +0200 Subject: [PATCH] Created action to get mercure integration info --- config/autoload/mercure.global.php | 16 +++++ docs/swagger/definitions/MercureInfo.json | 18 +++++ docs/swagger/paths/v2_mercure-info.json | 67 +++++++++++++++++++ docs/swagger/swagger.json | 4 ++ module/Rest/config/dependencies.config.php | 3 + module/Rest/config/routes.config.php | 2 + module/Rest/src/Action/MercureAction.php | 56 ++++++++++++++++ .../Rest/src/Exception/MercureException.php | 30 +++++++++ 8 files changed, 196 insertions(+) create mode 100644 docs/swagger/definitions/MercureInfo.json create mode 100644 docs/swagger/paths/v2_mercure-info.json create mode 100644 module/Rest/src/Action/MercureAction.php create mode 100644 module/Rest/src/Exception/MercureException.php diff --git a/config/autoload/mercure.global.php b/config/autoload/mercure.global.php index e04336f3..3466ce64 100644 --- a/config/autoload/mercure.global.php +++ b/config/autoload/mercure.global.php @@ -2,6 +2,9 @@ declare(strict_types=1); +use Laminas\ServiceManager\Proxy\LazyServiceFactory; +use Shlinkio\Shlink\Common\Mercure\LcobucciJwtProvider; + return [ 'mercure' => [ @@ -12,4 +15,17 @@ return [ 'jwt_issuer' => 'Shlink', ], + 'dependencies' => [ + 'delegators' => [ + LcobucciJwtProvider::class => [ + LazyServiceFactory::class, + ], + ], + 'lazy_services' => [ + 'class_map' => [ + LcobucciJwtProvider::class => LcobucciJwtProvider::class, + ], + ], + ], + ]; diff --git a/docs/swagger/definitions/MercureInfo.json b/docs/swagger/definitions/MercureInfo.json new file mode 100644 index 00000000..ac1f273a --- /dev/null +++ b/docs/swagger/definitions/MercureInfo.json @@ -0,0 +1,18 @@ +{ + "type": "object", + "required": ["mercureHubUrl", "jwt", "jwtExpiration"], + "properties": { + "mercureHubUrl": { + "type": "string", + "description": "The public URL of the mercure hub that can be used to get real-time updates published by Shlink" + }, + "jwt": { + "type": "string", + "description": "A JWT with subscribe permissions which is valid with the mercure hub" + }, + "jwtExpiration": { + "type": "string", + "description": "The date (in ISO-8601 format) in which the JWT will expire" + } + } +} diff --git a/docs/swagger/paths/v2_mercure-info.json b/docs/swagger/paths/v2_mercure-info.json new file mode 100644 index 00000000..24f7fb5f --- /dev/null +++ b/docs/swagger/paths/v2_mercure-info.json @@ -0,0 +1,67 @@ +{ + "get": { + "operationId": "mercureInfo", + "tags": [ + "Integrations" + ], + "summary": "Get mercure integration info", + "description": "Returns information to consume updates published by Shlink on a mercure hub. https://mercure.rocks/", + "parameters": [ + { + "$ref": "../parameters/version.json" + } + ], + "security": [ + { + "ApiKey": [] + } + ], + "responses": { + "200": { + "description": "The mercure integration info", + "content": { + "application/json": { + "schema": { + "$ref": "../definitions/MercureInfo.json" + } + } + }, + "examples": { + "application/json": { + "mercureHubUrl": "https://example.com/.well-known/mercure", + "jwt": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJTaGxpbmsiLCJpYXQiOjE1ODY2ODY3MzIsImV4cCI6MTU4Njk0NTkzMiwibWVyY3VyZSI6eyJzdWJzY3JpYmUiOltdfX0.P-519lgU7dFz0bbNlRG1CXyqugGbaHon4kw6fu4QBdQ", + "jwtExpiration": "2020-04-15T12:18:52+02:00" + } + } + }, + "501": { + "description": "This Shlink instance is not integrated with a mercure hub", + "content": { + "application/problem+json": { + "schema": { + "$ref": "../definitions/Error.json" + } + } + }, + "examples": { + "application/json": { + "title": "Mercure integration not configured", + "type": "MERCURE_NOT_CONFIGURED", + "detail": "This Shlink instance is not integrated with a mercure hub.", + "status": 501 + } + } + }, + "500": { + "description": "Unexpected error.", + "content": { + "application/problem+json": { + "schema": { + "$ref": "../definitions/Error.json" + } + } + } + } + } + } +} diff --git a/docs/swagger/swagger.json b/docs/swagger/swagger.json index 32e0caf3..c30bab97 100644 --- a/docs/swagger/swagger.json +++ b/docs/swagger/swagger.json @@ -82,6 +82,10 @@ "$ref": "paths/v1_short-urls_{shortCode}_visits.json" }, + "/rest/v{version}/mercure-info": { + "$ref": "paths/v2_mercure-info.json" + }, + "/rest/health": { "$ref": "paths/health.json" }, diff --git a/module/Rest/config/dependencies.config.php b/module/Rest/config/dependencies.config.php index b24ec1ee..e9fcbfa5 100644 --- a/module/Rest/config/dependencies.config.php +++ b/module/Rest/config/dependencies.config.php @@ -9,6 +9,7 @@ use Laminas\ServiceManager\AbstractFactory\ConfigAbstractFactory; use Laminas\ServiceManager\Factory\InvokableFactory; use Mezzio\Router\Middleware\ImplicitOptionsMiddleware; use Psr\Log\LoggerInterface; +use Shlinkio\Shlink\Common\Mercure\LcobucciJwtProvider; use Shlinkio\Shlink\Core\Options\AppOptions; use Shlinkio\Shlink\Core\Service; use Shlinkio\Shlink\Rest\Service\ApiKeyService; @@ -20,6 +21,7 @@ return [ ApiKeyService::class => ConfigAbstractFactory::class, Action\HealthAction::class => ConfigAbstractFactory::class, + Action\MercureAction::class => ConfigAbstractFactory::class, Action\ShortUrl\CreateShortUrlAction::class => ConfigAbstractFactory::class, Action\ShortUrl\SingleStepCreateShortUrlAction::class => ConfigAbstractFactory::class, Action\ShortUrl\EditShortUrlAction::class => ConfigAbstractFactory::class, @@ -46,6 +48,7 @@ return [ ApiKeyService::class => ['em'], Action\HealthAction::class => [Connection::class, AppOptions::class, 'Logger_Shlink'], + Action\MercureAction::class => [LcobucciJwtProvider::class, 'config.mercure', 'Logger_Shlink'], Action\ShortUrl\CreateShortUrlAction::class => [ Service\UrlShortener::class, 'config.url_shortener.domain', diff --git a/module/Rest/config/routes.config.php b/module/Rest/config/routes.config.php index b104d81b..afb44249 100644 --- a/module/Rest/config/routes.config.php +++ b/module/Rest/config/routes.config.php @@ -33,6 +33,8 @@ return [ Action\Tag\DeleteTagsAction::getRouteDef(), Action\Tag\CreateTagsAction::getRouteDef(), Action\Tag\UpdateTagAction::getRouteDef(), + + Action\MercureAction::getRouteDef(), ], ]; diff --git a/module/Rest/src/Action/MercureAction.php b/module/Rest/src/Action/MercureAction.php new file mode 100644 index 00000000..7c33fa31 --- /dev/null +++ b/module/Rest/src/Action/MercureAction.php @@ -0,0 +1,56 @@ +<?php + +declare(strict_types=1); + +namespace Shlinkio\Shlink\Rest\Action; + +use Cake\Chronos\Chronos; +use Laminas\Diactoros\Response\JsonResponse; +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; +use Psr\Log\LoggerInterface; +use Shlinkio\Shlink\Common\Mercure\JwtProviderInterface; +use Shlinkio\Shlink\Rest\Exception\MercureException; +use Throwable; + +class MercureAction extends AbstractRestAction +{ + protected const ROUTE_PATH = '/mercure-info'; + protected const ROUTE_ALLOWED_METHODS = [self::METHOD_GET]; + + private JwtProviderInterface $jwtProvider; + private array $mercureConfig; + + public function __construct( + JwtProviderInterface $jwtProvider, + array $mercureConfig, + ?LoggerInterface $logger = null + ) { + parent::__construct($logger); + $this->jwtProvider = $jwtProvider; + $this->mercureConfig = $mercureConfig; + } + + public function handle(ServerRequestInterface $request): ResponseInterface + { + $hubUrl = $this->mercureConfig['public_hub_url'] ?? null; + if ($hubUrl === null) { + throw MercureException::mercureNotConfigured(); + } + + $days = $this->mercureConfig['jwt_days_duration'] ?? 3; + $expiresAt = Chronos::now()->addDays($days); + + try { + $jwt = $this->jwtProvider->buildSubscriptionToken($expiresAt); + } catch (Throwable $e) { + throw MercureException::mercureNotConfigured($e); + } + + return new JsonResponse([ + 'mercureHubUrl' => $hubUrl, + 'token' => $jwt, + 'jwtExpiration' => $expiresAt->toAtomString(), + ]); + } +} diff --git a/module/Rest/src/Exception/MercureException.php b/module/Rest/src/Exception/MercureException.php new file mode 100644 index 00000000..6c318e93 --- /dev/null +++ b/module/Rest/src/Exception/MercureException.php @@ -0,0 +1,30 @@ +<?php + +declare(strict_types=1); + +namespace Shlinkio\Shlink\Rest\Exception; + +use Fig\Http\Message\StatusCodeInterface; +use Mezzio\ProblemDetails\Exception\CommonProblemDetailsExceptionTrait; +use Mezzio\ProblemDetails\Exception\ProblemDetailsExceptionInterface; +use Throwable; + +class MercureException extends RuntimeException implements ProblemDetailsExceptionInterface +{ + use CommonProblemDetailsExceptionTrait; + + private const TITLE = 'Mercure integration not configured'; + private const TYPE = 'MERCURE_NOT_CONFIGURED'; + + public static function mercureNotConfigured(?Throwable $prev = null): self + { + $e = new self('This Shlink instance is not integrated with a mercure hub.', 1, $prev); + + $e->detail = $e->getMessage(); + $e->title = self::TITLE; + $e->type = self::TYPE; + $e->status = StatusCodeInterface::STATUS_NOT_IMPLEMENTED; + + return $e; + } +}