diff --git a/.gitignore b/.gitignore index 869728c..ba05ac6 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,4 @@ webroot/theme/node_modules .vscode docker/run/ .phpunit.result.cache -config.json \ No newline at end of file +config.json diff --git a/composer.json b/composer.json index bc562a0..4e1af85 100644 --- a/composer.json +++ b/composer.json @@ -19,7 +19,9 @@ "cakephp/bake": "^2.0.3", "cakephp/cakephp-codesniffer": "~4.0.0", "cakephp/debug_kit": "^4.0", + "fzaninotto/faker": "^1.9", "josegonzalez/dotenv": "^3.2", + "league/openapi-psr7-validator": "^0.16.4", "phpunit/phpunit": "^8.5", "psy/psysh": "@stable" }, @@ -44,7 +46,6 @@ "scripts": { "post-install-cmd": "App\\Console\\Installer::postInstall", "post-create-project-cmd": "App\\Console\\Installer::postInstall", - "post-autoload-dump": "Cake\\Composer\\Installer\\PluginInstaller::postAutoloadDump", "check": [ "@test", "@cs-check" diff --git a/config/routes.php b/config/routes.php index ac1ab26..e467725 100644 --- a/config/routes.php +++ b/config/routes.php @@ -92,14 +92,14 @@ $routes->prefix('Open', function (RouteBuilder $routes) { $routes->fallbacks(DashedRoute::class); }); -/* - * If you need a different set of middleware or none at all, - * open new scope and define routes there. - * - * ``` - * $routes->scope('/api', function (RouteBuilder $builder) { - * // No $builder->applyMiddleware() here. - * // Connect API actions here. - * }); - * ``` - */ +// API routes +$routes->scope('/api', function (RouteBuilder $routes) { + // $routes->applyMiddleware('ratelimit', 'auth.api'); + $routes->scope('/v1', function (RouteBuilder $routes) { + // $routes->applyMiddleware('v1compat'); + $routes->setExtensions(['json']); + + // Generic API route + $routes->connect('/{controller}/{action}/*'); + }); +}); \ No newline at end of file diff --git a/src/Controller/ApiController.php b/src/Controller/ApiController.php new file mode 100644 index 0000000..65cd11c --- /dev/null +++ b/src/Controller/ApiController.php @@ -0,0 +1,19 @@ +set('url', $url); + } +} diff --git a/src/Controller/Component/ACLComponent.php b/src/Controller/Component/ACLComponent.php index afa70ff..0c6d897 100644 --- a/src/Controller/Component/ACLComponent.php +++ b/src/Controller/Component/ACLComponent.php @@ -193,6 +193,9 @@ class ACLComponent extends Component 'getBookmarks' => ['*'], 'saveBookmark' => ['*'], 'deleteBookmark' => ['*'] + ], + 'Api' => [ + 'index' => ['*'] ] ); diff --git a/templates/Api/index.php b/templates/Api/index.php new file mode 100644 index 0000000..96be4b8 --- /dev/null +++ b/templates/Api/index.php @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/tests/Fixture/AuthKeysFixture.php b/tests/Fixture/AuthKeysFixture.php index 5924c5b..8291811 100644 --- a/tests/Fixture/AuthKeysFixture.php +++ b/tests/Fixture/AuthKeysFixture.php @@ -11,24 +11,60 @@ class AuthKeysFixture extends TestFixture { public $connection = 'test'; - public const ADMIN_API_KEY = '4cd687b314a3b9c4d83264e6195b9a3706ef4c2f'; + public const ADMIN_API_KEY = 'd033e22ae348aeb5660fc2140aec35850c4da997'; + public const SYNC_API_KEY = '6b387ced110858dcbcda36edb044dc18f91a0894'; + public const ORG_ADMIN_API_KEY = '1c4685d281d478dbcebd494158024bc3539004d0'; + public const USER_API_KEY = '12dea96fec20593566ab75692c9949596833adc9'; public function init(): void { $hasher = new DefaultPasswordHasher(); + $faker = \Faker\Factory::create(); $this->records = [ [ - 'id' => 1, - 'uuid' => '3ebfbe50-e7d2-406e-a092-f031e604b6e5', + 'uuid' => $faker->uuid(), 'authkey' => $hasher->hash(self::ADMIN_API_KEY), - 'authkey_start' => '4cd6', - 'authkey_end' => '4c2f', + 'authkey_start' => substr(self::ADMIN_API_KEY, 0, 4), + 'authkey_end' => substr(self::ADMIN_API_KEY, -4), 'expiration' => 0, - 'user_id' => 1, + 'user_id' => UsersFixture::USER_ADMIN_ID, 'comment' => '', - 'created' => time(), - 'modified' => time() + 'created' => $faker->dateTime()->getTimestamp(), + 'modified' => $faker->dateTime()->getTimestamp() + ], + [ + 'uuid' => $faker->uuid(), + 'authkey' => $hasher->hash(self::SYNC_API_KEY), + 'authkey_start' => substr(self::SYNC_API_KEY, 0, 4), + 'authkey_end' => substr(self::SYNC_API_KEY, -4), + 'expiration' => 0, + 'user_id' => UsersFixture::USER_SYNC_ID, + 'comment' => '', + 'created' => $faker->dateTime()->getTimestamp(), + 'modified' => $faker->dateTime()->getTimestamp() + ], + [ + 'uuid' => $faker->uuid(), + 'authkey' => $hasher->hash(self::ORG_ADMIN_API_KEY), + 'authkey_start' => substr(self::ORG_ADMIN_API_KEY, 0, 4), + 'authkey_end' => substr(self::ORG_ADMIN_API_KEY, -4), + 'expiration' => 0, + 'user_id' => UsersFixture::USER_ORG_ADMIN_ID, + 'comment' => '', + 'created' => $faker->dateTime()->getTimestamp(), + 'modified' => $faker->dateTime()->getTimestamp() + ], + [ + 'uuid' => $faker->uuid(), + 'authkey' => $hasher->hash(self::USER_API_KEY), + 'authkey_start' => substr(self::USER_API_KEY, 0, 4), + 'authkey_end' => substr(self::USER_API_KEY, -4), + 'expiration' => 0, + 'user_id' => UsersFixture::USER_REGULAR_USER_ID, + 'comment' => '', + 'created' => $faker->dateTime()->getTimestamp(), + 'modified' => $faker->dateTime()->getTimestamp() ] ]; parent::init(); diff --git a/tests/Fixture/IndividualsFixture.php b/tests/Fixture/IndividualsFixture.php index 31261f2..9c96f8b 100644 --- a/tests/Fixture/IndividualsFixture.php +++ b/tests/Fixture/IndividualsFixture.php @@ -10,16 +10,64 @@ class IndividualsFixture extends TestFixture { public $connection = 'test'; - public $records = [ - [ - 'id' => 1, - 'uuid' => '3ebfbe50-e7d2-406e-a092-f031e604b6e1', - 'email' => 'admin@admin.test', - 'first_name' => 'admin', - 'last_name' => 'admin', - 'position' => 'admin', - 'created' => '2022-01-04 10:00:00', - 'modified' => '2022-01-04 10:00:00' - ] - ]; + // Admin individual + public const INDIVIDUAL_ADMIN_ID = 1; + + // Sync individual + public const INDIVIDUAL_SYNC_ID = 2; + + // Org Admin individual + public const INDIVIDUAL_ORG_ADMIN_ID = 3; + + // Regular User individual + public const INDIVIDUAL_REGULAR_USER_ID = 4; + + public function init(): void + { + $faker = \Faker\Factory::create(); + + $this->records = [ + [ + 'id' => self::INDIVIDUAL_ADMIN_ID, + 'uuid' => $faker->uuid(), + 'email' => $faker->email(), + 'first_name' => $faker->firstName, + 'last_name' => $faker->lastName, + 'position' => 'admin', + 'created' => $faker->dateTime()->getTimestamp(), + 'modified' => $faker->dateTime()->getTimestamp() + ], + [ + 'id' => self::INDIVIDUAL_SYNC_ID, + 'uuid' => $faker->uuid(), + 'email' => $faker->email(), + 'first_name' => $faker->firstName, + 'last_name' => $faker->lastName, + 'position' => 'sync', + 'created' => $faker->dateTime()->getTimestamp(), + 'modified' => $faker->dateTime()->getTimestamp() + ], + [ + 'id' => self::INDIVIDUAL_ORG_ADMIN_ID, + 'uuid' => $faker->uuid(), + 'email' => $faker->email(), + 'first_name' => $faker->firstName, + 'last_name' => $faker->lastName, + 'position' => 'org_admin', + 'created' => $faker->dateTime()->getTimestamp(), + 'modified' => $faker->dateTime()->getTimestamp() + ], + [ + 'id' => self::INDIVIDUAL_REGULAR_USER_ID, + 'uuid' => $faker->uuid(), + 'email' => $faker->email(), + 'first_name' => $faker->firstName, + 'last_name' => $faker->lastName, + 'position' => 'user', + 'created' => $faker->dateTime()->getTimestamp(), + 'modified' => $faker->dateTime()->getTimestamp() + ] + ]; + parent::init(); + } } diff --git a/tests/Fixture/OrganisationsFixture.php b/tests/Fixture/OrganisationsFixture.php new file mode 100644 index 0000000..d8b81f9 --- /dev/null +++ b/tests/Fixture/OrganisationsFixture.php @@ -0,0 +1,52 @@ +records = [ + [ + 'id' => self::ORGANISATION_A_ID, + 'uuid' => $faker->uuid(), + 'name' => 'Organisation A', + 'url' => $faker->url, + 'nationality' => $faker->countryCode, + 'sector' => 'IT', + 'type' => '', + 'contacts' => '', + 'created' => $faker->dateTime()->getTimestamp(), + 'modified' => $faker->dateTime()->getTimestamp() + ], + [ + 'id' => self::ORGANISATION_B_ID, + 'uuid' => $faker->uuid(), + 'name' => 'Organisation B', + 'url' => $faker->url, + 'nationality' => $faker->countryCode, + 'sector' => 'IT', + 'type' => '', + 'contacts' => '', + 'created' => $faker->dateTime()->getTimestamp(), + 'modified' => $faker->dateTime()->getTimestamp() + ] + ]; + parent::init(); + } +} diff --git a/tests/Fixture/RolesFixture.php b/tests/Fixture/RolesFixture.php index ced82a1..1230e8f 100644 --- a/tests/Fixture/RolesFixture.php +++ b/tests/Fixture/RolesFixture.php @@ -10,15 +10,53 @@ class RolesFixture extends TestFixture { public $connection = 'test'; - public $records = [ - [ - 'id' => 1, - 'uuid' => '3ebfbe50-e7d2-406e-a092-f031e604b6e4', - 'name' => 'admin', - 'is_default' => true, - 'perm_admin' => true, - 'perm_sync' => true, - 'perm_org_admin' => true - ] - ]; + public const ROLE_ADMIN_ID = 1; + public const ROLE_SYNC_ID = 2; + public const ROLE_ORG_ADMIN_ID = 3; + public const ROLE_REGULAR_USER_ID = 4; + + public function init(): void + { + $faker = \Faker\Factory::create(); + + $this->records = [ + [ + 'id' => self::ROLE_ADMIN_ID, + 'uuid' => $faker->uuid(), + 'name' => 'admin', + 'is_default' => false, + 'perm_admin' => true, + 'perm_sync' => false, + 'perm_org_admin' => false + ], + [ + 'id' => self::ROLE_SYNC_ID, + 'uuid' => $faker->uuid(), + 'name' => 'sync', + 'is_default' => false, + 'perm_admin' => false, + 'perm_sync' => true, + 'perm_org_admin' => false + ], + [ + 'id' => self::ROLE_ORG_ADMIN_ID, + 'uuid' => $faker->uuid(), + 'name' => 'org_admin', + 'is_default' => false, + 'perm_admin' => false, + 'perm_sync' => false, + 'perm_org_admin' => true + ], + [ + 'id' => self::ROLE_REGULAR_USER_ID, + 'uuid' => $faker->uuid(), + 'name' => 'user', + 'is_default' => true, + 'perm_admin' => false, + 'perm_sync' => false, + 'perm_org_admin' => false + ] + ]; + parent::init(); + } } diff --git a/tests/Fixture/UsersFixture.php b/tests/Fixture/UsersFixture.php index df6b8bd..ba35e7a 100644 --- a/tests/Fixture/UsersFixture.php +++ b/tests/Fixture/UsersFixture.php @@ -11,26 +11,80 @@ class UsersFixture extends TestFixture { public $connection = 'test'; - public const ADMIN_USER = 'admin'; - public const ADMIN_PASSWORD = 'Password1234'; + // Admin user + public const USER_ADMIN_ID = 1; + public const USER_ADMIN_USERNAME = 'admin'; + public const USER_ADMIN_PASSWORD = 'AdminPassword'; + + // Sync user + public const USER_SYNC_ID = 2; + public const USER_SYNC_USERNAME = 'sync'; + public const USER_SYNC_PASSWORD = 'SyncPassword'; + + // Org Admin user + public const USER_ORG_ADMIN_ID = 3; + public const USER_ORG_ADMIN_USERNAME = 'org_admin'; + public const USER_ORG_ADMIN_PASSWORD = 'OrgAdminPassword'; + + // Regular User user + public const USER_REGULAR_USER_ID = 4; + public const USER_REGULAR_USER_USERNAME = 'user'; + public const USER_REGULAR_USER_PASSWORD = 'UserPassword'; + public function init(): void { $hasher = new DefaultPasswordHasher(); - + $faker = \Faker\Factory::create(); $this->records = [ [ - 'id' => 1, - 'uuid' => '3ebfbe50-e7d2-406e-a092-f031e604b6e5', - 'username' => self::ADMIN_USER, - 'password' => $hasher->hash(self::ADMIN_PASSWORD), - 'role_id' => 1, - 'individual_id' => 1, + 'id' => self::USER_ADMIN_ID, + 'uuid' => $faker->uuid(), + 'username' => self::USER_ADMIN_USERNAME, + 'password' => $hasher->hash(self::USER_ADMIN_PASSWORD), + 'role_id' => RolesFixture::ROLE_ADMIN_ID, + 'individual_id' => IndividualsFixture::INDIVIDUAL_ADMIN_ID, 'disabled' => 0, - 'organisation_id' => 1, - 'created' => '2022-01-04 10:00:00', - 'modified' => '2022-01-04 10:00:00' + 'organisation_id' => OrganisationsFixture::ORGANISATION_A_ID, + 'created' => $faker->dateTime()->getTimestamp(), + 'modified' => $faker->dateTime()->getTimestamp() + ], + [ + 'id' => self::USER_SYNC_ID, + 'uuid' => $faker->uuid(), + 'username' => self::USER_SYNC_USERNAME, + 'password' => $hasher->hash(self::USER_SYNC_PASSWORD), + 'role_id' => RolesFixture::ROLE_SYNC_ID, + 'individual_id' => IndividualsFixture::INDIVIDUAL_SYNC_ID, + 'disabled' => 0, + 'organisation_id' => OrganisationsFixture::ORGANISATION_A_ID, + 'created' => $faker->dateTime()->getTimestamp(), + 'modified' => $faker->dateTime()->getTimestamp() + ], + [ + 'id' => self::USER_ORG_ADMIN_ID, + 'uuid' => $faker->uuid(), + 'username' => self::USER_ORG_ADMIN_USERNAME, + 'password' => $hasher->hash(self::USER_ORG_ADMIN_PASSWORD), + 'role_id' => RolesFixture::ROLE_ORG_ADMIN_ID, + 'individual_id' => IndividualsFixture::INDIVIDUAL_ORG_ADMIN_ID, + 'disabled' => 0, + 'organisation_id' => OrganisationsFixture::ORGANISATION_A_ID, + 'created' => $faker->dateTime()->getTimestamp(), + 'modified' => $faker->dateTime()->getTimestamp() + ], + [ + 'id' => self::USER_REGULAR_USER_ID, + 'uuid' => $faker->uuid(), + 'username' => self::USER_REGULAR_USER_USERNAME, + 'password' => $hasher->hash(self::USER_REGULAR_USER_PASSWORD), + 'role_id' => RolesFixture::ROLE_REGULAR_USER_ID, + 'individual_id' => IndividualsFixture::INDIVIDUAL_REGULAR_USER_ID, + 'disabled' => 0, + 'organisation_id' => OrganisationsFixture::ORGANISATION_A_ID, + 'created' => $faker->dateTime()->getTimestamp(), + 'modified' => $faker->dateTime()->getTimestamp() ] ]; parent::init(); diff --git a/tests/Helper/ApiTestTrait.php b/tests/Helper/ApiTestTrait.php new file mode 100644 index 0000000..e749677 --- /dev/null +++ b/tests/Helper/ApiTestTrait.php @@ -0,0 +1,79 @@ +_authToken = $authToken; + + // somehow this is not set automatically in test environment + $_SERVER['HTTP_AUTHORIZATION'] = $authToken; + + $this->configRequest([ + 'headers' => [ + 'Accept' => 'application/json', + 'Authorization' => $this->_authToken + ] + ]); + } + + /** + * Parse the OpenAPI specification and create a validator + * + * @param string $specFile + * @return void + */ + public function initializeValidator(string $specFile): void + { + $this->validator = (new ValidatorBuilder)->fromYamlFile($specFile); + $this->requestValidator = $this->validator->getRequestValidator(); + $this->responseValidator = $this->validator->getResponseValidator(); + } + + /** + * Validates the API request against the OpenAPI spec + * + * @param string $path The path to the API endpoint + * @param string $method The HTTP method used to call the endpoint + * @return void + */ + public function validateRequest(string $endpoint, string $method = 'get'): void + { + // TODO: find a workaround to create a PSR-7 request object for validation + throw NotImplementedException("Unfortunately cakephp does not save the PSR-7 request object in the test context"); + } + + /** + * Validates the API response against the OpenAPI spec + * + * @param string $path The path to the API endpoint + * @param string $method The HTTP method used to call the endpoint + * @return void + */ + public function validateResponse(string $endpoint, string $method = 'get'): void + { + $address = new OperationAddress($endpoint, $method); + $this->responseValidator->validate($address, $this->_response); + } +} diff --git a/tests/README.md b/tests/README.md index 898e249..f37bcf3 100644 --- a/tests/README.md +++ b/tests/README.md @@ -1,5 +1,14 @@ # Testing -Add a test database to your `config/app_local.php` config file and set `debug` mode to `true`. + +1. Add a `cerebrate_test` database to the db: +```mysql +CREATE DATABASE cerebrate_test; +GRANT ALL PRIVILEGES ON cerebrate_test.* to cerebrate@localhost; +FLUSH PRIVILEGES; +QUIT; +``` + +2. Add a the test database to your `config/app_local.php` config file and set `debug` mode to `true`. ```php 'debug' => true, 'Datasources' => [ diff --git a/tests/TestCase/Api/Users/UsersApiTest.php b/tests/TestCase/Api/Users/UsersApiTest.php new file mode 100644 index 0000000..2f2c2b8 --- /dev/null +++ b/tests/TestCase/Api/Users/UsersApiTest.php @@ -0,0 +1,55 @@ +initializeValidator(APP . '../webroot/docs/openapi.yaml'); + } + + public function testViewMe(): void + { + $this->setAuthToken(AuthKeysFixture::ADMIN_API_KEY); + $this->get(self::ENDPOINT); + + $this->assertResponseOk(); + $this->assertResponseContains(sprintf('"username": "%s"', UsersFixture::USER_ADMIN_USERNAME)); + // TODO: $this->validateRequest() + $this->validateResponse(self::ENDPOINT); + } + + public function testViewById(): void + { + $this->setAuthToken(AuthKeysFixture::ADMIN_API_KEY); + $url = sprintf('%s/%d', self::ENDPOINT, UsersFixture::USER_ADMIN_ID); + $this->get($url); + + $this->assertResponseOk(); + $this->assertResponseContains(sprintf('"username": "%s"', UsersFixture::USER_ADMIN_USERNAME)); + // TODO: $this->validateRequest() + $this->validateResponse($url); + } +} diff --git a/tests/TestCase/Api/UsersApiTest.php b/tests/TestCase/Api/UsersApiTest.php deleted file mode 100644 index 5353305..0000000 --- a/tests/TestCase/Api/UsersApiTest.php +++ /dev/null @@ -1,40 +0,0 @@ -configRequest([ - 'headers' => [ - // this does not work: https://book.cakephp.org/4/en/development/testing.html#testing-stateless-authentication-and-apis - // 'Authorization' => AuthKeysFixture::ADMIN_API_KEY, - 'Accept' => 'application/json' - ] - ]); - - $this->get('/users/view'); - - $this->assertResponseOk(); - $this->assertResponseContains(sprintf('"username": "%s"', UsersFixture::ADMIN_USER)); - } -} diff --git a/tests/TestCase/Controller/UsersControllerTest.php b/tests/TestCase/Controller/Users/UsersControllerTest.php similarity index 93% rename from tests/TestCase/Controller/UsersControllerTest.php rename to tests/TestCase/Controller/Users/UsersControllerTest.php index 0c856e8..37dd73a 100644 --- a/tests/TestCase/Controller/UsersControllerTest.php +++ b/tests/TestCase/Controller/Users/UsersControllerTest.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace App\Test\TestCase\Controller; +namespace App\Test\TestCase\Controller\Users; use Cake\TestSuite\IntegrationTestTrait; use Cake\TestSuite\TestCase; diff --git a/tests/bootstrap.php b/tests/bootstrap.php index f8088be..9283095 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -66,4 +66,6 @@ if (!in_array('skip-migrations', $_SERVER['argv'])) { ['plugin' => 'Tags', 'connection' => 'test', 'skip' => ['*']], ['plugin' => 'ADmad/SocialAuth', 'connection' => 'test', 'skip' => ['*']] ]); +}else{ + echo "[ * ] Skipping migrations ...\n"; } diff --git a/webroot/docs/openapi.yaml b/webroot/docs/openapi.yaml new file mode 100644 index 0000000..5a55225 --- /dev/null +++ b/webroot/docs/openapi.yaml @@ -0,0 +1,215 @@ +openapi: 3.0.0 +info: + version: 1.3.0 + title: Cerebrate Project API + description: | + + TODO: markdown description + +servers: + - url: https://cerebrate.local + +tags: + - name: Users + description: "TODO: users resource descriptions" + +paths: + /api/v1/users/view: + get: + summary: "Get information about the current user" + operationId: viewUserMe + tags: + - Users + responses: + "200": + $ref: "#/components/responses/ViewUserResponse" + "403": + $ref: "#/components/responses/UnauthorizedApiErrorResponse" + default: + $ref: "#/components/responses/ApiErrorResponse" + + /api/v1/users/view/{userId}: + get: + summary: "Get information of a user by id" + operationId: viewUserById + tags: + - Users + parameters: + - $ref: "#/components/parameters/userId" + responses: + "200": + $ref: "#/components/responses/ViewUserResponse" + "403": + $ref: "#/components/responses/UnauthorizedApiErrorResponse" + default: + $ref: "#/components/responses/ApiErrorResponse" + +components: + schemas: + # General + UUID: + type: string + format: uuid + maxLength: 36 + example: "c99506a6-1255-4b71-afa5-7b8ba48c3b1b" + + ID: + type: integer + format: int32 + example: 1 + + DateTime: + type: string + format: datetime + example: "2022-01-05T11:19:26+00:00" + + # Users + Username: + type: string + example: "admin" + + User: + type: object + properties: + id: + $ref: "#/components/schemas/ID" + uuid: + $ref: "#/components/schemas/UUID" + username: + $ref: "#/components/schemas/Username" + role_id: + $ref: "#/components/schemas/ID" + individual_id: + $ref: "#/components/schemas/ID" + disabled: + type: boolean + created: + $ref: "#/components/schemas/DateTime" + modified: + $ref: "#/components/schemas/DateTime" + organisation_id: + $ref: "#/components/schemas/ID" + + # Individuals + + # Organisations + + # Roles + RoleName: + type: string + maxLength: 255 + example: "admin" + + Role: + type: object + properties: + id: + $ref: "#/components/schemas/ID" + name: + $ref: "#/components/schemas/RoleName" + is_default: + type: boolean + perm_admin: + type: boolean + perm_sync: + type: boolean + perm_org_admin: + type: boolean + + # Errors + ApiError: + type: object + required: + - name + - message + - url + properties: + name: + type: string + message: + type: string + url: + type: string + example: "/users" + + UnauthorizedApiError: + type: object + required: + - name + - message + - url + properties: + name: + type: string + example: "Authentication failed. Please make sure you pass the API key of an API enabled user along in the Authorization header." + message: + type: string + example: "Authentication failed. Please make sure you pass the API key of an API enabled user along in the Authorization header." + url: + type: string + example: "/users" + + NotFoundApiError: + type: object + required: + - name + - message + - url + properties: + name: + type: string + example: "Invalid user" + message: + type: string + example: "Invalid user" + url: + type: string + example: "/users/1234" + + parameters: + userId: + name: userId + in: path + description: "Numeric ID of the User" + required: true + schema: + $ref: "#/components/schemas/ID" + + securitySchemes: + ApiKeyAuth: + type: apiKey + in: header + name: Authorization + description: | + The authorization is performed by using the following header in the HTTP requests: + + Authorization: YOUR_API_KEY + + # requestBodies: + + responses: + # User + ViewUserResponse: + description: "User response" + content: + application/json: + schema: + $ref: "#/components/schemas/User" + + # Errors + ApiErrorResponse: + description: "Unexpected API error" + content: + application/json: + schema: + $ref: "#/components/schemas/ApiError" + + UnauthorizedApiErrorResponse: + description: "Authentication failed. Please make sure you pass the API key of an API enabled user along in the Authorization header." + content: + application/json: + schema: + $ref: "#/components/schemas/UnauthorizedApiError" + +security: + - ApiKeyAuth: []