diff --git a/composer.json b/composer.json index 7f1ef082..58e9d40d 100644 --- a/composer.json +++ b/composer.json @@ -73,5 +73,8 @@ "serve": "php -S 0.0.0.0:8000 -t public/", "test": "phpunit --coverage-clover build/clover.xml", "pretty-test": "phpunit --coverage-html build/coverage" + }, + "config": { + "process-timeout": 0 } } diff --git a/docs/swagger.yaml b/docs/swagger.yaml new file mode 100644 index 00000000..27037fe5 --- /dev/null +++ b/docs/swagger.yaml @@ -0,0 +1,289 @@ +swagger: '2.0' +info: + title: Shlink + description: Shlink, the self-hosted URL shortener + version: "1.2.0" + +schemes: + - https + +basePath: /rest +produces: + - application/json + +paths: + /authenticate: + post: + description: Performs an authentication + parameters: + - name: apiKey + in: formData + description: The API key to authenticate with + required: true + type: string + responses: + 200: + description: The authentication worked. + schema: + type: object + properties: + token: + type: string + description: The authentication token that needs to be sent in the Authorization header + 400: + description: An API key was not provided. + schema: + $ref: '#/definitions/Error' + 401: + description: The API key is incorrect, is disabled or has expired. + schema: + $ref: '#/definitions/Error' + 500: + description: Unexpected error. + schema: + $ref: '#/definitions/Error' + /short-codes: + get: + description: Returns the list of short codes + parameters: + - name: page + in: query + description: The page to be displayed. Defaults to 1 + required: false + type: integer + - name: Authorization + in: header + description: The authorization token with Bearer type + required: true + type: string + responses: + 200: + description: The list of short URLs + schema: + type: object + properties: + shortUrls: + type: object + properties: + data: + type: array + items: + $ref: '#/definitions/ShortUrl' + pagination: + $ref: '#/definitions/Pagination' + 500: + description: Unexpected error. + schema: + $ref: '#/definitions/Error' + post: + description: Creates a new short code + parameters: + - name: longUrl + in: formData + description: The URL to parse + required: true + type: string + - name: tags + in: formData + description: The URL to parse + required: false + type: array + items: + type: string + - name: Authorization + in: header + description: The authorization token with Bearer type + required: true + type: string + responses: + 200: + description: The result of parsing the long URL + schema: + type: object + properties: + longUrl: + type: string + description: The original long URL that has been parsed + shortUrl: + type: string + description: The generated short URL + shortCode: + type: string + description: the short code that is being used in the short URL + 400: + description: The long URL was not provided or is invalid. + schema: + $ref: '#/definitions/Error' + 500: + description: Unexpected error. + schema: + $ref: '#/definitions/Error' + /short-codes/{shortCode}: + get: + description: Get the long URL behind a short code. + parameters: + - name: shortCode + in: path + type: string + description: The short code to resolve. + required: true + - name: Authorization + in: header + description: The authorization token with Bearer type + required: true + type: string + responses: + 200: + description: The long URL behind a short code. + schema: + type: object + properties: + longUrl: + type: string + description: The original long URL behind the short code. + 404: + description: No URL was found for provided short code. + schema: + $ref: '#/definitions/Error' + 400: + description: Provided shortCode does not match the character set currently used by the app to generate short codes. + schema: + $ref: '#/definitions/Error' + 500: + description: Unexpected error. + schema: + $ref: '#/definitions/Error' + /short-codes/{shortCode}/visits: + get: + description: Get the list of visits on provided short code. + parameters: + - name: shortCode + in: path + type: string + description: The shortCode from which we want to get the visits. + required: true + - name: Authorization + in: header + description: The authorization token with Bearer type + required: true + type: string + responses: + 200: + description: List of visits. + schema: + type: object + properties: + visits: + type: object + properties: + data: + type: array + items: + $ref: '#/definitions/Visit' + 404: + description: The short code does not belong to any short URL. + schema: + $ref: '#/definitions/Error' + 500: + description: Unexpected error. + schema: + $ref: '#/definitions/Error' + /short-codes/{shortCode}/tags: + put: + description: Edit the tags on provided short code. + parameters: + - name: shortCode + in: path + type: string + description: The shortCode in which we want to edit tags. + required: true + - name: tags + in: formData + type: array + items: + type: string + description: The list of tags to set to the short URL. + required: true + - name: Authorization + in: header + description: The authorization token with Bearer type + required: true + type: string + responses: + 200: + description: List of tags. + schema: + type: object + properties: + tags: + type: array + items: + type: string + 400: + description: The request body does not contain a "tags" param with array type. + schema: + $ref: '#/definitions/Error' + 404: + description: No short URL was found for provided short code. + schema: + $ref: '#/definitions/Error' + 500: + description: Unexpected error. + schema: + $ref: '#/definitions/Error' + +definitions: + ShortUrl: + type: object + properties: + shortCode: + type: string + description: The short code for this short URL. + originalUrl: + type: string + description: The original long URL. + dateCreated: + type: string + format: date-time + description: The date in which the short URL was created in ISO format. + visitsCount: + type: integer + description: The number of visits that this short URL has recieved. + tags: + type: array + items: + type: string + description: A list of tags applied to this short URL + + Visit: + type: object + properties: + referer: + type: string + date: + type: string + format: date-time + remoteAddr: + type: string + userAgent: + type: string + + Error: + type: object + properties: + code: + type: string + description: A machine unique code + message: + type: string + description: A human-friendly error message + + Pagination: + type: object + properties: + currentPage: + type: integer + description: The number of current page being displayed. + pagesCount: + type: integer + description: The total number of pages that can be displayed. diff --git a/module/Core/src/Repository/VisitRepository.php b/module/Core/src/Repository/VisitRepository.php index 743427f6..f86017a4 100644 --- a/module/Core/src/Repository/VisitRepository.php +++ b/module/Core/src/Repository/VisitRepository.php @@ -29,12 +29,6 @@ class VisitRepository extends EntityRepository implements VisitRepositoryInterfa $shortUrl = $shortUrl instanceof ShortUrl ? $shortUrl : $this->getEntityManager()->find(ShortUrl::class, $shortUrl); - if (! isset($dateRange) || $dateRange->isEmpty()) { - $startDate = $shortUrl->getDateCreated(); - $endDate = clone $startDate; - $endDate->add(new \DateInterval('P2D')); - $dateRange = new DateRange($startDate, $endDate); - } $qb = $this->createQueryBuilder('v'); $qb->where($qb->expr()->eq('v.shortUrl', ':shortUrl')) diff --git a/module/Rest/src/Action/AuthenticateAction.php b/module/Rest/src/Action/AuthenticateAction.php index 020a0fb5..a20e5964 100644 --- a/module/Rest/src/Action/AuthenticateAction.php +++ b/module/Rest/src/Action/AuthenticateAction.php @@ -66,7 +66,7 @@ class AuthenticateAction extends AbstractRestAction // Authenticate using provided API key $apiKey = $this->apiKeyService->getByKey($authData['apiKey']); - if (! $apiKey->isValid()) { + if (! isset($apiKey) || ! $apiKey->isValid()) { return new JsonResponse([ 'error' => RestUtils::INVALID_API_KEY_ERROR, 'message' => $this->translator->translate('Provided API key does not exist or is invalid.'), diff --git a/module/Rest/src/Middleware/CheckAuthenticationMiddleware.php b/module/Rest/src/Middleware/CheckAuthenticationMiddleware.php index 53f6cbe9..5b18ea31 100644 --- a/module/Rest/src/Middleware/CheckAuthenticationMiddleware.php +++ b/module/Rest/src/Middleware/CheckAuthenticationMiddleware.php @@ -13,6 +13,7 @@ use Shlinkio\Shlink\Rest\Util\RestUtils; use Zend\Diactoros\Response\JsonResponse; use Zend\Expressive\Router\RouteResult; use Zend\I18n\Translator\TranslatorInterface; +use Zend\Stdlib\ErrorHandler; use Zend\Stratigility\MiddlewareInterface; class CheckAuthenticationMiddleware implements MiddlewareInterface @@ -117,9 +118,11 @@ class CheckAuthenticationMiddleware implements MiddlewareInterface } try { + ErrorHandler::start(); if (! $this->jwtService->verify($jwt)) { return $this->createTokenErrorResponse(); } + ErrorHandler::stop(true); // Update the token expiration and continue to next middleware $jwt = $this->jwtService->refresh($jwt); @@ -131,6 +134,14 @@ class CheckAuthenticationMiddleware implements MiddlewareInterface } catch (AuthenticationException $e) { $this->logger->warning('Tried to access API with an invalid JWT.' . PHP_EOL . $e); return $this->createTokenErrorResponse(); + } catch (\Exception $e) { + $this->logger->warning('Unexpected error occurred.' . PHP_EOL . $e); + return $this->createTokenErrorResponse(); + } catch (\Throwable $e) { + $this->logger->warning('Unexpected error occurred.' . PHP_EOL . $e); + return $this->createTokenErrorResponse(); + } finally { + ErrorHandler::clean(); } } diff --git a/module/Rest/src/Middleware/CrossDomainMiddleware.php b/module/Rest/src/Middleware/CrossDomainMiddleware.php index 4327df9e..d6b84d5b 100644 --- a/module/Rest/src/Middleware/CrossDomainMiddleware.php +++ b/module/Rest/src/Middleware/CrossDomainMiddleware.php @@ -41,7 +41,8 @@ class CrossDomainMiddleware implements MiddlewareInterface } // Add Allow-Origin header - $response = $response->withHeader('Access-Control-Allow-Origin', $request->getHeader('Origin')); + $response = $response->withHeader('Access-Control-Allow-Origin', $request->getHeader('Origin')) + ->withHeader('Access-Control-Expose-Headers', 'Authorization'); if ($request->getMethod() !== 'OPTIONS') { return $response; }