From 98ab632d196263e27bb58fe8b1f0d57277377ebd Mon Sep 17 00:00:00 2001 From: alexmerlin Date: Wed, 26 Nov 2025 18:19:20 +0200 Subject: [PATCH] Issue #141: Added v7 documentation Signed-off-by: alexmerlin --- docs/book/v7/commands/create-admin-account.md | 37 + .../commands/display-available-endpoints.md | 73 ++ .../commands/generate-database-migrations.md | 64 ++ docs/book/v7/commands/generate-tokens.md | 64 ++ docs/book/v7/core-features/authentication.md | 114 +++ docs/book/v7/core-features/authorization.md | 72 ++ .../v7/core-features/content-validation.md | 97 ++ .../v7/core-features/dependency-injection.md | 57 ++ docs/book/v7/core-features/error-reporting.md | 125 +++ docs/book/v7/core-features/exceptions.md | 127 +++ .../rendering-and-sending-emails.md | 44 + .../book/v7/extended-features/core-and-app.md | 38 + .../v7/extended-features/handler-structure.md | 43 + .../injectable-input-filters.md | 72 ++ .../v7/extended-features/problem-details.md | 78 ++ .../v7/extended-features/route-grouping.md | 29 + docs/book/v7/flow/default-library-flow.md | 5 + docs/book/v7/flow/library-flow-for-email.md | 5 + docs/book/v7/flow/middleware-flow.md | 5 + docs/book/v7/installation/composer.md | 72 ++ .../v7/installation/configuration-files.md | 17 + docs/book/v7/installation/doctrine-orm.md | 168 ++++ docs/book/v7/installation/faq.md | 39 + docs/book/v7/installation/getting-started.md | 13 + .../v7/installation/test-the-installation.md | 33 + docs/book/v7/introduction/file-structure.md | 116 +++ docs/book/v7/introduction/introduction.md | 90 ++ docs/book/v7/introduction/packages.md | 33 + docs/book/v7/introduction/psr.md | 38 + .../v7/introduction/server-requirements.md | 46 + .../book/v7/openapi/generate-documentation.md | 53 ++ docs/book/v7/openapi/getting-help.md | 9 + .../book/v7/openapi/initialized-components.md | 234 +++++ docs/book/v7/openapi/introduction.md | 5 + docs/book/v7/openapi/render-documentation.md | 81 ++ docs/book/v7/openapi/use-documentation.md | 116 +++ docs/book/v7/openapi/write-documentation.md | 99 +++ .../v7/reference/account-anonymization.md | 40 + docs/book/v7/security/basic-security.md | 85 ++ docs/book/v7/security/oauth2-security.md | 33 + .../api-tools-vs-dotkernel-api.md | 21 + .../discovery-phase.md | 37 + .../transition-approach.md | 14 + docs/book/v7/tutorials/api-evolution.md | 65 ++ docs/book/v7/tutorials/cors.md | 90 ++ .../create-book-module-via-dot-maker.md | 432 +++++++++ docs/book/v7/tutorials/create-book-module.md | 834 ++++++++++++++++++ .../v7/tutorials/find-user-by-identity.md | 212 +++++ .../book/v7/tutorials/token-authentication.md | 361 ++++++++ docs/book/v7/upgrading/UPGRADE-6.0.md | 24 + docs/book/v7/upgrading/UPGRADE-7.0.md | 9 + docs/book/v7/upgrading/upgrading.md | 19 + mkdocs.yml | 72 +- 53 files changed, 4756 insertions(+), 3 deletions(-) create mode 100644 docs/book/v7/commands/create-admin-account.md create mode 100644 docs/book/v7/commands/display-available-endpoints.md create mode 100644 docs/book/v7/commands/generate-database-migrations.md create mode 100644 docs/book/v7/commands/generate-tokens.md create mode 100644 docs/book/v7/core-features/authentication.md create mode 100644 docs/book/v7/core-features/authorization.md create mode 100644 docs/book/v7/core-features/content-validation.md create mode 100644 docs/book/v7/core-features/dependency-injection.md create mode 100644 docs/book/v7/core-features/error-reporting.md create mode 100644 docs/book/v7/core-features/exceptions.md create mode 100644 docs/book/v7/core-features/rendering-and-sending-emails.md create mode 100644 docs/book/v7/extended-features/core-and-app.md create mode 100644 docs/book/v7/extended-features/handler-structure.md create mode 100644 docs/book/v7/extended-features/injectable-input-filters.md create mode 100644 docs/book/v7/extended-features/problem-details.md create mode 100644 docs/book/v7/extended-features/route-grouping.md create mode 100644 docs/book/v7/flow/default-library-flow.md create mode 100644 docs/book/v7/flow/library-flow-for-email.md create mode 100644 docs/book/v7/flow/middleware-flow.md create mode 100644 docs/book/v7/installation/composer.md create mode 100644 docs/book/v7/installation/configuration-files.md create mode 100644 docs/book/v7/installation/doctrine-orm.md create mode 100644 docs/book/v7/installation/faq.md create mode 100644 docs/book/v7/installation/getting-started.md create mode 100644 docs/book/v7/installation/test-the-installation.md create mode 100644 docs/book/v7/introduction/file-structure.md create mode 100644 docs/book/v7/introduction/introduction.md create mode 100644 docs/book/v7/introduction/packages.md create mode 100644 docs/book/v7/introduction/psr.md create mode 100644 docs/book/v7/introduction/server-requirements.md create mode 100644 docs/book/v7/openapi/generate-documentation.md create mode 100644 docs/book/v7/openapi/getting-help.md create mode 100644 docs/book/v7/openapi/initialized-components.md create mode 100644 docs/book/v7/openapi/introduction.md create mode 100644 docs/book/v7/openapi/render-documentation.md create mode 100644 docs/book/v7/openapi/use-documentation.md create mode 100644 docs/book/v7/openapi/write-documentation.md create mode 100644 docs/book/v7/reference/account-anonymization.md create mode 100644 docs/book/v7/security/basic-security.md create mode 100644 docs/book/v7/security/oauth2-security.md create mode 100644 docs/book/v7/transition-from-api-tools/api-tools-vs-dotkernel-api.md create mode 100644 docs/book/v7/transition-from-api-tools/discovery-phase.md create mode 100644 docs/book/v7/transition-from-api-tools/transition-approach.md create mode 100644 docs/book/v7/tutorials/api-evolution.md create mode 100644 docs/book/v7/tutorials/cors.md create mode 100644 docs/book/v7/tutorials/create-book-module-via-dot-maker.md create mode 100644 docs/book/v7/tutorials/create-book-module.md create mode 100644 docs/book/v7/tutorials/find-user-by-identity.md create mode 100644 docs/book/v7/tutorials/token-authentication.md create mode 100644 docs/book/v7/upgrading/UPGRADE-6.0.md create mode 100644 docs/book/v7/upgrading/UPGRADE-7.0.md create mode 100644 docs/book/v7/upgrading/upgrading.md diff --git a/docs/book/v7/commands/create-admin-account.md b/docs/book/v7/commands/create-admin-account.md new file mode 100644 index 00000000..551dcdf4 --- /dev/null +++ b/docs/book/v7/commands/create-admin-account.md @@ -0,0 +1,37 @@ +# Creating admin accounts in Dotkernel API + +## Usage + +Run the following command in your application’s root directory: + +```shell +php ./bin/cli.php admin:create-admin -i {IDENTITY} -p {PASSWORD} -f {FIRST_NAME} -l {LAST_NAME} +``` + +OR + +```shell +php ./bin/cli.php admin:create-admin --identity {IDENTITY} --password {PASSWORD} --firstName {FIRST_NAME} --lastName {LAST_NAME} +``` + +after replacing: + +* {IDENTITY} with a valid username OR email address +* {PASSWORD} with a valid password +* {FIRST_NAME} and {LAST_NAME} with valid names + +> If the specified fields contain special characters, make sure you surround them with double quote signs this method does not allow specifying an admin role – newly created accounts will have a role of admin. + +If the submitted data is valid, the outputted response is: + +```text +Admin account has been created. +``` + +The new admin account is ready to use. + +You can get more help with this command by running: + +```shell +php ./bin/cli.php help admin:create +``` diff --git a/docs/book/v7/commands/display-available-endpoints.md b/docs/book/v7/commands/display-available-endpoints.md new file mode 100644 index 00000000..3c087250 --- /dev/null +++ b/docs/book/v7/commands/display-available-endpoints.md @@ -0,0 +1,73 @@ +# Displaying Dotkernel API endpoints using dot-cli + +## Usage + +Run the following command in your application’s root directory: + +```shell +php ./bin/cli.php route:list +``` + +The command runs through all routes and extracts endpoint information in realtime. +The output should be similar to the following: + +```text ++-------------------- 37 Routes ------+-------------------------------------+ +| Request method | Route name | Route path | ++----------------+-------------------------------------+-------------------------------------+ +| GET | app::view-index | / | +| GET | admin::list-admin | /admin | +| POST | admin::create-admin | /admin | +| GET | admin::view-account | /admin/account | +| PATCH | admin::update-account | /admin/account | +| GET | admin::list-role | /admin/role | +| GET | admin::view-role | /admin/role/{uuid} | +| DELETE | admin::delete-admin | /admin/{uuid} | +| GET | admin::view-admin | /admin/{uuid} | +| PATCH | admin::update-admin | /admin/{uuid} | +| POST | app::create-error-report | /error-report | +| POST | security::token | /security/token | +| GET | user::list-user | /user | +| POST | user::create-user | /user | +| DELETE | user::delete-account | /user/account | +| GET | user::view-account | /user/account | +| PATCH | user::update-account | /user/account | +| POST | user::create-account | /user/account | +| POST | user::request-activate-account | /user/account/activate | +| PATCH | user::activate-account | /user/account/activate/{hash} | +| DELETE | user::delete-account-avatar | /user/account/avatar | +| GET | user::view-account-avatar | /user/account/avatar | +| POST | user::create-account-avatar | /user/account/avatar | +| POST | user::recover-account | /user/account/recover | +| POST | user::create-account-reset-password | /user/account/reset-password | +| GET | user::check-account-reset-password | /user/account/reset-password/{hash} | +| PATCH | user::update-account-reset-password | /user/account/reset-password/{hash} | +| GET | user::list-role | /user/role | +| GET | user::view-role | /user/role/{uuid} | +| DELETE | user::delete-user | /user/{uuid} | +| GET | user::view-user | /user/{uuid} | +| PATCH | user::update-user | /user/{uuid} | +| PATCH | user::activate-user | /user/{uuid}/activate | +| DELETE | user::delete-user-avatar | /user/{uuid}/avatar | +| GET | user::view-user-avatar | /user/{uuid}/avatar | +| POST | user::create-user-avatar | /user/{uuid}/avatar | +| PATCH | user::deactivate-user | /user/{uuid}/deactivate | ++------+----------------+-------------------------------------+-------------------------------------+ + +``` + +## Filtering results + +The following filters can be applied when displaying the route list: + +* Filter routes by name, using: `-i|--name[=NAME]` +* Filter routes by path, using: `-p|--path[=PATH]` +* Filter routes by method, using: `-m|--method[=METHOD]` + +The filters are case-insensitive and can be combined. + +Get more help by running this command: + +```shell +php ./bin/cli.php route:list --help +``` diff --git a/docs/book/v7/commands/generate-database-migrations.md b/docs/book/v7/commands/generate-database-migrations.md new file mode 100644 index 00000000..d0df10ef --- /dev/null +++ b/docs/book/v7/commands/generate-database-migrations.md @@ -0,0 +1,64 @@ +# Generate a database migration without dropping custom tables + +## Usage + +Run the following command in your application’s root directory: + +```shell +vendor/bin/doctrine-migrations diff +``` + +If you have mapping modifications, this will create a new migration file under `data/doctrine/migrations/` directory. +Opening the migration file, you will notice that it contains some queries that will drop your `oauth_*` tables because they are unmapped (there is no doctrine entity describing them). +You should delete your latest migration with the DROP queries in it as we will create another one, without the DROP queries in it. +To avoid dropping these tables, you need to add a parameter called `filter-expression`. + +The command to be executed without dropping these tables looks like this: + +On Windows (use double quotes): + +```shell +vendor/bin/doctrine-migrations diff --filter-expression="/^(?!oauth_)/" +``` + +On Linux/macOS (use single quotes): + +```shell +vendor/bin/doctrine-migrations diff --filter-expression='/^(?!oauth_)/' +``` + +## Filtering multiple unmapped table patterns + +If your database contains multiple unmapped table groups, then the pattern in `filter-expression` should hold all table prefixes concatenated by pipe character (`|`). +For example, if you need to filter tables prefixed with `foo_` and `bar_`, then the command should look like this: + +On Windows: + +```shell +vendor/bin/doctrine-migrations diff --filter-expression="/^(?!foo_|bar_)/" +``` + +On Linux/macOS: + +```shell +vendor/bin/doctrine-migrations diff --filter-expression='/^(?!foo_|bar_)/' +``` + +## Troubleshooting + +On Windows, running the command in PowerShell might still add the `DROP TABLE oauth_*` queries to the migration file. +This happens because for PowerShell the caret (`^`) is a special character, so it gets dropped (`"/^(?!oauth_)/"` becomes `"/(?!oauth_)/"` when it reaches your command). +Escaping it will not help either. +In this case, we recommend running the command: + +* directly from your IDE +* using `Linux shell` +* from the `Command Prompt` + +## Help + +You can get more help with this command by running: + +```shell +vendor/bin/doctrine-migrations help diff +``` diff --git a/docs/book/v7/commands/generate-tokens.md b/docs/book/v7/commands/generate-tokens.md new file mode 100644 index 00000000..a27cc52f --- /dev/null +++ b/docs/book/v7/commands/generate-tokens.md @@ -0,0 +1,64 @@ +# Generating tokens in Dotkernel API + +This is a multipurpose command that allows creating tokens required by different parts of the API. + +## Usage + +Go to your application's root directory. + +Run the token generator command by executing the following command: + +```shell +php ./bin/cli.php token:generate +``` + +Where `` is one of the following: + +* [error-reporting](#generate-error-reporting-token) + +If you need help using the command, execute the following command: + +```shell +php ./bin/cli.php token:generate --help +``` + +### Generate error reporting token + +You can generate an error reporting token by executing the following command: + +```shell +php ./bin/cli.php token:generate error-reporting +``` + +The output should look similar to this: + +```text +Error reporting token: + + 0123456789abcdef0123456789abcdef01234567 +``` + +Copy the generated token. + +Open `config/autoload/error-handling.global.php` and paste the copied token as shown below: + +```php +return [ + ... + ErrorReportServiceInterface::class => [ + ... + 'tokens' => [ + '0123456789abcdef0123456789abcdef01234567', + ], + ... + ] +] +``` + +Save and close `config/autoload/error-handling.global.php`. + +> If your application is NOT in development mode, make sure you clear your config cache by executing: + +```shell +php ./bin/clear-config-cache.php +``` diff --git a/docs/book/v7/core-features/authentication.md b/docs/book/v7/core-features/authentication.md new file mode 100644 index 00000000..e2ecd6f3 --- /dev/null +++ b/docs/book/v7/core-features/authentication.md @@ -0,0 +1,114 @@ +# Authentication + +Authentication is the process by which an identity is presented to the application. +It ensures that the entity making the request has the proper credentials to access the API. + +**Dotkernel API** identities are delivered to the application from the client through the `Authorization` request. +If it is present, the application tries to find and assign the identity to the application. +If it is not presented, Dotkernel API assigns a default `guest` identity, represented by an instance of the class `Mezzio\Authentication\UserInterface`. + +## Configuration + +Authentication in Dotkernel API is built around the `mezzio/mezzio-authentication-oauth2` component and is already configured out of the box. +But if you want to dig more, the configuration is stored in `config/autoload/local.php` under the `authentication` key. + +> You can check the +> [mezzio/mezzio-authentication-oauth2](https://docs.mezzio.dev/mezzio-authentication-oauth2/v1/intro/#configuration) +> configuration part for more info. + +## How it works + +Dotkernel API authentication system can be used for SPAs (single-page applications), mobile applications, and simple, token-based APIs. +It allows each user of your application to generate API tokens for their accounts. + +The authentication happens through the middleware in the `Api\App\Middleware\AuthenticationMiddleware`. + +## Database + +When you install **Dotkernel API** for the first time, you need to run the migrations and seeders. +All the tables required for authentication are automatically created and populated. + +In Dotkernel API, authenticated users come from either the `admin` or the `user` table. +We choose to keep the admin table separated from the users to prevent users of the application from accessing sensitive data, which only the administrators of the application should access. + +The `oauth_clients` table is pre-populated with the default `admin` and `frontend` clients with the same password as their names (**we recommend you change the default passwords**). + +As you guessed each client serves to authenticate `admin` or `user`. + +Another table that is pre-populated is the `oauth_scopes` table, with the `api` scope. + +### Issuing API Tokens + +Token generation in Dotkernel API is done using the `password` `grant_type` scenario, which in this case allows authentication to an API using the user's credentials (generally a username and password). + +The client sends a POST request to the `/security/generate-token` with the following parameters: + +- `grant_type` = password. +- `client_id` = column `name` from the `oauth_clients` table +- `client_secret` = column `secret` from the `oauth_clients` table +- `scope` = column `scope` from the `oauth_scopes` table +- `username` = column `identity` from table `admin`/`user` +- `password` = column `password` from table `admin`/`user` + +```shell +POST /security/generate-token HTTP/1.1 +Accept: application/json +Content-Type: application/json +{ + "grant_type": "password", + "client_id": "frontend", + "client_secret": "frontend", + "scope": "api", + "username": "test@dotkernel.com", + "password": "dotkernel" +} +``` + +The server responds with a JSON as follows: + +```json +{ + "token_type": "Bearer", + "expires_in": 86400, + "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9...", + "refresh_token": "def5020087199939a49d0f2f818..." +} +``` + +Next time when you make a request to the server to an authenticated endpoint, the client should use the `Authorization` header request. + +```shell +GET /users/1 HTTP/1.1 +Accept: application/json +Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9... +``` + +### Refreshing tokens + +Dotkernel API can refresh the access token, based on the expired access token's `refresh_token`. + +The clients need to send a `POST` request to the `/security/refresh-token` with the following request: + +```shell +POST /security/refresh-token HTTP/1.1 +Accept: application/json +Content-Type: application/json +{ + "grant_type": "refresh_token", + "client_id": "frontend", + "client_secret": "frontend", + "scope": "api", + "refresh_token" : "def5020087199939a49d0f2f818..." +} +``` + +The server responds with a JSON as follows: + +```json +{ + "token_type": "Bearer", + "expires_in": 86400, + "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9...", + "refresh_token": "def5020087199939a49d0f2f818..." +} +``` diff --git a/docs/book/v7/core-features/authorization.md b/docs/book/v7/core-features/authorization.md new file mode 100644 index 00000000..c15865c0 --- /dev/null +++ b/docs/book/v7/core-features/authorization.md @@ -0,0 +1,72 @@ +# Authorization + +Authorization is the process by which a system takes a validated identity and checks if that identity has access to a given resource. + +**Dotkernel API**'s implementation of authorization uses `Mezzio\Authorization\Rbac\LaminasRbac` as a model of Role-Based Access Control (RBAC). + +## How it works + +In Dotkernel API each authenticatable entity (admin/user) comes with their `roles` table where you can define roles for each entity. +RBAC comes in to ensure that each entity has the appropriate role and permission to access a resource. + +The authorization happens through the `Api\App\Middleware\AuthorizationMiddleware` middleware. + +## Configuration + +Dotkernel API makes use of `mezzio-authorization-rbac` and includes the full configuration. + +The configuration file for the role and permission definitions is `config/autoload/authorization.global.php`. + +```php +'mezzio-authorization-rbac' => [ + 'roles' => [ + AdminRole::ROLE_SUPERUSER => [], + AdminRole::ROLE_ADMIN => [ + AdminRole::ROLE_SUPERUSER, + ], + UserRole::ROLE_GUEST => [ + UserRole::ROLE_USER, + ], + ], + 'permissions' => [ + AdminRole::ROLE_SUPERUSER => [], + AdminRole::ROLE_ADMIN => [ + 'other.routes' + 'admin.list', + 'home' + ], + UserRole::ROLE_USER => [ + 'other.routes', + 'user.my-account.update', + 'user.my-account.view', + ], + UserRole::ROLE_GUEST => [ + 'other.routes', + 'security.refresh-token', + 'error.report', + 'home', + ], + ], +], +``` + +> See [mezzio-authorization-rbac](https://docs.mezzio.dev/mezzio-authorization-rbac/v1/basic-usage/) +> for more information. + +## Usage + +Based on the configuration file above, we have two admin roles (`superuser`, `admin`) and two user roles (`user`, `guest`). + +Roles inherit the permissions from their parents: + +- `superuser` has no parent +- `admin` has `superuser` as a parent which means `superuser` also has `admin` permissions +- `user` has no parent +- `guest` has `user` as a parent which means `user` also has `guest` permissions + +For each role we defined an array of permissions. +A permission in Dotkernel API is basically a route name. + +As you can see, the `superuser` does not have its own permissions, because it gains all the permissions from `admin`, no need to define explicit permissions. + +The `user` role, gains all the permission from `guest` so no need to define that `user` can access `home` route, but `guest` cannot access user-specific routes. diff --git a/docs/book/v7/core-features/content-validation.md b/docs/book/v7/core-features/content-validation.md new file mode 100644 index 00000000..51b6f24a --- /dev/null +++ b/docs/book/v7/core-features/content-validation.md @@ -0,0 +1,97 @@ +# Content Negotiation + +> Introduced in Dotkernel API 5.0.0 + +An application performs **Content Negotiation** to: + +- match the requested format as specified by the client via the `Accept` header with a format the application can deliver. +- determine the `Content-Type` of incoming data and deserialize it so the application can use it. + +Essentially, content negotiation is the *client* telling the server what it is sending and what it wants in return, and the server determining if it can do what the client requests. + +Content negotiation validation in **Dotkernel API** happens through middleware, and it ensures that the incoming request and the outgoing response conform to the content types specified in the config file for all routes or for a specific route. +It performs validation on the `Accept` and `Content-Type` headers of the request and response. +It returns appropriate error responses when necessary. + +## Configuration + +In Dotkernel API the configuration file for content negotiation is `config/autoload/content-negotiation.global.php`. +The contents look like this: + +```php +return [ + 'content-negotiation' => [ + 'default' => [ + 'Accept' => [ + 'application/json', + 'application/hal+json', + ], + 'Content-Type' => [ + 'application/json', + 'application/hal+json', + ], + ], + 'your.route.name' => [ + 'Accept' => [], + 'Content-Type' => [], + ], + ], +]; +``` + +Excepting the `default` key, all your keys must match the route name. +For example, in Dotkernel API we have the route to list all admins, whose name is `admin.list`. +If you did not specify content negotiation for a given route, the `default` setup will be used. +The `default` key is mandatory. + +Every route configuration must come with `Accept` and `Content-Type` keys. +These keys will be used as request headers for validation. + +## Accept Negotiation + +This specifies that your server can return that format, or at least one of the formats sent by the client. + +```shell +GET /admin HTTP/1.1 +Accept: application/json +``` + +This request indicates the client wants `application/json` in return. +The server will use the config file to see if that format can be returned, basically if `application/json` is present in the `Accept` key. + +- If the format cannot be returned, a status code `406 - Not Acceptable` will be returned. +- If the format can be returned, the server should report the media type through the `Content-Type` header in the response. + +> Due to how these validations are made, the server can return a more generic media type, e.g., for a `json` media type. +> For example, if the client sends `Accept: application/vnd.api+json`, but you configured your `Accept` key as `application/json`, the format will still be returned as `json`. + +> If the `Accept` header of the request contains `*/*` it means that whatever format the server can return is OK. + +## Content-Type Negotiation + +The second aspect of content negotiation is the `Content-Type` header and to determine if the server can deserialize the data. + +```shell +POST /admin/1 HTTP/1.1 +Accept: application/json +Content-Type: application/json +{ + "foo": "bar" +} +``` + +The server will try to validate the `Content-Type` header against your configured `Content-Type` key from the config file, and if the format is not supported, a status code `415 - Unsupported Media Type` will be returned. + +For example, if you have a route that needs a file to be uploaded, normally you will configure the `Content-Type` of that route to be `multipart/form-data`. +The above request will fail because the client sends `application/json` as `Content-Type`. + +> If the request does not contain a "Content-Type" header, that means that the server will try to deserialize the data to the best of its abilities. + +## The `Request <-> Response` validation + +In addition to the validation described above, a third and last one occurs. +The server will check if the format in the `Accept` header for the request can be returned in the response. + +The way **Dotkernel API** returns a response in handler means a content type is always set. +This cannot be the case in any custom response, but the server will always check the `Content-Type` for the response and will try to validate that against the `Accept` header of the request. +If the validation fails, a status code `406 - Not Acceptable` will be returned. diff --git a/docs/book/v7/core-features/dependency-injection.md b/docs/book/v7/core-features/dependency-injection.md new file mode 100644 index 00000000..f9a29dd5 --- /dev/null +++ b/docs/book/v7/core-features/dependency-injection.md @@ -0,0 +1,57 @@ +# Dependency Injection + +Dependency injection is a design pattern used in software development to implement inversion of control. +In simpler terms, it's the act of providing dependencies for an object during instantiation. + +In PHP, dependency injection can be implemented in various ways, including through constructor injection, setter injection and property injection. + +> Introduced in Dotkernel API 5.0.0 + +Dotkernel API, through its [dot-dependency-injection](https://github.com/dotkernel/dot-dependency-injection) package, focuses only on constructor injection. + +## Usage + +**Dotkernel API** comes out of the box with the [dot-dependency-injection](https://github.com/dotkernel/dot-dependency-injection) package, which provides all we need for injecting dependencies into any object you want. + +`dot-dependency-injection` determines the dependencies by looking at the `#[Inject]` attribute, added to the constructor of a class. +Dependencies are specified as separate parameters of the `#[Inject]` attribute. + +For our example we will inject `UserService` and `config` dependencies into a `UseHandler`. + +```php +use Dot\DependencyInjection\Attribute\Inject; + +class UserHandler implements RequestHandlerInterface +{ + #[Inject( + UserService::class, + "config", + )] + public function __construct( + protected UserServiceInterface $userService, + protected array $config, + ) { + } +} +``` + +> If your class needs the value of a specific configuration key, you can specify the path using dot notation `config.example`. + +The next step is to register the class in the `ConfigProvider` under `factories` using `Dot\DependencyInjection\Factory\AttributedServiceFactory::class` + +```php +public function getDependencies(): array +{ + return [ + 'factories' => [ + UserHandler::class => AttributedServiceFactory::class + ] + ]; +} +``` + +That's it. +When your object is instantiated from the container, it will automatically have its dependencies resolved. + +> Dependency injection is available to any object within Dotkernel API. +> For example, you can inject dependencies in a service, a handler and so on, simply by registering it in the `ConfigProvider`. diff --git a/docs/book/v7/core-features/error-reporting.md b/docs/book/v7/core-features/error-reporting.md new file mode 100644 index 00000000..7bfe6d02 --- /dev/null +++ b/docs/book/v7/core-features/error-reporting.md @@ -0,0 +1,125 @@ +# Error reporting endpoint + +The error reporting endpoint was designed to allow the **frontend developers** of your API to report any bugs they encounter securely that are fully under your control. +To prevent unauthorized usage, the endpoint is protected by a token in the request's header. + +## Example case usage + +- Frontend developed in Angular. +- Frontend developer will use try-catch in the code to send **frontend errors** back to the API. + +## How to use it on the API side + +Error reporting is done by sending a **POST** request to the `/error-report` endpoint, together with a **token** in the header. +In the sections below we will detail how to configure error reporting in your API and how the endpoint is used by the frontend developers. + +### Generating a token and adding it to your API config + +First, you need to generate a token for your request. +This is done by using the command: + +```bash +php ./bin/cli.php token:generate error-reporting +``` + +The resulting token has this format `0123456789abcdef0123456789abcdef01234567`. + +> This example is not a valid token, it just lets you know what to look for. + +Copy the generated token in your `config/autoload/error-handling.global.php` file. +It should look similar to the example below. +Your API can have multiple tokens, if needed. + +```php +return [ + ... + ErrorReportServiceInterface::class => [ + ... + 'tokens' => [ + '0123456789abcdef0123456789abcdef01234567', + ], + ... + ] +] +``` + +### Validation mechanism + +Behind the scenes, the API validates your configuration and lets you know if any config items prevent the submission of the error report. +Below are the requirements for an application to be able to send error messages to Dotkernel API. + +- **Server-side requirements** stored in `config/autoload/error-handling.global.php` (these can be set/overwritten in `config/autoload/local.php`): + - All keys (`enabled`, `path`, `tokens`, `domain_whitelist` and `ip_whitelist`) must exist under `ErrorReportServiceInterface::class`. + - The error reporting feature must be enabled by setting `ErrorReportServiceInterface::class` . `enabled` to `true`. + - `ErrorReportServiceInterface::class` . `path` it must have a value; if the destination file does not exist, it will be created automatically. + - `ErrorReportServiceInterface::class` . `tokens` must contain at least one token. + - At least one of `ErrorReportServiceInterface::class` . `domain_whitelist`/`ip_whitelist` must have at least one value. + +> In `src/App/src/Service/ErrorReportService.php`, the method `checkRequest()` tries to validate the request by checking matches for `domain_whitelist` with `isMatchingDomain()` and for `ip_whitelist` with `isMatchingIpAddress()`. +> If both return `false`, a `ForbiddenException` is thrown and the error message does not get stored. + +- **Application-side requirements**: + - Send the `Error-Reporting-Token` header with a valid token previously stored in `config/autoload/error-handling.global.php` in the `ErrorReportServiceInterface::class` . `tokens` array. + - Send the `Origin` header set to the application's URL; this is the application that sends the error message. + +> The tokens under `ErrorReportServiceInterface::class`->`tokens` do not expire. +> The log file stores the token value too, making it easy to identify which application sent the error message. + +If your request passes all the checks, the message is saved in the log file specified in `ErrorReportServiceInterface::class`->`path`. + +#### Tips and tricks + +If there are multiple applications that report errors to your API, you can **assign a different error reporting token** for each. +The tokens support key-value pairs where: + +- The **key** is an alias relevant to the assigned application that uses it. +- The **value** is the token itself. + +Example: + +```php +// ... +return [ + ... + ErrorReportServiceInterface::class => [ + // ... + 'tokens' => [ + 'frontend' => '0123456789abcdef0123456789abcdef01234567', + 'admin' => '9876543210abcdef0123456789abcdef7654321', + // other tokens + ], + ], +]; +``` + +The log file will have entries similar to the below: + +> [2024-08-29 12:47:00] [0123456789abcdef0123456789abcdef01234567] Demo error message + +The inclusion of the token helps you identify the source of the error message. +In our example, it's the application that uses the `0123456789abcdef0123456789abcdef01234567` token, which is assigned to the application `frontend`. + +## How to use it on the Frontend side (Angular example) + +The API developer sends a generated token to the frontend developer who will save it in their `environment.staging.ts` and/or `environment.prod.ts`. +From then on, it's the frontend developer's job to set up an error reporting function similar to the one below. + +```javascript +postError(body: object): Promise { + return new Promise((resolve, reject) => { + return this.http.post(API_ENDPOINT + 'error-report', body , {headers: new HttpHeaders({'Error-Reporting-Token': 'TOKEN', 'Origin': 'https://example.com'})})).subscribe({ + next: (response: any) => { + resolve(response); + }, + error: (e: HttpErrorResponse) => reject(e), + complete: () => console.info('Error on sending error'), + }); + }); + } +``` + +Whenever an error is found, the frontend will call `postError()` with a relevant description under `message`. + +```javascript +apiService.postError({message: 'ERROR MESSAGE'}) +``` diff --git a/docs/book/v7/core-features/exceptions.md b/docs/book/v7/core-features/exceptions.md new file mode 100644 index 00000000..43d605d0 --- /dev/null +++ b/docs/book/v7/core-features/exceptions.md @@ -0,0 +1,127 @@ +# Exceptions + +## What are exceptions? + +Exceptions are a powerful mechanism for handling errors and other exceptional conditions that may occur during the execution of a script. +They provide a way to manage errors in a structured and controlled manner, separating error-handling code from regular code. + +## How we use exceptions + +When it comes to handling exceptions, **Dotkernel API** relies on the usage of easy-to-understand, problem-specific exceptions. +Below we will list the available custom exceptions. + +### `BadRequestException` thrown when + +* The Client tries to **create/update resource**, but the **request data is invalid/incomplete** (example: client tries to create an account, but does not send the required `identity` field) + +### `ConflictException` thrown when + +* The **resource cannot be created** because a different resource with the same identifier **already exists** (example: cannot change existing user's identity because another user with the same identity already exists) +* The **resource cannot change its state** because it is **already in the specified state** (example: user cannot be activated because it is already active) + +### `ExpiredException` thrown when + +* The **resource cannot be accessed** + * because it has **expired** (example: account activation link) + * because it has been **consumed** (example: one-time password) + +### `ForbiddenException` thrown when + +* The **resource cannot be accessed** by the authenticated client's **role** (example: client authenticated as regular user sends a `GET /admin` request) + +### `MethodNotAllowedException` thrown when + +* The client tries to interact with a resource via an **invalid HTTP request method** (example: client sends a `PATCH /avatar` request) + +### `NotFoundException` thrown when + +* The client tries to interact with a **resource that does not exist** on the server (example: client sends a `GET /resource-does-not-exist` request) + +### `UnauthorizedException` thrown when + +* The **resource cannot be accessed** because the **client is not authenticated** (example: unauthenticated client sends a `GET /admin` request) + +## How it works + +During a request, if there is no uncaught exception, **Dotkernel API** will return a JSON response with the data provided by the handler that processed the request. + +Otherwise, it will build and send a response based on the exception thrown: + +* `BadRequestException` will return a `400 Bad Request` response +* `UnauthorizedException` will return a `401 Unauthorized` response +* `ForbiddenException` will return a `403 Forbidden` response +* `OutOfBoundsException` and `NotFoundException` will return a `404 Not Found` response +* `MethodNotAllowedException` will return a `405 Method Not Allowed` response +* `ConflictException` will return a `409 Conflict` response +* `ExpiredException` will return a `410 Gone` response +* `MailException`, `RuntimeException` and the generic `Exception` will return a `500 Internal Server Error` response + +## How to extend + +In this example we will + +* Create a custom exception called `CustomException` +* Place it next to the already existing custom exceptions (you can use your preferred location) +* Return a custom HTTP status code when `CustomException` is encountered. + +### Step 1: Create exception file + +Navigate to the directory `src/App/src/Handler/Exception` and create a PHP class called `CustomException.php`. +Open `CustomException.php` and add the following content: + +```php +errorResponse($exception->getMessage(), StatusCodeInterface::STATUS_IM_A_TEAPOT); +``` + +Save and close the file. + +### Step 5: Test for success + +Access your API's home page URL, which should return the same content. +Notice that this time it returns `418 I'm a teapot` HTTP status code. diff --git a/docs/book/v7/core-features/rendering-and-sending-emails.md b/docs/book/v7/core-features/rendering-and-sending-emails.md new file mode 100644 index 00000000..da20a7fc --- /dev/null +++ b/docs/book/v7/core-features/rendering-and-sending-emails.md @@ -0,0 +1,44 @@ +# Rendering and sending emails + +In the previous versions of Dotkernel API we have been composing email bodies using **Twig** from the `mezzio/mezzio-twigrenderer` package. +In the current version of Dotkernel API, we introduced the core mail service `Core/src/App/src/Service/MailService` which is responsible for sending all emails. + +Being a core service, `MailService` is used across all projects implementing the Core architecture. +To compose and send an email, a solid implementation of `TemplateRendererInterface` was required to be injected into `MailService`, because each method rendered and parsed their respective templates in place before sending an email. +This is acceptable with other Dotkernel applications which in most cases return a rendered template, but being that Dotkernel API mostly returns JSON objects, rendered with a different renderer, **Twig** had to be replaced with a lighter solution. + +The solution is a custom [`Api\App\Template\Renderer`](https://github.com/dotkernel/api/blob/7.0/src/App/src/Template/Renderer.php) implementing [`Api\App\Template\RendererInterface`](https://github.com/dotkernel/api/blob/7.0/src/App/src/Template/RendererInterface.php). +This is a lightweight renderer, aimed at rendering a combination of **PHP** and **HTML** files with `phtml` extension. + +With the new solution, `MailService` requires no implementation of any renderer because it no longer has to render templates internally. +Instead, an implementation of `Api\App\Template\RendererInterface` is first injected in the handler: + +```php +class ExampleHandler extends AbstractHandler +{ + #[Inject( + MailService::class, + RendererInterface::class, + )] + public function __construct( + protected MailService $mailService, + protected RendererInterface $renderer, + ) { +} +``` + +Then, the handler calls the renderer and saves the rendered template in a variable: + +```php +$body = $this->renderer->render('user::welcome', ['user' => $user]); +``` + +And finally, the handler calls the mail service with the composed $body being passed as a parameter to the method which sends the email: + +```php +// $user object contains email, firstname and lastname + +$this->mailService->sendWelcomeMail($user, $body); +``` + +> Other Dotkernel applications implementing the Core architecture do the same in the handlers but keep using Twig as the template renderer. diff --git a/docs/book/v7/extended-features/core-and-app.md b/docs/book/v7/extended-features/core-and-app.md new file mode 100644 index 00000000..49131de8 --- /dev/null +++ b/docs/book/v7/extended-features/core-and-app.md @@ -0,0 +1,38 @@ +# Core and App code structure + +Since version 6.0, the project is split into two main parts: **App** and **Core**. + +When you start a new project, there are chances that the requirements are not defined well. +Because of that, your platform needs to be flexible and allow growth in the long term. + +Our purpose is to reach a **Headless CMS** architecture for easier scalability. + +> Headless CMS is a backend-only content management system that acts primarily as a content repository. +> Compared to traditional CMS platforms (e.g., WordPress) that tightly couple the front end and back end, a headless CMS decouples the content management from the presentation layer. +> The content is delivered through APIs allowing any frontend to fetch and display it, which also enables working in parallel on the backend and potentially multiple frontends. + +## What is "App" and what is "Core"? + +### Core + +The **Core** is the backbone of the application. +It contains the core business logic, the lowest-level features. + +- It handles things like: + - Authentication + - Database setup + - Middleware + +You usually don’t touch this unless you’re updating how the system works "behind the scenes." + +### App + +The **App** is where you build your actual project — the "body" of your application. + +- This is where you will: + - Define your routes + - Write your handlers + - Add your custom logic + - Error reporting + +If you're building features for the project, you're mostly working here. diff --git a/docs/book/v7/extended-features/handler-structure.md b/docs/book/v7/extended-features/handler-structure.md new file mode 100644 index 00000000..6a4b514a --- /dev/null +++ b/docs/book/v7/extended-features/handler-structure.md @@ -0,0 +1,43 @@ +# The new handler structure + +Since version 6.0, Dotkernel API contains some new architectural changes compared to its older version that uses controllers. +The goal of this update is to implement PSR-15 handlers into Dotkernel API. + +## What is a handler? + +A "handler" is the piece of code that reacts when a user makes a specific request (like visiting a webpage or submitting a form). +It's basically the "controller" that decides what happens next. + +HTTP request handlers are at the core of any web application. +They receive a request, process it and return a response. + +Controllers with several actions are fine, but handlers split the code into manageable chunks that make your life a lot easier in the long run. +This follows the first of the [SOLID](https://www.digitalocean.com/community/conceptual-articles/s-o-l-i-d-the-first-five-principles-of-object-oriented-design) principles. + +## What is a naming pattern? + +A naming pattern helps you organize and quickly identify your files by using relevant strings in file names like: + +* What a file refers to. +* The action a file performs. +* How a file relates to other files. +* The author of the file’s contents. +* The creation date or the event the file refers to. + +### The naming pattern for Dotkernel Handlers + +The naming pattern for our Handlers contains: + +* The **method** or verb used by the handler (e.g., GET, POST). +* The **resource** name (e.g., Admin, Account). +* The performed **action** (e.g., CreateForm, List). +* An optional **Form** if the handler returns a form that will perform another action when submitted. +* The string **Handler**. + +In this way, the developer can easily figure out the functionality of each handler by looking at its name. + +## Mapping of the handlers + +The full mapping of the handlers and their current paths and actions can be found in the full [naming convention table](https://docs.dotkernel.org/img/api/v7/naming-convention.png). + +[![naming-convention-thumbnail](https://docs.dotkernel.org/img/api/v7/naming-convention-thumbnail.png)](https://docs.dotkernel.org/img/api/v7/naming-convention.png) diff --git a/docs/book/v7/extended-features/injectable-input-filters.md b/docs/book/v7/extended-features/injectable-input-filters.md new file mode 100644 index 00000000..790fb568 --- /dev/null +++ b/docs/book/v7/extended-features/injectable-input-filters.md @@ -0,0 +1,72 @@ +# Injectable input filters + +In the current version of Dotkernel API has an Injectable Input Filter system into the constructors of our handlers. + +When building APIs or backend applications in PHP, especially within frameworks that support dependency injection, input validation is a critical concern. +Many developers instinctively instantiate input filters or validators inside their handlers or controllers. +However, injecting input filters is a cleaner, more testable, and flexible approach. + +The **previous** version that contained inline instantiation: + +```php +public function handle(ServerRequestInterface $request): ResponseInterface +{ + $inputFilter = (new CreateAdminInputFilter())->setData((array) $request->getParsedBody()); + if (! $inputFilter->isValid()) { + throw (new BadRequestException())->setMessages($inputFilter->getMessages()); + } + + $admin = $this->adminService->createAdmin($inputFilter->getValues()); + + return $this->createdResponse($request, $admin); +} +``` + +While simple, this ties your handler directly to a concrete class. It’s harder to reuse logic across contexts and mock or replace the filter during testing. + +Our **current** approach uses constructor injection: + +```php +class PostAdminResourceHandler extends AbstractHandler +{ + #[Inject( + AdminServiceInterface::class, + CreateAdminInputFilter::class, + )] + public function __construct( + protected AdminServiceInterface $adminService, + protected CreateAdminInputFilter $inputFilter, + ) { + } + + /** + * @throws BadRequestException + * @throws ConflictException + * @throws NotFoundException + */ + public function handle(ServerRequestInterface $request): ResponseInterface + { + $this->inputFilter->setData((array) $request->getParsedBody()); + if (! $this->inputFilter->isValid()) { + throw (new BadRequestException())->setMessages($this->inputFilter->getMessages()); + } + + $admin = $this->adminService->createAdmin((array) $this->inputFilter->getValues()); + + return $this->createdResponse($request, $admin); + } +} +``` + +This new approach makes it trivial to mock the filters during tests: + +```php +$mockFilter = $this->createMock(CreateAdminInputFilter::class); +$mockFilter->method('setData')->willReturnSelf(); +$mockFilter->method('isValid')->willReturn(true); + +$handler = new PostAdminResourceHandler($adminService, $mockFilter); +$response = $handler->handle($request); +``` + +You're no longer tied to the real filter logic in your handler tests. diff --git a/docs/book/v7/extended-features/problem-details.md b/docs/book/v7/extended-features/problem-details.md new file mode 100644 index 00000000..b41c42af --- /dev/null +++ b/docs/book/v7/extended-features/problem-details.md @@ -0,0 +1,78 @@ +# Problem details + +With the usage of `mezzio/mezzio-problem-details` we have implemented a way to help the developers understand better the errors that they are getting from their APIs based on the [RFC 9457](https://www.rfc-editor.org/rfc/rfc9457.html) standards. + +Example of a response with details: + +```json +{ + "title": "Unauthorized", + "type": "https://docs.dotkernel.org/api-documentation/v7/core-features/error-reporting/", + "status": 401, + "detail": "You are not allowed to report errors." +} +``` + +Usually the response includes: + +- A title related to the error +- The type of error +- The status of the request (e.g `404`) +- Different error messages + +More fields can be added based on the preference of the developer. + +## Our changes + +In order for us to implement this new feature, a new middleware component was required. +We have created `ProblemDetailsMiddleware` along with `ProblemDetailsNotFoundHandler` which is being called in the `config/pipeline.php` file. +Our exceptions have also been modified to be slimmed around the requirement for the `problem-details` package. + +Example from `src/App/src/Exception/BadRequestException.php`: + +```php +public static function create(string $detail, string $type = '', string $title = '', array $additional = []): self + { + $exception = new self(); + + $exception->type = $type; + $exception->detail = $detail; + $exception->status = StatusCodeInterface::STATUS_BAD_REQUEST; + $exception->title = $title; + $exception->additional = $additional; + + return $exception; + } +``` + +An example configuration file for setting custom links has also been created in `config/autoload/problem-details.global.php`. +Here the statuses of the API calls are being attributed to a link. + +```php +return [ + 'problem-details' => [ + 'default_types_map' => [ + StatusCodeInterface::STATUS_BAD_REQUEST + => 'https://datatracker.ietf.org/doc/html/rfc9110#name-400-bad-request', + StatusCodeInterface::STATUS_UNAUTHORIZED + => 'https://datatracker.ietf.org/doc/html/rfc9110#name-401-unauthorized', + StatusCodeInterface::STATUS_FORBIDDEN + => 'https://datatracker.ietf.org/doc/html/rfc9110#name-403-forbidden', + StatusCodeInterface::STATUS_NOT_FOUND + => 'https://datatracker.ietf.org/doc/html/rfc9110#name-404-not-found', + StatusCodeInterface::STATUS_METHOD_NOT_ALLOWED + => 'https://datatracker.ietf.org/doc/html/rfc9110#name-405-method-not-allowed', + StatusCodeInterface::STATUS_NOT_ACCEPTABLE + => 'https://datatracker.ietf.org/doc/html/rfc9110#name-406-not-acceptable', + StatusCodeInterface::STATUS_CONFLICT + => 'https://datatracker.ietf.org/doc/html/rfc9110#name-409-conflict', + StatusCodeInterface::STATUS_GONE + => 'https://datatracker.ietf.org/doc/html/rfc9110#name-410-gone', + StatusCodeInterface::STATUS_UNSUPPORTED_MEDIA_TYPE + => 'https://datatracker.ietf.org/doc/html/rfc9110#name-415-unsupported-media-type', + StatusCodeInterface::STATUS_INTERNAL_SERVER_ERROR + => 'https://datatracker.ietf.org/doc/html/rfc9110#name-500-internal-server-error', + ], + ], +]; +``` diff --git a/docs/book/v7/extended-features/route-grouping.md b/docs/book/v7/extended-features/route-grouping.md new file mode 100644 index 00000000..68a74040 --- /dev/null +++ b/docs/book/v7/extended-features/route-grouping.md @@ -0,0 +1,29 @@ +# Route grouping + +In Dotkernel API with the help of the new [dot-router](https://docs.dotkernel.org/dot-router/v1/overview/) package, we have managed to implement a nicer way of creating routes. +A lot of the times developers need to create sets of routes that have a similar format. As an example: + +```php +$app->post('/product/create', CreateProductHandler::class, 'product:create'); +$app->delete('/product/delete/{id}', DeleteProductHandler::class, 'product:delete'); +$app->patch('/product/update/{id}', UpdateProductHandler::class, 'product:update'); +$app->get('/product/view/{id}', GetProductHandler::class, 'product:view'); +``` + +Along with the features from `mezzio/mezzio-fastroute`, the new `dot-router` package provides the ability to create route groups which are collections of routes that have the same base string for the path. + +Here we have an example from `src/User/src/RoutesDelegator.php` with the new grouping method: + +```php +$routeCollector->group('/user/' . $uuid) + ->delete('', DeleteUserResourceHandler::class, 'user::delete-user') + ->get('', GetUserResourceHandler::class, 'user::view-user') + ->patch('', PatchUserResourceHandler::class, 'user::update-user'); +``` + +The advantages of this new implementation: + +- **DRY**: no need for repeating common route parts +- **encapsulation**: similar routes are grouped in a single block of code (versus each route a separate statement) +- **easy path refactoring**: modify all routes at once by changing only the prefix +- **easy copying/moving**: copying/moving an entire group makes sure that you don't accidentally omit a route diff --git a/docs/book/v7/flow/default-library-flow.md b/docs/book/v7/flow/default-library-flow.md new file mode 100644 index 00000000..c91750e4 --- /dev/null +++ b/docs/book/v7/flow/default-library-flow.md @@ -0,0 +1,5 @@ +# Default Library Flow + +The graph below demonstrates a default flow between Dotkernel's libraries. + +![Dotkernel API Default Library Flow!](https://docs.dotkernel.org/img/api/v7/dotkernel-library-flow.png) diff --git a/docs/book/v7/flow/library-flow-for-email.md b/docs/book/v7/flow/library-flow-for-email.md new file mode 100644 index 00000000..ba2a3570 --- /dev/null +++ b/docs/book/v7/flow/library-flow-for-email.md @@ -0,0 +1,5 @@ +# Library Flow for Email + +The graph below demonstrates the simplified flow between Dotkernel's libraries for sending an email. + +![Dotkernel API Default Library Flow!](https://docs.dotkernel.org/img/api/v7/dotkernel-library-flow-email.png) diff --git a/docs/book/v7/flow/middleware-flow.md b/docs/book/v7/flow/middleware-flow.md new file mode 100644 index 00000000..83396c07 --- /dev/null +++ b/docs/book/v7/flow/middleware-flow.md @@ -0,0 +1,5 @@ +# Middleware flow + +The graph below demonstrates a default flow between Dotkernel's middlewares. + +![Dotkernel API Middleware Flow!](https://docs.dotkernel.org/img/api/v7/dotkernel-middleware-flow.png) diff --git a/docs/book/v7/installation/composer.md b/docs/book/v7/installation/composer.md new file mode 100644 index 00000000..16e2a2f9 --- /dev/null +++ b/docs/book/v7/installation/composer.md @@ -0,0 +1,72 @@ +# Composer Installation of Packages + +Composer is required to install Dotkernel `api`. You can install Composer from the [official site](https://getcomposer.org/). + +> First, make sure that you have navigated your command prompt to the folder where you copied the files in the previous step. + +## Install dependencies + +Run this command in the command prompt. + +> Use the **CLI** to ensure interactivity for proper configuration. + +```shell +composer install +``` + +You should see this text below, along with a long list of packages to be installed instead of the `[...]`. +In this example there are 164 packages, though the number can change in future updates. +You will find the packages in the `vendor` folder. + +```shell +No composer.lock file present. Updating dependencies to latest instead of installing from lock file. See https://getcomposer.org/install for more information. +Loading composer repositories with package information +Updating dependencies +Lock file operations: 164 installs, 0 updates, 0 removals +[...] +Writing lock file +Installing dependencies from lock file (including require-dev) +Package operations: 164 installs, 0 updates, 0 removals +[...] +``` + +The setup script may prompt for some configuration settings, for example, the lines below. +If you don't see them, you can skip to the next section. + +```shell +Please select which config file you wish to inject 'Laminas\Diactoros\ConfigProvider' into: + [0] Do not inject + [1] config/config.php + Make your selection (default is 1): +``` + +Type `0` to select `[0] Do not inject`. + +> We choose `0` because Dotkernel includes its own ConfigProvider, which already contains the prompted configurations. +> If you choose `[1] config/config.php`, an extra `ConfigProvider` will be injected. + +The next question is: + +`Remember this option for other packages of the same type? (y/N)` + +Type `y` here, and hit `enter` to complete this stage. + +## Development mode + +If you're installing the project for development, make sure you have development mode enabled by running: + +```shell +composer development-enable +``` + +You can disable the development mode by running: + +```shell +composer development-disable +``` + +You can check if you have development mode enabled by running: + +```shell +composer development-status +``` diff --git a/docs/book/v7/installation/configuration-files.md b/docs/book/v7/installation/configuration-files.md new file mode 100644 index 00000000..f3d5e7db --- /dev/null +++ b/docs/book/v7/installation/configuration-files.md @@ -0,0 +1,17 @@ +# Configuration Files + +## Prepare config files + +* duplicate `config/autoload/cors.local.php.dist` as `config/autoload/cors.local.php` + +> If your API is consumed by another application, make sure to configure the `allowed_origins` variable. + +* duplicate `config/autoload/local.php.dist` as `config/autoload/local.php` + +* duplicate `config/autoload/mail.local.php.dist` as `config/autoload/mail.local.php` + +> If your API sends emails, make sure to fill in SMTP connection params + +* **optional**: to run/create tests, duplicate `config/autoload/local.test.php.dist` as `config/autoload/local.test.php` + +> This creates a new in-memory database that your tests will run on. diff --git a/docs/book/v7/installation/doctrine-orm.md b/docs/book/v7/installation/doctrine-orm.md new file mode 100644 index 00000000..3042587a --- /dev/null +++ b/docs/book/v7/installation/doctrine-orm.md @@ -0,0 +1,168 @@ +# Doctrine ORM + +This step saves the database connection credentials in an API configuration file. +We do not cover the creation steps of the database itself. + +## Setup database + +Create a new **MariaDB**/**PostgreSQL** database and set its collation to `utf8mb4_general_ci`. + +Make sure you fill out the database credentials in `config/autoload/local.php` under `$databases['mariadb']` or `$databases['postgresql']`. +Below is the item you need to focus on: + +```php +$databases = [ + 'mariadb' => [ + 'host' => 'localhost', + 'dbname' => 'dotkernel', + 'user' => '', + 'password' => '', + 'port' => 3306, + 'driver' => 'pdo_mysql', + 'collation' => 'utf8mb4_general_ci', + 'table_prefix' => '', + ], + 'postgresql' => [ + 'host' => 'localhost', + 'dbname' => 'dotkernel', + 'user' => '', + 'password' => '', + 'port' => 5432, + 'driver' => 'pdo_pgsql', + 'collation' => 'utf8mb4_general_ci', + 'table_prefix' => '', + ], +]; +``` + +`my_database`, `my_user`, `my_password` are provided only as an example. + +> You can add more database connections to this array. +> Only one active connection is allowed at a time. +> By default, the application uses the 'mariadb' connection. +> You can switch to another connection by activating it under `doctrine` -> `connection` -> `orm_default` -> `params`. + +### Creating migrations + +Create a database migration by executing the following command: + +```shell +php ./vendor/bin/doctrine-migrations diff +``` + +The new migration file will be placed in `src/Core/src/App/src/Migration/`. + +### Running migrations + +Run the database migrations by executing the following command: + +```shell +php ./vendor/bin/doctrine-migrations migrate +``` + +> If you have already run the migrations, you may get the below message: + +```text +WARNING! You have x previously executed migrations in the database that are not registered migrations. + {migration list} +Are you sure you wish to continue? (y/n) +``` + +> In this case, you should double-check to make sure the new migrations are ok to run. + +When using an empty database, you will get this confirmation message: + +```text +WARNING! You are about to execute a migration in database "" that could result in schema changes and data loss. Are you sure you wish to continue? (yes/no) +``` + +Hit `Enter` to confirm the operation. +This will run all the migrations in chronological order. +Each migration will be logged in the `migrations` table to prevent running the same migration more than once, which is often not desirable. + +If everything ran correctly, you will get this confirmation. + +```text +[OK] Successfully migrated to version: Core\App\Migration\VersionYYYYMMDDHHMMSS +``` + +### Executing fixtures + +**Fixtures are used to seed the database with initial values and should be executed after migrating the database.** + +To list all the fixtures, run: + +```shell +php ./bin/doctrine fixtures:list +``` + +This will output all the fixtures in the order of execution. + +To execute all fixtures, run: + +```shell +php ./bin/doctrine fixtures:execute +``` + +To execute a specific fixture, run: + +```shell +php ./bin/doctrine fixtures:execute --class=FixtureClassName +``` + +More details on how fixtures work can be found on [dot-data-fixtures documentation](https://github.com/dotkernel/dot-data-fixtures#creating-fixtures) + +### Prefixing table names + +Note in the database configuration array the key called `table_prefix`. +By default, it is an empty string, which means that all the tables will be named exactly the way they are configured in the entities. + +```text +├─ admin +├─ admin_login +├─ admin_role +├─ admin_roles +├─ doctrine_migration_versions +├─ oauth_access_tokens +├─ oauth_access_token_scopes +├─ oauth_auth_codes +├─ oauth_auth_code_scopes +├─ oauth_clients +├─ oauth_refresh_tokens +├─ oauth_scopes +├─ settings +├─ user +├─ user_avatar +├─ user_detail +├─ user_reset_password +├─ user_role +└─ user_roles +``` + +Adding a prefix, for example `dot_`, all the table will be composed of the prefix and the original table name. + +```text +├─ dot_admin +├─ dot_admin_login +├─ dot_admin_role +├─ dot_admin_roles +├─ doctrine_migration_versions +├─ dot_oauth_access_tokens +├─ dot_oauth_access_token_scopes +├─ dot_oauth_auth_codes +├─ dot_oauth_auth_code_scopes +├─ dot_oauth_clients +├─ dot_oauth_refresh_tokens +├─ dot_oauth_scopes +├─ dot_settings +├─ dot_user +├─ dot_user_avatar +├─ dot_user_detail +├─ dot_user_reset_password +├─ dot_user_role +└─ dot_user_roles +``` + +> The configured prefix is prepended as is, no intermediary character will be added. + +> `doctrine_migration_versions` is an exception, being a special table handled by Doctrine Migrations. diff --git a/docs/book/v7/installation/faq.md b/docs/book/v7/installation/faq.md new file mode 100644 index 00000000..71a57722 --- /dev/null +++ b/docs/book/v7/installation/faq.md @@ -0,0 +1,39 @@ +# Frequently Asked Questions + +## How do I fix common permission issues? + +If running your project, you encounter some permission issues, follow the below steps. + +### Errors + +> PHP Fatal error: Uncaught InvalidArgumentException: The directory "/var/www/_example.local_/html/data" is not writable... + +> PHP Fatal error: Uncaught InvalidArgumentException: The directory "/var/www/_example.local_/html/data/cache" is not writable... + +> PHP Fatal error: Uncaught InvalidArgumentException: The directory "/var/www/_example.local_/html/data/cache/doctrine" is not writable... + +**Fix:** + +```shell +chmod -R 777 data +``` + +### Error + +> PHP Fatal error: Uncaught InvalidArgumentException: The directory "/var/www/_example.local_/html/public/uploads" is not writable... + +**Fix:** + +```shell +chmod -R 777 public/uploads +``` + +### Error + +> PHP Fatal error: Uncaught ErrorException: fopen(/var/www/_example.local_/config/autoload/../../log/error-log-_yyyy-mm-dd.log_): Failed to open stream: Permission denied... + +**Fix:** + +```shell +chmod -R 777 log +``` diff --git a/docs/book/v7/installation/getting-started.md b/docs/book/v7/installation/getting-started.md new file mode 100644 index 00000000..cb925f15 --- /dev/null +++ b/docs/book/v7/installation/getting-started.md @@ -0,0 +1,13 @@ +# Clone the project + +## Recommended development environment + +> If you are using Windows as an OS on your machine, you can use WSL2 as a development environment. +> Read more here: [PHP-Mariadb-on-WLS2](https://www.dotkernel.com/php-development/almalinux-9-in-wsl2-install-php-apache-mariadb-composer-phpmyadmin/) + +Using your terminal, navigate inside the directory you want to download the project files into. +Make sure that the directory is empty before proceeding to the download process. Once there, run the following command: + +```shell +git clone https://github.com/dotkernel/api.git . +``` diff --git a/docs/book/v7/installation/test-the-installation.md b/docs/book/v7/installation/test-the-installation.md new file mode 100644 index 00000000..f49732c9 --- /dev/null +++ b/docs/book/v7/installation/test-the-installation.md @@ -0,0 +1,33 @@ +# Test the installation + +Sending a GET request to the [home page](http://0.0.0.0:8080/) should output the following message: + +```json +{"message": "Dotkernel API version 7"} +``` + +## Old way of doing things, using PHP built-in server + +```shell +php -S 0.0.0.0:8080 -t public +``` + +## Running tests + +The project has two types of tests: functional and unit tests, you can run both types at the same type by executing this command: + +```shell +php vendor/bin/phpunit +``` + +## Running unit tests + +```shell +vendor/bin/phpunit --testsuite=UnitTests --testdox --colors=always +``` + +## Running functional tests + +```shell +vendor/bin/phpunit --testsuite=FunctionalTests --testdox --colors=always +``` diff --git a/docs/book/v7/introduction/file-structure.md b/docs/book/v7/introduction/file-structure.md new file mode 100644 index 00000000..5df25162 --- /dev/null +++ b/docs/book/v7/introduction/file-structure.md @@ -0,0 +1,116 @@ +# File structure + +Dotkernel API follows the [PSR-4](https://www.php-fig.org/psr/psr-4/) standards. + +It is a good practice to standardize the file structure of projects. + +When using Dotkernel API, the following structure is installed by default: + +![Dotkernel API File Structure!](https://docs.dotkernel.org/img/api/v7/file-structure-dk-api.png) + +## Special purpose folders + +* `.github` - Contains GitHub workflow files +* `.laminas-ci` - Contains laminas-ci workflow files + +## `bin` folder + +This folder contains: + +* `clear-config-cache.php` - Removes the config cache file `data/cache/config-cache.php`; available only when development mode is enabled +* `cli.php` - Used to build console applications based on [laminas-cli](https://github.com/laminas/laminas-cli) +* `doctrine` - Used by the doctrine fixtures to populate the database tables + +## `config` folder + +This folder contains all application-related config files: + +* `cli-config.php` - Command line interface configuration used by migrations, fixtures, cron jobs +* `config.php` - Registers ConfigProviders for installing packages +* `container.php` - Main service container that provides access to all registered services +* `development.config.php.dist` - Activates debug mode; gets symlinked as `development.config.php` when enabling development mode +* `migrations.php` - Configuration for database migration, like migration file location and table to save the migration log +* `pipeline.php` - Contains a list of middlewares, in the order of their execution + +### `config/autoload` folder + +This folder contains all service-related local and global config files: + +* `authorization.global.php` - Configures access per route for user roles +* `cli.global.php` - Configures cli +* `content-negotiation.global.php` - Configures request and response formats +* `cors.local.php.dist` - Configures Cross-Origin Resource Sharing, like call origin, headers, cookies +* `dependencies.global.php` - Sets global dependencies that should be accessible by all modules +* `development.local.php.dist` - Gets symlinked as `development.local.php` when enabling development mode; activates error handlers +* `doctrine.global.php` - Configuration used by Object–relational mapping +* `error-handling.global.php` - Configures and activates error logs +* `local.php.dist` - Local configuration file where you can overwrite application name and URL +* `local.test.php.dist` - Local configuration for functional tests +* `mail.local.php.dist` - Mail configuration; e.g. sendmail vs smtp, message configuration, mail logging +* `mezzio.global.php` - Mezzio core config file +* `mezzio-tooling-factories.global.php` Add or remove factory definitions +* `response-header.global.php` - Defines headers per route +* `templates.global.php` - `dotkernel/dot-twigrenderer` config file + +## `data` folder + +This folder is a storage for project data files and service caches. +It contains these folders: + +* `cache` - Cache for e.g. Twig files +* `doctrine` - Database migrations and fixtures +* `oauth` - Encryption, private and public keys needed for authentication +* `lock` - Contains lock files generated by [`dotkernel/dot-cli`](https://docs.dotkernel.org/dot-cli/v3/lock-files/) + +> AVOID storing sensitive data on the repository! + +## `log` folder + +This folder stores daily log files. +When you access the application from the browser, (if not already created) a new log file gets created in the format specified in the `config/autoload/error-handling.global.php` config file under the `stream` array key. + +## `public` folder + +This folder contains all publicly available assets and serves as the entry point of the application: + +* `uploads` - Normally contains files uploaded via the application +* `.htaccess` - Server configuration file used by Apache web server; it enables the URL rewrite functionality +* `index.php` - The application's main entry point +* `robots.txt.dist` - A sample robots.txt file that allows/denies bot access to certain areas of your application; activate it by duplicating the file as `robots.txt` and comment out the lines that don't match your environment + +## `src` folder + +This folder contains a separate folder for each Module. + +These are the modules included by default: + +* `Admin` - Contains functionality for managing users with `admin` role; note these are users save in the `admin` database table +* `App` - Contains functionality such as error reporting +* `Core` - Contains core functionality, from authentication, to rendering +* `Security` - Contains security-related functionality +* `User` - Contains functionality for managing regular users + +### Module contents + +Each Module folder, in turn, should contain the following folders, unless they are empty: + +* `src/Handler` - Action classes (similar to Controllers but can only perform one action) +* `src/Entity` - Used by database entities +* `src/Service` - Service classes +* `src/Repository` - Entity repository folder + +The above example is just some of the folders a project may include, but they should give you an idea about the recommended structure. +Other classes the `src` folder may include are `InputFilter`, `EventListener`, `Helper`, `Command`, `Factory` etc. + +The `src` folder in each Module folder normally also contains these files: + +* `ConfigProvider.php` - Configuration data for the module +* `OpenAPI.php` - Detailed descriptions for each endpoint in the OpenAPI format +* `RoutesDelegator.php` - Module specific route registrations + +### `templates` folder in Modules + +This folder contains the template files, used, for example, to help render e-mail templates. + +> `twig` is used as Templating Engine. +> All template files have the extension `.html.twig` diff --git a/docs/book/v7/introduction/introduction.md b/docs/book/v7/introduction/introduction.md new file mode 100644 index 00000000..8aa3154a --- /dev/null +++ b/docs/book/v7/introduction/introduction.md @@ -0,0 +1,90 @@ +# Introduction + +Below is a quick overview of features in Dotkernel API. + +## Doctrine 3 ORM + +For the persistence in a relational database management system we chose Doctrine ORM (object-relational mapper). + +The benefit of Doctrine for the programmer is the ability to focus on the object-oriented business logic and worry about persistence only as a secondary priority. + +## Documentation + +### OpenAPI using Swagger UI + +See the [OpenAPI specification](../openapi/introduction.md) for more information. + +### Postman + +The following files store information about every available endpoint ready to be tested: + +* `documentation/Dotkernel_API.postman_collection.json` +* `documentation/Dotkernel_API.postman_environment.json` + +## Hypertext Application Language + +For our API payloads (a value object for describing the API resource, its relational links and any embedded/child resources related to it) we use [mezzio/mezzio-hal](https://github.com/mezzio/mezzio-hal). + +## CORS + +By using `MezzioCorsMiddlewareCorsMiddleware`, the CORS preflight will be recognized and the middleware will start to detect the proper CORS configuration. +The Router is used to detect every allowed request method by executing a route match with all possible request methods. +Therefore, for every preflight request, there is at least one Router request. + +## OAuth 2.0 + +OAuth 2.0 is an authorization framework that enables applications to get limited access to user accounts on your Dotkernel API. +We use [mezzio/mezzio-authentication-oauth2](https://github.com/mezzio/mezzio-authentication-oauth2), which provides OAuth 2.0 authentication for Mezzio and PSR-15 applications by using the [thephpleague/oauth2-server](https://github.com/thephpleague/oauth2-server) package. + +## Email + +It is not unlikely for an API to send emails depending on the use case. +Here is another area where Dotkernel API shines. +Using `DotMailServiceMailService` provided by [dotkernel/dot-mail](https://github.com/dotkernel/dot-mail) you can send custom email templates. + +## Configuration + +From authorization at request route level to API keys for your application, you can find every configuration variable in the `config` directory. + +Registering a new module can be done by including its `ConfigProvider.php` in `config.php`. + +Brand new middlewares should go into `pipeline.php`. Here you can edit the order in which they run and find more info about the currently included ones. + +You can further customize your api within the `autoload` directory that holds configuration files for each category. + +## Routing + +Each module has a `RoutesDelegator.php` file for managing existing routes inside that specific module. +It also allows a quick way of adding new routes by providing the route path, Middlewares that the route will use and the route name. + +You can allocate permissions per route name to restrict access for a user role to a specific route in `config/autoload/authorization.global.php`. + +## Commands + +For registering new commands first make sure your command class extends `Symfony\Component\Console\Command\Command`. +Then you can enable it by registering it in `config/autoload/cli.global.php`. + +## File locker + +Here you will also find our file locker configuration, so you can enable and disable it (by default: `'enabled' => true`). + +Note: The File Locker System will create a `command-{command-default-name}.lock` file which will not let another instance of the same command to run until the previous one has finished. + +## Tests + +One of the best ways to ensure the quality of your product is to create and run functional and unit tests. +You can find factory-made tests in the `test` folder, and you can also register your own. + +We have two types of tests: functional and unit tests. +You can run both types at the same type by executing this command: + +```shell +php vendor/bin/phpunit +``` + +Alternatively, you can run each test category separately with these commands: + +```shell +vendor/bin/phpunit --testsuite=UnitTests --testdox --colors=always +vendor/bin/phpunit --testsuite=FunctionalTests --testdox --colors=always +``` diff --git a/docs/book/v7/introduction/packages.md b/docs/book/v7/introduction/packages.md new file mode 100644 index 00000000..07a50989 --- /dev/null +++ b/docs/book/v7/introduction/packages.md @@ -0,0 +1,33 @@ +# Packages + +* `doctrine/dbal`:`^4.2` - Powerful PHP database abstraction layer (DBAL) with many features for database schema introspection and management. +* `doctrine/orm`:`^3.4` - Object-Relational-Mapper for PHP +* `dotkernel/dot-cache`:`^4.3` - Cache component extending symfony-cache +* `dotkernel/dot-cli`:`^3.9` - Component for creating console applications based on laminas-cli +* `dotkernel/dot-data-fixtures`:`^1.4` - Provides a CLI interface for listing & executing doctrine data fixtures +* `dotkernel/dot-dependency-injection`:`^1.2` - Dependency injection component using class attributes. +* `dotkernel/dot-errorhandler`:`^4.0` - Logging Error Handler for Middleware Applications +* `dotkernel/dot-mail`:`^5.3` - Mail component based on Symfony Mailer +* `dotkernel/dot-response-header`:`^3.5` - Middleware for setting custom response headers. +* `dotkernel/dot-router`:`^1.0` - Dotkernel component to build complex routes, based on `mezzio/mezzio-fastroute` +* `laminas/laminas-authentication`:`^2.18` - API for authentication and includes concrete authentication adapters for common use case scenarios +* `laminas/laminas-component-installer`:`^3.5` - Composer plugin for injecting modules and configuration providers into application configuration +* `laminas/laminas-config-aggregator`:`^1.18` - Lightweight library for collecting and merging configuration from different sources +* `laminas/laminas-hydrator`:`^4.16` - Serialize objects to arrays, and vice versa +* `laminas/laminas-inputfilter`:`^2.31` - Normalize and validate input sets from the web, APIs, the CLI, and more, including files +* `laminas/laminas-servicemanager`:`^3.23` - Factory-Driven Dependency Injection Container +* `laminas/laminas-stdlib`:`^3.20` - SPL extensions, array utilities, error handlers, and more +* `mezzio/mezzio`:`^3.20` - PSR-15 Middleware Microframework +* `mezzio/mezzio-authentication-oauth2`:`^2.11` - OAuth2 (server) authentication middleware for Mezzio and PSR-15 applications +* `mezzio/mezzio-authorization-acl`:`^1.11` - laminas-permissions-acl adapter for mezzio-authorization +* `mezzio/mezzio-authorization-rbac`:`^1.8` - mezzio authorization rbac adapter for laminas/laminas-permissions-rbac +* `mezzio/mezzio-cors`:`^1.13` - CORS component for Mezzio and other PSR-15 middleware runners +* `mezzio/mezzio-fastroute`:`^3.12` - FastRoute integration for Mezzio +* `mezzio/mezzio-hal`:`^2.10` - Hypertext Application Language implementation for PHP and PSR-15 +* `mezzio/mezzio-helpers`:`^5.18` - Helper/Utility classes for Mezzio +* `mezzio/mezzio-problem-details`:`^1.15` - Problem Details for PSR-15 HTTP APIs addressing the RFC 7807 standard +* `ramsey/uuid`:`^4.5` - A PHP library for generating and working with universally unique identifiers (UUIDs). +* `ramsey/uuid-doctrine`:`^2.1` - Use ramsey/uuid as a Doctrine field type +* `roave/psr-container-doctrine`:`^5.2` || `^6.0` - Doctrine Factories for PSR-11 Containers +* `symfony/filesystem`:`^7.2` - Provides basic utilities for the filesystem +* `zircote/swagger-php`:`^5.0` - Generate interactive documentation for your RESTful API using PHP attributes (preferred) or PHPDoc annotations diff --git a/docs/book/v7/introduction/psr.md b/docs/book/v7/introduction/psr.md new file mode 100644 index 00000000..8fdf8db0 --- /dev/null +++ b/docs/book/v7/introduction/psr.md @@ -0,0 +1,38 @@ +# PSRs + +Some of the PSRs on this list are at the core of Dotkernel API, but several others are installed with the third party packages used in the application. +Below is the full list of PSRs present in Dotkernel API and their purpose. + +* PSR-3: [Logger Interface](https://www.php-fig.org/psr/psr-3/) + * Interface for logging libraries + * Interfaces implemented in [php-fig/log](https://github.com/php-fig/log) +* PSR-4: [Autoloader](https://www.php-fig.org/psr/psr-4/) + * Autoloading classes from file paths + * Interfaces implemented in [laminas/laminas-loader](https://github.com/laminas/laminas-loader) +* PSR-6: [Caching Interface](https://www.php-fig.org/psr/psr-6/) + * Interface for caching systems to improve the performance of any project + * Interfaces implemented in [php-fig/cache](https://github.com/php-fig/cache) +* PSR-7: [HTTP message interfaces](https://www.php-fig.org/psr/psr-7/) + * Interfaces for representing HTTP messages and URIs for use with HTTP messages + * Interfaces implemented in [php-fig/http-message](https://github.com/php-fig/http-message) +* PSR-11: [Container interface](https://www.php-fig.org/psr/psr-11/) + * Interface for dependency injection containers + * Interfaces implemented in [php-fig/container](https://github.com/php-fig/container) +* PSR-13: [Link definition interfaces](https://www.php-fig.org/psr/psr-13/) + * Way of representing a hypermedia link independently of the serialization format + * Interfaces implemented in [php-fig/link](https://github.com/php-fig/link) +* PSR-14: [Event Dispatcher](https://www.php-fig.org/psr/psr-14/) + * Mechanism for event-based extension and collaboration + * Interfaces implemented in [php-fig/event-dispatcher](https://github.com/php-fig/event-dispatcher) +* PSR-15: [HTTP Server Request Handlers](https://www.php-fig.org/psr/psr-15/) + * Interfaces for HTTP server request handlers and HTTP server middleware components that use HTTP messages + * Interfaces implemented in [php-fig/http-server-handler](https://github.com/php-fig/http-server-handler) and [php-fig/http-server-middleware](https://github.com/php-fig/http-server-middleware) +* PSR-17: [HTTP Factories](https://www.php-fig.org/psr/psr-17/) + * Standard for factories that create PSR-7 compliant HTTP objects + * Interfaces implemented in [php-fig/http-factory](https://github.com/php-fig/http-factory) +* PSR-18: [HTTP Client](https://www.php-fig.org/psr/psr-18/) + * Interface for sending HTTP requests and receiving HTTP responses + * Interfaces implemented in [php-fig/http-client](https://github.com/php-fig/http-client) +* PSR-20: [Clock](https://www.php-fig.org/psr/psr-20/) + * Interface for reading the system clock + * Interfaces implemented in [php-fig/clock](https://github.com/php-fig/clock) diff --git a/docs/book/v7/introduction/server-requirements.md b/docs/book/v7/introduction/server-requirements.md new file mode 100644 index 00000000..00c69e44 --- /dev/null +++ b/docs/book/v7/introduction/server-requirements.md @@ -0,0 +1,46 @@ +# Server Requirements + +For production, we highly recommend a *nix-based system. + +## Webserver + +### Apache >= 2.2 + +* mod_rewrite +* .htaccess support `(AllowOverride All)` + +> The repository includes a default `.htaccess` file in the `public` folder. + +### Nginx + +You need to convert the provided Apache related `.htaccess` file into Nginx configuration instructions. + +## PHP >= 8.2 + +Both mod_php and FCGI (FPM) are supported. + +## Required Settings and Modules & Extensions + +* memory_limit >= 128M +* upload_max_filesize and post_max_size >= 100 M (depending on your data) +* mbstring +* CLI SAPI (for Cron Jobs) +* Composer (added to $PATH) + +## RDBMS + +* Tested with MariaDB 10.6, 10.11 LTS, 11.4 LTS, and 11.8 LTS +* Tested with PostgreSQL 13 and above + +> MySQL is not supported because of missing UUID support. + +## Recommended extensions + +* `opcache` +* `pdo_mysql`, `pdo_pgsql` or `mysqli` (if using MariaDB or PostgreSQL as RDBMS) +* `dom` - if working with markup files structure (HTML, XML, etc.) +* `simplexml` - working with XML files +* `gd`, `exif` - if working with images +* `zlib`, `zip`, `bz2` - if compressing files +* `curl` (required if APIs are used) +* `sqlite3` - for tests diff --git a/docs/book/v7/openapi/generate-documentation.md b/docs/book/v7/openapi/generate-documentation.md new file mode 100644 index 00000000..c261df56 --- /dev/null +++ b/docs/book/v7/openapi/generate-documentation.md @@ -0,0 +1,53 @@ +# Generating the documentation file + +> Make sure that in `src/App/src/OpenAPI.php`, on the line with `#[OA\Server` the value of `url` is set to the of URL of your instance of **Dotkernel API**. + +Using your terminal, move to the root directory of your project. + +Dotkernel API stores the OpenAPI attributes in the `src` directory, so that's the path we will use for generating the static documentation file. + +## Methods of generating a documentation file + +### Without saving it to a file + +```shell +./vendor/bin/openapi ./src +``` + +This will output the generated content to the terminal. + +### Place it in a custom location + +```shell +./vendor/bin/openapi ./src --output public/openapi.yaml +``` + +This will place the generated file `openapi.yaml` in the `public` directory. + +### Specify OpenAPI version + +Supported OpenAPI versions are `3.0.0` and `3.1.0`, `3.0.0` being the default version. + +The below command will specify both the output location and the OpenAPI version: + +```shell +./vendor/bin/openapi ./src --version 3.1.0 +``` + +### Specify an output file format + +Supported file formats are `yaml` and `json`, `yaml` being the default format. + +The below command will specify the output location and `zircote/swagger-php` will determine the file format: + +```shell +./vendor/bin/openapi ./src --output public/openapi.json +``` + +Or be specific about the format by appending the `--format` argument: + +```shell +./vendor/bin/openapi ./src --output public/openapi.json --format json +``` + +These will place the generated file `openapi.json` in the `public` directory. diff --git a/docs/book/v7/openapi/getting-help.md b/docs/book/v7/openapi/getting-help.md new file mode 100644 index 00000000..27c1ae37 --- /dev/null +++ b/docs/book/v7/openapi/getting-help.md @@ -0,0 +1,9 @@ +# Getting help + +- consult the OpenAPI [specs](https://spec.openapis.org/oas/latest.html) for a complete reference of the presented objects +- see more examples of OpenAPI object representations in `zircote/swagger-php`'s [GitHub repository](https://zircote.github.io/swagger-php/guide/examples.html) +- consult `zircote/swagger-php`'s [online documentation](http://zircote.github.io/swagger-php/guide/generating-openapi-documents.html) or run the following command to see their help page: + +```shell +./vendor/bin/openapi --help +``` diff --git a/docs/book/v7/openapi/initialized-components.md b/docs/book/v7/openapi/initialized-components.md new file mode 100644 index 00000000..638aa668 --- /dev/null +++ b/docs/book/v7/openapi/initialized-components.md @@ -0,0 +1,234 @@ +# Initialized OpenAPI components + +Below you will find details on some prepopulated OpenAPI components we added to Dotkernel API. + +## OA\Info + +Defined in `src/App/src/OpenAPI.php`, this object provides general info about the API: + +- `version`: API version (example: `1.0.0`) +- `title`: title shown in the UI (example: `Dotkernel API`) + +For more info, see [this page](https://spec.openapis.org/oas/latest.html#info-object). + +## OA\Server + +Defined in `src/App/src/OpenAPI.php`, this object provides API server entries: + +- `url`: API server URL (example: `https://api.example.com` - use no trailing slash!) +- `description`: describes the purpose of the server (example: `Dev`, `Staging`, `Production` or even `Auth` if you use a separate authentication server) + +You can have multiple `Server` definitions, one for each of your Dotkernel API instances. + +For more info, see [this page](https://spec.openapis.org/oas/latest.html#server-object). + +## OA\SecurityScheme + +Defined in `src/App/src/OpenAPI.php`, you will find an object for the `AuthToken` security header: + +- `securityScheme`: the name of the security scheme—you will provide this to indicate that an endpoint is protected +- `type`: whether it's an API key, an authorization header etc. +- `in`: indicates where the scheme is applied (`query`/`header`/`cookie`) +- `bearerFormat`: a hint to the client to identify how the bearer token is formatted +- `scheme`: the name of the authorization scheme to be used + +And another object for the `ErrorReportingToken` security token: + +- `securityScheme`: the name of the security scheme—you will provide this to indicate that an endpoint is protected +- `type`: whether it's an API key, an authorization header etc. +- `in`: indicates where the scheme is applied (`query`/`header`/`cookie`) +- `name`: the name of the header + +For more info, see [this page](https://spec.openapis.org/oas/latest.html#security-scheme-object). + +## OA\ExternalDocumentation + +Defined in `src/App/src/OpenAPI.php`, in this object we provide the following details: + +- `description`: describes the purpose of the document +- `url`: external documentation URL + +For more info, see [this page](https://spec.openapis.org/oas/latest.html#external-documentation-object). + +## OA\Schema + +Schemas are OpenAPI objects describing an object or collection of objects existing in your project. + +### Schemas describing objects + +To describe an object (entity), you will need to transform it into a schema. + +Object: + +```php + Make sure that in `src/App/src/OpenAPI.php`, on the line with `#[OA\Server` the value of `url` is set to the of URL of your instance of **Dotkernel API**. +> +> You can add multiple servers (for staging, production, etc.) by duplicating the existing one. + +For more info, see [this page](https://spec.openapis.org/oas/latest.html#schema). + +### Common schemas + +We provided some schemas that are reusable across the entire project. They are defined in `src/App/src/OpenAPI.php`: + +- `#/components/schemas/Collection`: provides the default **HAL** structure to all the collections extending it +- `#/components/schemas/ErrorMessage`: describes an operation that resulted in an error—may contain multiple messages +- `#/components/schemas/InfoMessage`: describes an operation that completed successfully—may contain multiple messages diff --git a/docs/book/v7/openapi/introduction.md b/docs/book/v7/openapi/introduction.md new file mode 100644 index 00000000..38a3a8e2 --- /dev/null +++ b/docs/book/v7/openapi/introduction.md @@ -0,0 +1,5 @@ +# OpenAPI documentation + +To provide an interactive documentation, Dotkernel API implemented [zircote/swagger-php](https://github.com/zircote/swagger-php). + +Developers can use this library to auto-generate documentation that outlines available endpoints, their request details, and their response formats. diff --git a/docs/book/v7/openapi/render-documentation.md b/docs/book/v7/openapi/render-documentation.md new file mode 100644 index 00000000..c038bc3f --- /dev/null +++ b/docs/book/v7/openapi/render-documentation.md @@ -0,0 +1,81 @@ +# Rendering the documentation file + +At this step, you only have a static documentation file. +You will need an interface that can render it so that you will be able to interact with your Dotkernel API. + +To do this, we recommend using either of: + +- [swagger-api/swagger-ui](https://github.com/swagger-api/swagger-ui) +- [Redocly/redoc](https://github.com/Redocly/redoc) + +## Using Swagger UI + +Navigate to the `public` directory of your instance of Dotkernel API and create an HTML (you can call it `swagger.html`, the name is up to you) and place the following HTML content in it: + +```html + + + + + + + Dotkernel API Documentation + + + +
+ + + + +``` + +Make sure that you replace `PATH_TO_YOUR_OPENAPI_FILE` with the relative path to your documentation file +(openapi.json/openapi.yaml). The line should look similar to this: + +```js +window.ui = SwaggerUIBundle({url: './openapi.yaml', dom_id: '#swagger-ui'}); +``` + +Using your browser, open a new tab and type in the URL of your instance of Dotkernel API and append `/swagger.html` to it. +You should see the Redoc interface with your documentation file loaded in it. +From here, you can inspect each endpoint, see its URL, check if it needs authentication, the request payload (if any) and the possible response(s). + +## Using Redoc + +Navigate to the `public` directory of your instance of Dotkernel API and create an HTML (you can call it `redoc.html`, +the name is up to you) and place the following HTML content in it: + +```html + + + + + + + Dotkernel API Documentation + + + +
+ + + +``` + +Make sure that you replace `PATH_TO_YOUR_OPENAPI_FILE` with the relative path to your documentation file (openapi.json/openapi.yaml). +The line should look similar to this: + +```js +Redoc.init('./openapi.yaml', {}, document.getElementById('redoc-container')); +``` + +Using your browser, open a new tab and type in the URL of your instance of Dotkernel API and append `/redoc.html` to it. +You should see the Redoc interface with your documentation file loaded in it. +From here, you can inspect each endpoint, see its URL, check if it needs authentication, the request payload (if any) and the possible response(s). diff --git a/docs/book/v7/openapi/use-documentation.md b/docs/book/v7/openapi/use-documentation.md new file mode 100644 index 00000000..55780635 --- /dev/null +++ b/docs/book/v7/openapi/use-documentation.md @@ -0,0 +1,116 @@ +# Using the documentation + +Since Redoc is readonly, in the following section we will focus only on using Swagger UI. + +## Protected endpoints + +Now that you have a UI for the documentation, you can see all the endpoints. You will see that some of them have a lock symbol right before the collapse/expand arrow. +When you see this symbol next to an endpoint, it means that the endpoint is protected and can only be accessed when authenticated with an account with proper permissions. + +## Authentication + +In Swagger UI, you will see an `Authorize` button. Clicking it will open a modal where you will find two sections: + +- `AuthToken` - where you will have to enter a valid auth token +- `ErrorReportingToken` - where you will have to enter a valid error reporting token + +Below, we will walk you through on how to find both tokens. For now, let's close the modal. + +### Generating AuthToken + +This token is required with most of the Dotkernel API endpoints. There are two entities that generate this type of token: `(super)admin`s and `user`s. +Depending on the endpoint description, you will know which one you need to use. +Examples: + +- `/user`: the description says `Admin lists user accounts` - it means that you need an AccessToken with `(super)admin` privileges +- `/user/my-account`: the description says `User fetches their own account` - it means that you need an AccessToken with `user` privileges + +In the UI, find a section called `AccessToken`, toggle the `/security/generate-token` (`Generate access token`) endpoint and click the `Try it out` button. +Under the `Access token generation request` you will find a textarea prepopulated with a JSON object. +You will have to change the value of `username` and `password`. +See [this guide](../tutorials/token-authentication.md#credentials) for the credentials. + +After you have filled out the credentials, click on the `Execute` button below the textarea. +This will send the request to your instance of Dotkernel API. +If everything went well, under the textarea you should see: + +- the `curl` request that was made +- the `Request URL` the request was sent to +- the `Server response` with `200 OK` response code and the `Response body` with a JSON object containing `token_type`, + `expires_in`, `access_token` and `refresh_token`. + +> Save the `refresh_token` somewhere, you will need it later + +Now copy the value of `access_token` (make sure you copy all the characters, without the surrounding double quotes) and go back up to the `Authorize` button and click it to open the auth modal. +Paste the copied token as the value of the `AuthToken` and click on the **Authorize** button you see under the input field. +The **Authorize** button has now changed to **Logout**. +You can close the modal. + +From here, Swagger UI will remember the AuthToken until you close/refresh the browser tab. +Also, it will automatically append the `Authorization` header to each request, allowing you to make authorized API calls. + +If you need to switch to an account with different privileges, you go again to the `Authorize` button, click on it to open the auth modal, and click **Logout** for the `AuthToken`. +Then paste the new token as the value of the `AuthToken`, click on the **Authorize** button, close the modal and continue using the UI authenticated with the new account. + +### Refreshing AuthToken + +By default, auth tokens expire in 1 day. +If you make an API call, and you receive an error telling you that your auth token is expired, you need to either generate a new token (as seen above) or refresh the existing one using the `refresh_token` received when generating the current token. + +To refresh the auth token, you find the same section called `AccessToken`, toggle the `/security/refresh-token` (`Refresh access token`) endpoint and click the `Try it out` button. +Under the `Access token refresh request` you will find a textarea prepopulated with a JSON object. +You will have to change the value of `refresh_token` to the refresh token of your current auth token. + +Once done, click on the `Execute` button below the textarea. +This will send the request to your instance of Dotkernel API. +If everything went well, under the textarea you should see the same details: + +- the `curl` request that was made +- the `Request URL` the request was sent to +- the `Server response` with `200 OK` response code and the `Response body` with a JSON object containing `token_type`, `expires_in`, `access_token` and `refresh_token` + +From here, you will follow the same steps: + +- copy the `access_token` +- go to the `Authorize` button to open the auth modal +- paste the new token and click on **Authorize** +- close the modal + +### Generating ErrorReportingToken + +Just like the AuthTokens, ErrorReportingTokens are used to make authorized API calls. +The difference is that this token applies only to one specific endpoint: `/error-report` (`Report an error to the API`). +This endpoint is intended to be used by third-party applications and frontends to report an error back to the API. + +> This endpoint does not require `AuthTokens` + +To generate this token, follow [this guide](../commands/generate-tokens.md#generate-error-reporting-token). + +Once you have the error reporting token, go again to the `Authorize` button, paste the new token as the value of the `ErrorReportingToken`, click on the **Authorize** button and close the modal. +Now you're ready to report errors to your instance of Dotkernel API. + +## Making API calls + +> The UI does not use confirmation messages before making an API call, so double-check any operation before executing it. + +Once authorized in the UI, you can click on any endpoint to expand it. +There you will find an overview of the endpoint, including: + +- Request method (`DELETE`, `GET`, `PATCH`, `POST`, `PUT`) +- request URL (example: `/resource`) +- Short description +- Long description +- Parameters—if this area says `No parameters`, then there are no parameters to fill out; else, make sure you fill out all the required parameters +- Request body—if present, provides a textarea prepopulated with a JSON object describing the request +- Responses—a list of possible HTTP status codes and their respective response bodies + +Clicking the `Try it out` button will activate any parameter input fields and the request body textarea (if any). +Clicking `Cancel` will deactivate them. + +Make sure you fill out all the necessary data, then click on the `Execute` found button above `Responses`. +This will send the request and return and display the API response. +Once finished, you will see the response as the first item under `Responses`, including the HTTP status code and the response body. + +You can repeat the request by clicking again on the `Execute` button. +This will first clear the previous output and display the new response in the same place. +Additionally, between two executions, you can manually clear any previous output using the `Clear` button next to the `Execute` button. diff --git a/docs/book/v7/openapi/write-documentation.md b/docs/book/v7/openapi/write-documentation.md new file mode 100644 index 00000000..386631b2 --- /dev/null +++ b/docs/book/v7/openapi/write-documentation.md @@ -0,0 +1,99 @@ +# Writing documentation + +> To avoid polluting PHP files with maybe thousands of lines of OpenAPI attributes, we opted for storing them in separate files, called `OpenAPI.php`, one for each module. + +We already covered all the endpoints available in Dotkernel API, you can consult the existing documentation in each module's own `OpenAPI.php` file. +After you add more functionalities to your API, you will have to document the new endpoints. +This is easier than it sounds because in most cases you will do the same: add a request by method, describe the request payload (if any), add request parameters (if any) and describe the possible responses. + +## Common objects + +To do this, you will use the following request objects: + +- `OA\Delete`: delete an API resource identified by its unique id +- `OA\Get`: fetch API single or collections of API resources +- `OA\Post`: create a new API resource (unless if it already exists) +- `OA\Patch`: update an existing API resource +- `OA\Put`: create a new API resource (if it already exists, it is overwritten) + +Also, the following components describe PHP objects: + +- `OA\Schema`: describe an object sent in a request or received as a response - [read more](https://spec.openapis.org/oas/latest.html#schema-object) +- `OA\Parameter`: describe a `query`/`path` parameter - [read more](https://spec.openapis.org/oas/latest.html#parameter-object) +- `OA\RequestBody`: describe the body of a request - [read more](https://spec.openapis.org/oas/latest.html#request-body-object) + +There are a lot more, but these are the most often used ones. + +If you need help, take a look at the existing definitions found in Dotkernel API. + +### OA\Delete + +Defines a `DELETE` HTTP request. It should specify at least the following parameters: + +- `path`: the route to the resource (example: `/resource/{uuid}` - where `uuid` is a path parameter defined below) +- `description`: verbose description of the endpoint's purpose +- `summary`: short description of the endpoint's purpose +- `security`: an array of security scheme(s) to be used—omit if the endpoint is not protected +- `tags`: an array of tags to help group related requests (example: user-related requests could have a `User` tag) +- `parameters`: an array of `query`/`path` parameters - each parameter is specified as a new `OA\Parameter` object +- `responses`: an array of `OA\Response` objects, each describing a combination of HTTP status codes and their respective response bodies + +### OA\Get + +Defines a `GET` HTTP request. It should specify at least the following parameters: + +- `path`: the route to a single or collection of resources (example: `/resource/{uuid}` for a single resource or `/resource` for a collection of resources) +- `description`: verbose description of the endpoint's purpose +- `summary`: short description of the endpoint's purpose +- `security`: an array of security scheme(s) to be used—omit if the endpoint is not protected +- `tags`: an array of tags to help group related requests (example: user-related requests could have a `User` tag) +- `parameters`: an array of `query`/`path` parameters - each parameter is specified as a new `OA\Parameter` object +- `responses`: an array of `OA\Response` objects, each describing a combination of HTTP status codes and their respective response bodies + +### OA\Patch + +Defines a `PATCH` HTTP request. It should specify at least the following parameters: + +- `path`: the route to the resource (example: `/resource/{uuid}` - where `uuid` is a path parameter defined below) +- `description`: verbose description of the endpoint's purpose +- `summary`: short description of the endpoint's purpose +- `security`: an array of security scheme(s) to be used—omit if the endpoint is not protected +- `requestBody`: a `OA\RequestBody` object describing the data being sent in the request +- `tags`: an array of tags to help group related requests (example: user-related requests could have a `User` tag) +- `parameters`: an array of `query`/`path` parameters - each parameter is specified as a new `OA\Parameter` object +- `responses`: an array of `OA\Response` objects, each describing a combination of HTTP status codes and their respective response bodies + +### OA\Post + +Defines a `POST` HTTP request. It should specify at least the following parameters: + +- `path`: the route to the resource (example: `/resource/{uuid}` - where `uuid` is a path parameter defined below) +- `description`: verbose description of the endpoint's purpose +- `summary`: short description of the endpoint's purpose +- `security`: an array of security scheme(s) to be used—omit if the endpoint is not protected +- `requestBody`: a `OA\RequestBody` object describing the data being sent in the request +- `tags`: an array of tags to help group related requests (example: user-related requests could have a `User` tag) +- `parameters`: an array of `query`/`path` parameters - each parameter is specified as a new `OA\Parameter` object +- `responses`: an array of `OA\Response` objects, each describing a combination of HTTP status codes and their respective response bodies + +### OA\Put + +Defines a `PUT` HTTP request. It should specify at least the following parameters: + +- `path`: the route to the resource (example: `/resource/{uuid}` - where `uuid` is a path parameter defined below) +- `description`: verbose description of the endpoint's purpose +- `summary`: short description of the endpoint's purpose +- `security`: an array of security scheme(s) to be used—omit if the endpoint is not protected +- `requestBody`: a `OA\RequestBody` object describing the data being sent in the request +- `tags`: an array of tags to help group related requests (example: user-related requests could have a `User` tag) +- `parameters`: an array of `query`/`path` parameters - each parameter is specified as a new `OA\Parameter` object +- `responses`: an array of `OA\Response` objects, each describing a combination of HTTP status codes and their respective response bodies + +## Conclusion + +To summarize, the typical scenario on working on your own instance of Dotkernel API would follow these steps: + +- create new module (example: `Book`) +- add functionality to your new module (routes, entities, repositories, handlers, services, tests etc) +- create file `OpenAPI.php` in the new module and describe each new endpoint +- generate the latest version of a documentation file as described [in this tutorial](./generate-documentation.md) diff --git a/docs/book/v7/reference/account-anonymization.md b/docs/book/v7/reference/account-anonymization.md new file mode 100644 index 00000000..5d6a71a0 --- /dev/null +++ b/docs/book/v7/reference/account-anonymization.md @@ -0,0 +1,40 @@ +# Account anonymization + +## Premise + +According to the GDPR, companies that record personal data from EU citizens must delete said data if its owner requests its deletion. +An alternative is to anonymize the data, according to [this article](https://commission.europa.eu/law/law-topic/data-protection/reform/rules-business-and-organisations/dealing-citizens/do-we-always-have-delete-personal-data-if-person-asks_en). + +## Definition + +### What is Personally identifiable information? + +According to [this article](https://commission.europa.eu/law/law-topic/data-protection/reform/what-personal-data_en), Personally identifiable information (PII) is: + +- A name and surname. +- A home address. +- An email address such as name.surname@company.com. +- An identification card number. +- Location data (for example, the location data function on a mobile phone). +- An Internet Protocol (IP) address. +- A cookie ID. +- The advertising identifier of your phone. +- A phone number. +- Data held by a hospital or doctor, which could be a symbol that uniquely identifies a person. + +Out of the box, Dotkernel API saves the user's name (firstname and lastname) and email (identity). +This personal data is used for emails related to password reset and account activation. + +## Process + +### Anonymization + +The anonymization process makes these replacements: + +- The firstname and lastname are replaced with `anonymous` concatenated with the current UNIX timestamp, e.g. `anonymous1725980747`. +- The email is replaced with `anonymous` concatenated with the current UNIX timestamp and the value in `userAnonymizeAppend`, e.g. `anonymous1725980747@example.com`. +- The avatar image and its database record are deleted. + +The `userAnonymizeAppend` key can be set in `config/autoload/local.php` or left empty. + +> Using an email domain for `userAnonymizeAppend` would work as a catch-all email, if your email service provider has this option enabled. diff --git a/docs/book/v7/security/basic-security.md b/docs/book/v7/security/basic-security.md new file mode 100644 index 00000000..2adbfa55 --- /dev/null +++ b/docs/book/v7/security/basic-security.md @@ -0,0 +1,85 @@ +# Basic Security + +Dotkernel API provides all necessary tools to implement safe applications; however, you will need to manually make use of some of them. +This section will go over the provided tools and any steps you need to follow to use them successfully, as well as a few general considerations. + +## User Input Validation + +To validate user input, Dotkernel API makes use of [laminas/laminas-inputfilter](https://github.com/laminas/laminas-inputfilter). +It is strongly recommended that custom functionality parsing user input also make use of input filters to validate the data. + +## Content Negotiation + +Content negotiation in Dotkernel API is done by a middleware configured using the `config/autoload/content-negotiation.global.php` file. + +Whenever an endpoint needs custom `Accept` and/or `Content-Type`, make sure that you set them in the above file. + +> Read more about [content negotiation](https://www.dotkernel.com/dotkernel-api/content-negotiation-in-dotkernel-rest-api/) and the way it is implemented in [Dotkernel API](../core-features/content-validation.md). + +## Cross-Origin Resource Sharing + +Dotkernel API uses [mezzio/mezzio-cors](https://github.com/mezzio/mezzio-cors) to handle CORS details. +The default configuration, found in `config/autoload/cors.local.php`, makes the application accessible by any origin. + +Make sure your application specifies only the required origins when in a production environment. + +> This step is described in the [CORS](../tutorials/cors.md) tutorial. + +## Role-Based Access Control + +This project makes use of [mezzio/mezzio-authorization-rbac](../core-features/authorization.md) to handle access control. + +The default use cases have already been configured, but any custom functionality will require additional configuration to make sure it is protected. +Update the configuration file of this package (`config/autoload/authorization.global.php`) whenever you add new routes or roles. + +## Demo Credentials + +Dotkernel API ships with two demo accounts: an admin account (`admin`) and a user account (`test@dotkernel.com`), +with public identities and passwords as described in the [token authentication tutorial](https://docs.dotkernel.org/api-documentation/v7/tutorials/token-authentication/). + +Make sure to **update** or **remove** these demo accounts in your production environment. + +## Error Reporting Endpoint and ErrorReportingTokens + +The error reporting endpoint (`/error-report`) is intended to be used by third parties to report errors back to your application. +This endpoint requires its own token type, namely an `ErrorReportingToken`, that is to be added in the configuration file for every application sending reports. + +Since these tokens do not have an expiration date, consider periodically refreshing them manually. + +This feature is configured using the `config/autoload/error-handling.global.php` file, under the configured `ErrorReportServiceInterface::class` key. + +This file is visible to your VCS by default, so take care not to overwrite tokens locally and commit them to your production environment. +Additionally, make sure the `ip_whitelist` or `domain_whitelist` keys are set to your desired values, especially when in a production environment. + +> Read more about the [error reporting](../core-features/error-reporting.md) feature. + +## OpenAPI Documentation + +To provide an interactive documentation, Dotkernel API implemented [zircote/swagger-php](https://github.com/zircote/swagger-php). + +Make sure **not** to include sensitive data as examples for any of the documented endpoints. +It is not recommended to enable the documentation in a production environment. + +> Read more about the [OpenAPI documentation](../openapi/introduction.md). + +## PHP Dependencies + +Dotkernel API uses `composer` to handle PHP dependencies. +In time, make sure to review any common vulnerabilities and exposures for your dependencies. + +> You may also keep an eye on the Dotkernel API changelog for any updates relevant to your project. + +## General Considerations + +- `*.global.php` and `*.php.dist` configuration files are visible to the VCS, make sure **not** to include sensitive data in commits. + - `*.local.php` configuration files are ignored by the VCS by default and are the recommended place for sensitive data such as API keys. +- Make sure the `development mode` is correctly set - **do not** enable `development mode` in a production environment. + - You can use the following command to check the current status: + +```shell +composer development-status +``` + +- Dotkernel API ships with a [Laminas Continuous Integration](https://github.com/laminas/laminas-continuous-integration-action) GitHub Action, if you are using a public repository, consider keeping it in your custom applications to ensure code quality. + +> Read more about using [Laminas Continuous Integration](https://getlaminas.org/blog/2024-08-05-using-laminas-continuous-integration.html). diff --git a/docs/book/v7/security/oauth2-security.md b/docs/book/v7/security/oauth2-security.md new file mode 100644 index 00000000..0e869154 --- /dev/null +++ b/docs/book/v7/security/oauth2-security.md @@ -0,0 +1,33 @@ +# OAuth2 Security + +Dotkernel API uses the [mezzio/mezzio-authentication-oauth2](https://github.com/mezzio/mezzio-authentication-oauth2) component to provide the OAuth2 authentication service. +As a security stating point, when developing an application using this project, make sure you go over the following steps. + +## Default OAuth Clients + +The project ships with the default OAuth clients `admin` and `frontend` with passwords equal to their names, as described in the [Authentication](https://docs.dotkernel.org/api-documentation/v7/core-features/authentication/) guide. + +These clients **must not** remain unchanged in your production environment, as they are a security risk; ensure you deleted them or updated the passwords. + +## OAuth Token Lifetime and Refresh Hygiene + +The configuration for OAuth2 tokens can be edited in `config/autoload/local.php` under the `authentication` key. + +By default, the lifetimes of the `access` and `refresh` tokens are set to one day and one month respectively. +Make sure to adjust their values in accordance with your application's needs, with lower values being generally safer. + +> If your application requires it, you can revoke user OAuth tokens before their expiration by making use of the `revokeTokens` method of `UserService`. +> +> Read more about the available [configuration options](https://docs.mezzio.dev/mezzio-authentication-oauth2/v1/intro/#configuration). + +## Autogeneration of Cryptographic Keys + +Dotkernel API makes use of the `./vendor/bin/generate-oauth2-keys` command from `mezzio-authentication-oauth2` to automatically regenerate the +public/private key pair used to verify the transmitted JWTs. +This process is done after each `composer update` (or `composer install` with no lock file), as specified in `composer.json` under the `scripts.post-update-cmd` key. + +While hidden to the VCS by default, keep in mind not to commit any local keys. + +> Autogeneration of keys can be disabled by simply removing the `php ./vendor/bin/generate-oauth2-keys` command from the mentioned key. +> +> While not related to Dotkernel API itself, do ensure that the directory containing the keys is properly secured. diff --git a/docs/book/v7/transition-from-api-tools/api-tools-vs-dotkernel-api.md b/docs/book/v7/transition-from-api-tools/api-tools-vs-dotkernel-api.md new file mode 100644 index 00000000..47ebad74 --- /dev/null +++ b/docs/book/v7/transition-from-api-tools/api-tools-vs-dotkernel-api.md @@ -0,0 +1,21 @@ +# Laminas API Tools compared to Dotkernel API + +| | API Tools (formerly Apigility) | Dotkernel API | +|---------------------|------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------| +| URL | [api-tools](https://api-tools.getlaminas.org/) | [Dotkernel API](https://www.dotkernel.org) | +| First Release | 2012 | 2018 | +| PHP Version | <= 8.2 | >= 8.2 | +| Architecture | MVC, Event Driven | Middleware | +| OSS Lifecycle | Archived | ![OSS Lifecycle](https://img.shields.io/osslifecycle?style=flat&label=&file_url=https%3A%2F%2Fgithub.com%2Fdotkernel%2Fapi%2Fblob%2F7.0%2FOSSMETADATA) | +| Style | REST, RPC | REST | +| Versioning | Yes | [Deprecations](https://docs.dotkernel.org/api-documentation/v7/tutorials/api-evolution/) | +| Documentation | Swagger (Automated) | OpenAPI (Swagger) / Postman (Manual) | +| Content-Negotiation | Custom | Custom | +| License | BSD-3 | MIT | +| Default DB Layer | laminas-db | doctrine-orm | +| Authorization | ACL | RBAC-guard | +| Authentication | HTTP Basic/Digest OAuth2.0 | OAuth2.0 | +| CI/CD | Yes | Yes | +| Unit Tests | Yes | Yes | +| Code Generator | Yes | [dotkernel/dot-maker](https://www.dotkernel.com/headless-platform/dotmaker-generate-common-code-in-dotkernel/) | +| PSR | PSR-7 | PSR-7, PSR-15 | diff --git a/docs/book/v7/transition-from-api-tools/discovery-phase.md b/docs/book/v7/transition-from-api-tools/discovery-phase.md new file mode 100644 index 00000000..bc06e920 --- /dev/null +++ b/docs/book/v7/transition-from-api-tools/discovery-phase.md @@ -0,0 +1,37 @@ +# Discovery phase for a current system built using API Tools [WIP] + +To transition a system built using api-tools to Dotkernel API, we need to analyze the core components of it. + +## Database + +- is there a database in the current API? +- which is the connection to a database +- which library is used for database interaction (laminas-db, doctrine 2, eloquent, or else)? + +> Dotkernel API is tested only with MariaDB version 10.6, 10.11 LTS, 11.4 LTS, 11.8 LTS, and PostgreSQL version 13 and above. + +## Authentication and Authorization + +- how is authentication done? (basic, digest, oauth2, etc.) +- how is authorization done? (acl, rbac) + +## Modules + +- analyze configuration files of the modules (what needs to be configured to use a module) +- analyze routes (which are the routes, protection rules, which one needs auth, etc.) +- analyze a response format (content negotiation and validation, which ones are JSON, hal, views, etc.) +- analyze input field validations + +## Custom functionalities + +Analyze the custom code (code that cannot be generated through Admin UI and requires manual implementation) + +For instance: + +- caching +- events +- services +- extra installed packages and libraries +- jobs and queues +- third-parties +- tests diff --git a/docs/book/v7/transition-from-api-tools/transition-approach.md b/docs/book/v7/transition-from-api-tools/transition-approach.md new file mode 100644 index 00000000..f93842a0 --- /dev/null +++ b/docs/book/v7/transition-from-api-tools/transition-approach.md @@ -0,0 +1,14 @@ +# Transition approach [WIP] + +Dotkernel API is not a one-to-one replacement of api-tools (former Apigility), but is only a potential solution to migrate to. + +Functionalities, components and architecture are different. + +See the [Comparison between Dotkernel APi and api-tools](https://docs.dotkernel.org/api-documentation/v4/transition-from-api-tools/api-tools-vs-dotkernel-api/). + +## Business cases + +There are at least two approaches for this transition: + +- Clone 1:1 and recreate all endpoints and entities +- Build a new version of the current API using Dotkernel API and keep it running as separate platforms until the sunset of the current version of api-tools diff --git a/docs/book/v7/tutorials/api-evolution.md b/docs/book/v7/tutorials/api-evolution.md new file mode 100644 index 00000000..f8114cec --- /dev/null +++ b/docs/book/v7/tutorials/api-evolution.md @@ -0,0 +1,65 @@ +# API Evolution pattern + +API evolution: Updating an API while keeping it compatible for existing consumers by adding new features, fixing bugs, planning and removing outdated features. + +## How it works + +In Dotkernel API we can mark an entire endpoint or a single method as deprecated using attributes on handlers. +We use response headers to inform the consumers about the future changes by using two new headers: + +- `Link` - it's a link to the official documentation pointing out the changes that will take place. +- `Sunset` - this header is a date, indicating when the deprecated resource will potentially become unresponsive. + +**Both headers are independent, you can use them separately.** + +> Make sure you have the `DeprecationMiddleware:class` piped in your `pipeline` list. +> In our case it's `config/pipeline.php`. + +## Marking an entire endpoint as deprecated + +When you want to mark an entire resource as deprecated, you have to use the `ResourceDeprecation` attribute. + +```php +... +#[ResourceDeprecation( + sunset: '2038-01-01', + link: 'https://docs.dotkernel.org/api-documentation/v7/tutorials/api-evolution/', + deprecationReason: 'Resource deprecation example.', + rel: 'sunset', + type: 'text/html' +)] +class HomeHandler implements RequestHandlerInterface +{ +} +``` + +In the example above, the `ResourceDeprecation` attribute is attached to the class, marking the entire `/` (home) endpoint as deprecated starting from `2038-01-01`. + +Running the following curl will print out the response headers where we can see the **Sunset** and **Link** headers. + +```shell +curl --head -X GET http://0.0.0.0:8080 -H "Content-Type: application/json" +``` + +```shell +HTTP/1.1 200 OK +Host: 0.0.0.0:8080 +Date: Mon, 24 Jun 2024 10:23:11 GMT +Connection: close +X-Powered-By: PHP/6.4.20 +Content-Type: application/json +Permissions-Policy: interest-cohort=() +Sunset: 2038-01-01 +Link: https://docs.dotkernel.org/api-documentation/v7/tutorials/api-evolution/;rel="sunset";type="text/html" +Vary: Origin +``` + +## Notes + +> If `Link` or `Sunset` do not have a value they will not appear in the response headers. + +> `Sunset` has to be a **valid** date, otherwise it will throw an error. + +> Deprecations can only be attached to handler classes that implement `RequestHandlerInterface`. + +> The `rel` and `type` arguments are optional, they default to `sunset` and `text/html` if no value was provided and are `Link` related parts. diff --git a/docs/book/v7/tutorials/cors.md b/docs/book/v7/tutorials/cors.md new file mode 100644 index 00000000..185815a7 --- /dev/null +++ b/docs/book/v7/tutorials/cors.md @@ -0,0 +1,90 @@ +# CORS + +## What is CORS? + +**Cross-Origin Resource Sharing** or _CORS_ is an HTTP header-based mechanism that allows a server to indicate any other +origins (domain, scheme, or port) than its own from which a browser should permit loading of resources. + +## Why do we need CORS? + +When integrating an API, most developers have encountered the following error message: + +> Access to fetch at _RESOURCE_URL_ from origin _ORIGIN_URL_ has been blocked by CORS policy: +> No ‘Access-Control-Allow-Origin’ header is present on the requested resource. + +This happens because the API (_RESOURCE_URL_) is not configured to accept requests from the client (_ORIGIN_URL_). + +## How to fix? + +Dotkernel API fixes this issue using the [mezzio/mezzio-cors](https://github.com/mezzio/mezzio-cors) library. + +### Step 1: Install the library + +To install `mezzio/mezzio-cors`, run the following command: + +```shell +composer require mezzio/mezzio-cors +``` + +### Step 2: Configure your API + +#### Register ConfigProvider + +Register `mezzio/mezzio-cors` in your application by adding its ConfigProvider to your application's config aggregator. +Open the file `config/config.php` and paste the below lines at the beginning of the array passed to `ConfigAggregator`: + +```php +Laminas\Diactoros\ConfigProvider::class, +Mezzio\Cors\ConfigProvider::class, +``` + +Save and close the file. + +#### Add middleware + +Add `mezzio/mezzio-cors` middleware to your application's pipeline. +Open `config/pipeline.php` and paste the below line before the one with `RouteMiddleware::class`: + +```php +$app->pipe(\Mezzio\Cors\Middleware\CorsMiddleware::class); +``` + +Save and close the file. + +#### Create a config file + +Create and open file `config/autoload/cors.local.php` and add the following code inside it: + +```php + [ + 'allowed_origins' => [ + ConfigurationInterface::ANY_ORIGIN, + ], + 'allowed_headers' => ['Accept', 'Content-Type', 'Authorization'], + 'allowed_max_age' => '600', + 'credentials_allowed' => true, + 'exposed_headers' => [], + ], +]; +``` + +This list explains the above configuration values: + +- `allowed_origins`: an array of domains that are allowed to interact with the API (default `ConfigurationInterface::ANY_ORIGIN` which means that any domain can make requests to the API) +- `allowed_headers`: an array of allowed custom headers +- `allowed_max_age`: the maximum duration, since the preflight response may be cached by a client +- `credentials_allowed`: allows a request to pass cookies +- `exposed_headers`: an array of headers which are being exposed by the endpoint + +Save and close the file. + +> On the **production** environment, make sure you allow only specific origins by adding them to the `allowed_origins` array and removing the current value of `ConfigurationInterface::ANY_ORIGIN`. + +For more info, see [mezzio/mezzio-cors documentation](https://docs.mezzio.dev/mezzio-cors/v1/middleware/#configuration). diff --git a/docs/book/v7/tutorials/create-book-module-via-dot-maker.md b/docs/book/v7/tutorials/create-book-module-via-dot-maker.md new file mode 100644 index 00000000..0ae71296 --- /dev/null +++ b/docs/book/v7/tutorials/create-book-module-via-dot-maker.md @@ -0,0 +1,432 @@ +# Implementing a book module in Dotkernel API using DotMaker + +The `dotkernel/dot-maker` library can be used to programmatically generate project files and directories. +It can be added to your API installation by following the [official documentation](https://docs.dotkernel.org/dot-maker/). + +## Folder and files structure + +The below files structure is what we will have at the end of this tutorial and is just an example; you can have multiple components such as event listeners, wrappers, etc. + +```markdown +. +└── src/ + ├── Book/ + │ └── src/ + │ ├── Collection/ + │ │ └── BookCollection.php + │ ├── Handler/ + │ │ ├── GetBookCollectionHandler.php + │ │ ├── GetBookResourceHandler.php + │ │ └── PostBookResourceHandler.php + │ ├── InputFilter/ + │ │ ├── Input/ + │ │ │ ├── AuthorInput.php + │ │ │ ├── NameInput.php + │ │ │ └── ReleaseDateInput.php + │ │ └── CreateBookInputFilter.php + │ ├── Service/ + │ │ ├── BookService.php + │ │ └── BookServiceInterface.php + │ ├── ConfigProvider.php + │ └── RoutesDelegator.php + └── Core/ + └── src/ + └── Book/ + └── src/ + ├──Entity/ + │ └──Book.php + ├──Repository/ + │ └──BookRepository.php + └── ConfigProvider.php +``` + +* `src/Book/src/Collection/BookCollection.php` – a collection refers to a container for a group of related objects, typically used to manage sets of related entities fetched from a database +* `src/Book/src/Handler/GetBookCollectionHandler.php` – handler that reflects the GET action for the BookCollection class +* `src/Book/src/Handler/GetBookResourceHandler.php` – handler that reflects the GET action for the Book entity +* `src/Book/src/Handler/PostBookResourceHandler.php` – handler that reflects the POST action for the Book entity +* `src/Book/src/InputFilter/Input/*` – input filters and validator configurations +* `src/Book/src/InputFilter/CreateBookInputFilter.php` – input filters and validators +* `src/Book/src/Service/BookService.php` – is a class or component responsible for performing a specific task or providing functionality to other parts of the application +* `src/Book/src/Service/BookServiceInterface.php` – interface that reflects the publicly available methods in `BookService` +* `src/Book/src/ConfigProvider.php` – is a class that provides configuration for various aspects of the framework or application +* `src/Book/src/RoutesDelegator.php` – a RoutesDelegator is a delegator factory responsible for configuring routing middleware based on routing configuration provided by the application +* `src/Core/src/Book/src/Entity/Book.php` – an entity refers to a PHP class that represents a persistent object or data structure +* `src/Core/src/Book/src/Repository/BookRepository.php` – a repository is a class responsible for querying and retrieving entities from the database +* `src/Core/src/Book/src/ConfigProvider.php` – is a class that provides configuration for Doctrine ORM + +## File creation and contents + +After successfully installing `dot-maker`, it can be used to generate the Book module. +Invoke `dot-maker` by executing `./vendor/bin/dot-maker` or via the optional script described in the documentation - `composer make`. +This will list all component types that can be created - for this tutorial, enter `module`: + +```shell +./vendor/bin/dot-maker module +``` + +Type `book` when prompted to enter the module name. + +Next you will be prompted to add the relevant components of a module, accepting `y(es)`, `n(o)` and `Enter` (defaults to `yes`): + +> Note that `dot-maker` will automatically split the files into the described `Api` and `Core` structure without a further input needed. + +* `Entity and repository` (Y): will generate the `Book.php` entity and the associated `BookRepository.php`. +* `Service` and `service interface` (Y): will generate the `BookService` and the `BookServiceInterface`. +* `Command`, followed by `middleware`(N): not necessary for the module described in this tutorial. +* `Handler` (Y): this option is needed, and will further prompt you for the required actions. + * `Allow listing Books?` (Y): this will generate both the `GetBookResourceHandler.php` class and the `BookCollection.php` it uses. + * `Allow viewing Books?` (Y): will generate the single resource GET action handler - `GetBookResourceHandler.php`. + * `Allow creating Books?` (Y): will generate the POST action handler for the `Book` entity - `PostBookResourceHandler.php`, as well as the input filter used for validating the data - `CreateBookInputFilter.php`. + * `Allow deleting Books?`, `Allow editing Books?` and `Allow replacing Books?` (N): will generate handlers that reflect the DELETE, PATCH and PUT actions respectively, but are not necessary for this tutorial. +* Following this step, `dot-maker` will automatically generate the `ConfigProvider.php` classes for both the `Api` and `Core` namespaces, as well as the `OpenAPI.php` class which automatically documents the previously generated routes. + +You will then be instructed to: + +* Register the `ConfigProvider` classes by adding `Api\Book\ConfigProvider::class` and `Core\Book\ConfigProvider::class` to `config/config.php` +* Register the new `Book` namespace by adding `"Api\\Book\\": "src/Book/src/"` and `"Core\\Book\\": "src/Core/src/Book/src/"` to `composer.json` under the `autoload.psr-4` key. + * After registering the namespace, run the following command to regenerate the autoloaded files, as notified by `dot-maker`: + +```shell +composer dump +``` + +* `dot-maker` will by default prompt you to generate the migrations for the new entity, but for this tutorial we will run this after updating the generated entity. + +The next step is filling in the required logic for the proposed flow of this module. +While `dot-maker` does also include common logic in the relevant files, the tutorial adds custom functionality. +As such, the following section will go over the files that require changes. + +* `src/Core/src/Book/src/Entity/Book.php` + +To keep things simple in this tutorial, our book will have three properties: `name`, `author` and `releaseDate`. +Add the three properties and their getters and setters, while making sure to update the generated constructor method. + +```php +setName($name); + $this->setAuthor($author); + $this->setReleaseDate($releaseDate); + } + + public function getName(): string + { + return $this->name; + } + + public function setName(string $name): self + { + $this->name = $name; + + return $this; + } + + public function getAuthor(): string + { + return $this->author; + } + + public function setAuthor(string $author): self + { + $this->author = $author; + + return $this; + } + + public function getReleaseDate(): DateTimeImmutable + { + return $this->releaseDate; + } + + public function setReleaseDate(DateTimeImmutable $releaseDate): self + { + $this->releaseDate = $releaseDate; + + return $this; + } + + public function getArrayCopy(): array + { + return [ + 'uuid' => $this->getUuid()->toString(), + 'name' => $this->getName(), + 'author' => $this->getAuthor(), + 'releaseDate' => $this->getReleaseDate(), + ]; + } +} + +``` + +The `BookService` class will require minor modifications for the `getBooks()` and `saveBook()` methods, to add the custom properties added in the previous step. +The class should look like the following after updating the methods. + +* `src/Book/src/Service/BookService.php` + +```php +bookRepository; + } + + public function deleteBook( + Book $book, + ): void { + $this->bookRepository->deleteResource($book); + } + + /** + * @param array $params + */ + public function getBooks( + array $params, + ): QueryBuilder { + $filters = $params['filters'] ?? []; + $params = Paginator::getParams($params, 'book.created'); + + $sortableColumns = [ + 'book.name', + 'book.author', + 'book.releaseDate', + 'book.created', + ]; + if (! in_array($params['sort'], $sortableColumns, true)) { + $params['sort'] = 'book.created'; + } + + return $this->bookRepository->getBooks($params, $filters); + } + + /** + * @param array $data + */ + public function saveBook( + array $data, + ?Book $book = null, + ): Book { + if (! $book instanceof Book) { + $book = new Book( + $data['name'], + $data['author'], + new DateTimeImmutable($data['releaseDate']) + ); + } + + $this->bookRepository->saveResource($book); + + return $book; + } +} + +``` + +When creating or updating a book, we will need some validators, so we will create input filters that will be used to validate the data received in the request. + +By creating a `module` with `dot-maker`, separate inputs will not be created. +However, you can still generate them as using these steps: + +* Run the following to start adding `Input` classes: + +```shell +./vendor/bin/dot-maker input +``` + +* When prompted, enter the names `Author`, `Name` and `ReleaseDate` one by one to generate the classes. +* The resulting `AuthorInput.php`, `NameInput.php` and `ReleaseDateInput.php` classes require no further changes for the tutorial use case. + +The module creation process has generated the parent input filter `CreateBookInputFilter.php` with an empty constructor. +Now we add all the inputs together in the parent input filter's `__construct`, as below: + +* `src/Book/src/InputFilter/CreateBookInputFilter.php` + +```php +add(new NameInput('name')); + $this->add(new AuthorInput('author')); + $this->add(new ReleaseDateInput('releaseDate')); + } +} + +``` + +We create separate `Input` files to demonstrate their reusability and obtain a clean `CreateBookInputFilter` but you could have all the inputs created directly in the `CreateBookInputFilter` like this: + +> Note that `dot-maker` will not generate inputs in the constructor, so the following are to be added by hand **if** going for this approach. + +```php +$nameInput = new Input('name'); +$nameInput->setRequired(true); + +$nameInput->getFilterChain() + ->attachByName(StringTrim::class) + ->attachByName(StripTags::class); + +$nameInput->getValidatorChain() + ->attachByName(NotEmpty::class, [ + 'message' => Message::VALIDATOR_REQUIRED_FIELD, + ], true); + +$this->add($nameInput); + +$authorInput = new Input('author'); +$authorInput->setRequired(true); + +$authorInput->getFilterChain() + ->attachByName(StringTrim::class) + ->attachByName(StripTags::class); + +$authorInput->getValidatorChain() + ->attachByName(NotEmpty::class, [ + 'message' => Message::VALIDATOR_REQUIRED_FIELD, + ], true); + +$this->add($authorInput); + +$releaseDateInput = new Input('releaseDate'); +$releaseDateInput->setRequired(true); + +$releaseDateInput->getFilterChain() + ->attachByName(StringTrim::class) + ->attachByName(StripTags::class); + +$releaseDateInput->getValidatorChain() + ->attachByName(NotEmpty::class, [ + 'message' => Message::VALIDATOR_REQUIRED_FIELD, + ], true); + +$this->add($releaseDateInput); +``` + +## Migrations + +All changes are done, so at this point the migration file can be generated to create the associated table for the `Book` entity. + +> You can check the mapping files by running: + +```shell +php ./bin/doctrine orm:validate-schema +``` + +> Generate the migration files by running: + +```shell +php ./vendor/bin/doctrine-migrations diff +``` + +This will check for differences between your entities and database structure and create migration files if necessary, in `src/Core/src/App/src/Migration`. + +To execute the migrations, run: + +```shell +php ./vendor/bin/doctrine-migrations migrate +``` + +## Update the authorization file + +We need to configure access to the newly created endpoints. +Open `config/autoload/authorization.global.php` and append the below route names to the `UserRoleEnum::Guest->value` key: + +* `book::list-books` +* `book::view-book` +* `book::create-book` + +> Make sure you read and understand the `rbac` [documentation](https://docs.dotkernel.org/dot-rbac-guard/v4/configuration/). + +## Checking endpoints + +First, we start a local server by executing: + +```shell +composer serve +``` + +If we did everything as planned, we should be able to create a new book by executing the below command: + +```shell +curl -X POST http://0.0.0.0:8080/book \ + -H "Content-Type: application/json" \ + -d '{"name": "test", "author": "author name", "releaseDate": "2025-08-21"}' +``` + +To list the books, use: + +```shell +curl http://0.0.0.0:8080/book +``` + +To fetch a book, `curl` one of the links found in the output of the **list books** command, under `_embedded` . `books` . * . `_links` . `self` . `href`. + +The link should have the following format: + +```shell +curl http://0.0.0.0:8080/book/{uuid} +``` diff --git a/docs/book/v7/tutorials/create-book-module.md b/docs/book/v7/tutorials/create-book-module.md new file mode 100644 index 00000000..4892194d --- /dev/null +++ b/docs/book/v7/tutorials/create-book-module.md @@ -0,0 +1,834 @@ +# Implementing a book module in Dotkernel API + +## Folder and files structure + +The below files structure is what we will have at the end of this tutorial and is just an example; you can have multiple components such as event listeners, wrappers, etc. + +```markdown +. +└── src/ + ├── Book/ + │ └── src/ + │ ├── Collection/ + │ │ └── BookCollection.php + │ ├── Handler/ + │ │ ├── GetBookCollectionHandler.php + │ │ ├── GetBookResourceHandler.php + │ │ └── PostBookResourceHandler.php + │ ├── InputFilter/ + │ │ ├── Input/ + │ │ │ ├── AuthorInput.php + │ │ │ ├── NameInput.php + │ │ │ └── ReleaseDateInput.php + │ │ └── CreateBookInputFilter.php + │ ├── Service/ + │ │ ├── BookService.php + │ │ └── BookServiceInterface.php + │ ├── ConfigProvider.php + │ └── RoutesDelegator.php + └── Core/ + └── src/ + └── Book/ + └── src/ + ├──Entity/ + │ └──Book.php + ├──Repository/ + │ └──BookRepository.php + └── ConfigProvider.php +``` + +* `src/Book/src/Collection/BookCollection.php` – a collection refers to a container for a group of related objects, typically used to manage sets of related entities fetched from a database +* `src/Book/src/Handler/GetBookCollectionHandler.php` – handler that reflects the GET action for the BookCollection class +* `src/Book/src/Handler/GetBookResourceHandler.php` – handler that reflects the GET action for the Book entity +* `src/Book/src/Handler/PostBookResourceHandler.php` – handler that reflects the POST action for the Book entity +* `src/Book/src/InputFilter/Input/*` – input filters and validator configurations +* `src/Book/src/InputFilter/CreateBookInputFilter.php` – input filters and validators +* `src/Book/src/Service/BookService.php` – is a class or component responsible for performing a specific task or providing functionality to other parts of the application +* `src/Book/src/Service/BookServiceInterface.php` – interface that reflects the publicly available methods in `BookService` +* `src/Book/src/ConfigProvider.php` – is a class that provides configuration for various aspects of the framework or application +* `src/Book/src/RoutesDelegator.php` – a RoutesDelegator is a delegator factory responsible for configuring routing middleware based on routing configuration provided by the application +* `src/Core/src/Book/src/Entity/Book.php` – an entity refers to a PHP class that represents a persistent object or data structure +* `src/Core/src/Book/src/Repository/BookRepository.php` – a repository is a class responsible for querying and retrieving entities from the database +* `src/Core/src/Book/src/ConfigProvider.php` – is a class that provides configuration for Doctrine ORM + +## File creation and contents + +In `src` and `src/Core/src` folders we will create one `Book` folder and in those we will create the `src` folder. +So the final structure will be like this: `src/Book/src` and `src/Core/src/Book/src`. + +* `src/Book/src/Collection/BookCollection.php` + +```php +setName($name); + $this->setAuthor($author); + $this->setReleaseDate($releaseDate); + } + + public function getName(): string + { + return $this->name; + } + + public function setName(string $name): self + { + $this->name = $name; + + return $this; + } + + public function getAuthor(): string + { + return $this->author; + } + + public function setAuthor(string $author): self + { + $this->author = $author; + + return $this; + } + + public function getReleaseDate(): DateTimeImmutable + { + return $this->releaseDate; + } + + public function setReleaseDate(DateTimeImmutable $releaseDate): self + { + $this->releaseDate = $releaseDate; + + return $this; + } + + public function getArrayCopy(): array + { + return [ + 'uuid' => $this->getUuid()->toString(), + 'name' => $this->getName(), + 'author' => $this->getAuthor(), + 'releaseDate' => $this->getReleaseDate(), + ]; + } +} + +``` + +* `src/Core/src/Book/src/Repository/BookRepository.php` + +```php +getQueryBuilder() + ->select('book') + ->from(Book::class, 'book') + ->orderBy($params['sort'], $params['dir']) + ->setFirstResult($params['offset']) + ->setMaxResults($params['limit']); + } +} + +``` + +* `src/Book/src/Service/BookServiceInterface.php` + +```php +bookRepository; + } + + /** + * @throws Exception + */ + public function saveBook(array $data): Book + { + $book = new Book( + $data['name'], + $data['author'], + new DateTimeImmutable($data['releaseDate']) + ); + + $this->bookRepository->saveResource($book); + + return $book; + } + + public function getBooks(array $params = []): QueryBuilder + { + $filters = $params['filters'] ?? []; + $params = Paginator::getParams($filters, 'book.created'); + + $sortableColumns = [ + 'book.name', + 'book.author', + 'book.releaseDate', + 'book.created', + ]; + + if (! in_array($params['sort'], $sortableColumns, true)) { + $params['sort'] = 'book.created'; + } + + return $this->bookRepository->getBooks($params, $filters); + } +} + +``` + +When creating or updating a book, we will need some validators, so we will create input filters that will be used to validate the data received in the request + +* `src/Book/src/InputFilter/Input/AuthorInput.php` + +```php +setRequired($isRequired); + + $this->getFilterChain() + ->attachByName(StringTrim::class) + ->attachByName(StripTags::class); + + $this->getValidatorChain() + ->attachByName(NotEmpty::class, [ + 'message' => Message::VALIDATOR_REQUIRED_FIELD, + ], true); + } +} + +``` + +* `src/Book/src/InputFilter/Input/NameInput.php` + +```php +setRequired($isRequired); + + $this->getFilterChain() + ->attachByName(StringTrim::class) + ->attachByName(StripTags::class); + + $this->getValidatorChain() + ->attachByName(NotEmpty::class, [ + 'message' => Message::VALIDATOR_REQUIRED_FIELD, + ], true); + } +} + +``` + +* `src/Book/src/InputFilter/Input/ReleaseDateInput.php` + +```php +setRequired($isRequired); + + $this->getFilterChain() + ->attachByName(StringTrim::class) + ->attachByName(StripTags::class); + + $this->getValidatorChain() + ->attachByName(Date::class, [ + 'message' => Message::invalidValue('releaseDate'), + ], true); + } +} + +``` + +Now we add all the inputs together in a parent input filter. + +* `src/Book/src/InputFilter/CreateBookInputFilter.php` + +```php +add(new NameInput('name')); + $this->add(new AuthorInput('author')); + $this->add(new ReleaseDateInput('releaseDate')); + } +} + +``` + +We create separate `Input` files to demonstrate their reusability and obtain a clean `CreateBookInputFilter` but you could have all the inputs created directly in the `CreateBookInputFilter` like this: + +```php +$nameInput = new Input('name'); +$nameInput->setRequired(true); + +$nameInput->getFilterChain() + ->attachByName(StringTrim::class) + ->attachByName(StripTags::class); + +$nameInput->getValidatorChain() + ->attachByName(NotEmpty::class, [ + 'message' => Message::VALIDATOR_REQUIRED_FIELD, + ], true); + +$this->add($nameInput); + +$authorInput = new Input('author'); +$authorInput->setRequired(true); + +$authorInput->getFilterChain() + ->attachByName(StringTrim::class) + ->attachByName(StripTags::class); + +$authorInput->getValidatorChain() + ->attachByName(NotEmpty::class, [ + 'message' => Message::VALIDATOR_REQUIRED_FIELD, + ], true); + +$this->add($authorInput); + +$releaseDateInput = new Input('releaseDate'); +$releaseDateInput->setRequired(true); + +$releaseDateInput->getFilterChain() + ->attachByName(StringTrim::class) + ->attachByName(StripTags::class); + +$releaseDateInput->getValidatorChain() + ->attachByName(NotEmpty::class, [ + 'message' => Message::VALIDATOR_REQUIRED_FIELD, + ], true); + +$this->add($releaseDateInput); +``` + +Now it's time to create the handlers. + +* `src/Book/src/Handler/GetBookCollectionHandler.php` + +```php +createResponse( + $request, + new BookCollection($this->bookService->getBooks($request->getQueryParams())) + ); + } +} + +``` + +* `src/Book/src/Handler/GetBookResourceHandler.php` + +```php +createResponse( + $request, + $request->getAttribute(Book::class) + ); + } +} + +``` + +* `src/Book/src/Handler/PostBookResourceHandler.php` + +```php +inputFilter->setData((array) $request->getParsedBody()); + if (! $this->inputFilter->isValid()) { + throw BadRequestException::create( + detail: Message::VALIDATOR_INVALID_DATA, + additional: ['errors' => $this->inputFilter->getMessages()] + ); + } + + /** @var non-empty-array $data */ + $data = (array) $this->inputFilter->getValues(); + + return $this->createdResponse($request, $this->bookService->saveBook($data)); + } +} + +``` + +In `src/Book/src` we now create the two PHP files: `RoutesDelegator.php` and `ConfigProvider.php`. + +`RoutesDelegator.php` contains all of our routes while `ConfigProvider` contains all the necessary configuration needed, so the above files work properly like dependency injection, aliases and so on. + +* `src/Book/src/ConfigProvider.php` + +```php + $this->getDependencies(), + MetadataMap::class => $this->getHalConfig(), + ]; + } + + private function getDependencies(): array + { + return [ + 'delegators' => [ + Application::class => [RoutesDelegator::class], + PostBookResourceHandler::class => [HandlerDelegatorFactory::class], + GetBookResourceHandler::class => [HandlerDelegatorFactory::class], + GetBookCollectionHandler::class => [HandlerDelegatorFactory::class], + ], + 'factories' => [ + PostBookResourceHandler::class => AttributedServiceFactory::class, + GetBookResourceHandler::class => AttributedServiceFactory::class, + GetBookCollectionHandler::class => AttributedServiceFactory::class, + BookService::class => AttributedServiceFactory::class, + ], + 'aliases' => [ + BookServiceInterface::class => BookService::class, + ], + ]; + } + + private function getHalConfig(): array + { + return [ + AppConfigProvider::getResource(Book::class, 'book::view-book'), + AppConfigProvider::getCollection(BookCollection::class, 'book::list-books', 'books'), + ]; + } +} + +``` + +* `src/Book/src/RoutesDelegator.php` + +```php +get(RouteCollectorInterface::class); + + $routeCollector->post('/book', PostBookResourceHandler::class, 'book::create-book'); + $routeCollector->get('/book/' . $uuid, GetBookResourceHandler::class, 'book::view-book'); + $routeCollector->get('/book', GetBookCollectionHandler::class, 'book::list-books'); + + return $callback(); + } +} + +``` + +In `src/Core/src/Book/src` we will create `ConfigProvider.php` where we configure Doctrine ORM. + +* `src/Core/src/Book/src/ConfigProvider.php`. + +```php + $this->getDependencies(), + 'doctrine' => $this->getDoctrineConfig(), + ]; + } + + private function getDependencies(): array + { + return [ + 'factories' => [ + BookRepository::class => AttributedRepositoryFactory::class, + ], + ]; + } + + private function getDoctrineConfig(): array + { + return [ + 'driver' => [ + 'orm_default' => [ + 'drivers' => [ + 'Core\Book\Entity' => 'BookEntities', + ], + ], + 'BookEntities' => [ + 'class' => AttributeDriver::class, + 'cache' => 'array', + 'paths' => [__DIR__ . '/Entity'], + ], + ], + ]; + } +} + +``` + +### Registering the module + +* register the module config by adding `Api\Book\ConfigProvider::class,` and `Core\Book\ConfigProvider::class,` in `config/config.php` under the `Api\User\ConfigProvider::class,` +* register the namespace by adding this line `"Api\\Book\\": "src/Book/src/"` and `"Core\\Book\\": "src/Core/src/Book/src/"`, in `composer.json` under the `autoload`.`psr-4` key +* update Composer autoloader by running the command: + +```shell +composer dump-autoload +``` + +That's it. The module is now registered. + +We need to configure access to the newly created endpoints. +Open `config/autoload/authorization.global.php` and append the below route names to the `UserRoleEnum::Guest->value` key: + +* `book::list-books` +* `book::view-book` +* `book::create-book` + +> Make sure you read and understand the rbac [documentation](https://docs.dotkernel.org/dot-rbac-guard/v4/configuration/). + +## Migrations + +We created the `Book` entity, but we didn't create the associated table for it. + +> You can check the mapping files by running: + +```shell +php ./bin/doctrine orm:validate-schema +``` + +Doctrine can handle the table creation, run the following command: + +```shell +php ./vendor/bin/doctrine-migrations diff +``` + +This will check for differences between your entities and database structure and create migration files if necessary, in `src/Core/src/App/src/Migration`. + +To execute the migrations, run: + +```shell +php ./vendor/bin/doctrine-migrations migrate +``` + +## Checking endpoints + +First, we start a local server by executing: + +```shell +composer serve +``` + +If we did everything as planned, we should be able to create a new book by executing the below command: + +```shell +curl -X POST http://0.0.0.0:8080/book + -H "Content-Type: application/json" + -d '{"name": "test", "author": "author name", "releaseDate": "2023-03-03"}' +``` + +To list the books, use: + +```shell +curl http://0.0.0.0:8080/book +``` + +To fetch a book, `curl` one of the links found in the output of the **list books** command, under `_embedded` . `books` . * . `_links` . `self` . `href`. + +The link should have the following format: + +```shell +curl http://0.0.0.0:8080/book/{uuid} +``` diff --git a/docs/book/v7/tutorials/find-user-by-identity.md b/docs/book/v7/tutorials/find-user-by-identity.md new file mode 100644 index 00000000..fd822e76 --- /dev/null +++ b/docs/book/v7/tutorials/find-user-by-identity.md @@ -0,0 +1,212 @@ +# A practical example: Find a user by identity + +## Our goal + +Create a new endpoint that fetches a user record by its identity column. + +We already have an endpoint that retrieves a user based on their UUID, so we can review it and create something similar. + +## What we have + +Let's print out all available endpoints using : + +```shell +php ./bin/cli.php route:list +``` + +This command will list all available endpoints, which looks like this: + +```text ++--------+---------------------------------+--------------------------------+ +| Method | Name | Path | ++--------+---------------------------------+--------------------------------+ +| POST | account.activate.request | /account/activate | +| PATCH | account.activate | /account/activate/{hash} | +| PATCH | account.modify-password | /account/reset-password/{hash} | +............................................................................. +............................................................................. +............................................................................. +| GET | user.my-avatar.view | /user/my-avatar | +| GET | user.role.list | /user/role | +| GET | user.role.view | /user/role/{uuid} | +| PATCH | user.update | /user/{uuid} | +| GET | user.view | /user/{uuid} | ++--------+---------------------------------+--------------------------------+ +``` + +### Note + +> **The above output is just an example.** +> +> More info about listing available endpoints can be found in `../commands/display-available-endpoints.md`. + +The endpoint we're focusing on is the last one, `user.view`, so let's take a closer look at its functionality. + +If we search for the route name `user.view` we will find its definition in the `src/User/src/RoutesDelegator.php` class, where all user-related endpoints are found. + +```php +$app->get('/user/' . $uuid, UserHandler::class, 'user.view'); +``` + +Our route points to `get` method from `UserHandler` so let's navigate to that method. + +```php +public function get(ServerRequestInterface $request): ResponseInterface +{ + $user = $this->userService->findOneBy(['uuid' => $request->getAttribute('uuid')]); + + return $this->createResponse($request, $user); +} +``` + +As we can see, the method will query the database for the user based on its uuid taken from the endpoint. + +We now have an understanding of how things work, and we can start to implement our own endpoint. + +### Implementation + +We need to create a new handler that will process our request, we can call it `IdentityHandler`. + +Create a new PHP class called `IdentityHandler.php` in `src/User/src/Handler` folder. + +```php +getAttribute('identity'); + if (empty($identity)) { + throw (new BadRequestException())->setMessages([sprintf(Message::INVALID_VALUE, 'identity')]); + } + + $user = $this->userService->findByIdentity($identity); + if (! $user instanceof User) { + throw new NotFoundException(Message::USER_NOT_FOUND); + } + + return $this->createResponse($request, $user); + } +} +``` + +Our handler is very similar to the existing one, with some extra steps: + +* We store the identity from the request in the `$identity` variable for later use. +* If the identity is empty we throw a `BadRequestException` with an appropriate message. +* If we can't find the user in the database, we throw an `NotFoundException`. +* If the record is found, we generate and return the response. + +The next step is to register the new handler. +To do this, go to `src/User/src/ConfigProvider.php`. +In the `getDependencies()` method under the `factories` key add `IdentityHandler::class => AttributedServiceFactory::class,` + +Next, create the route in `src/User/src/RoutesDelegator.php`: + +```php + $app->get( + '/user/{identity}', + IdentityHandler::class, + 'user.view.identity' + ); +``` + +### Note + +> Make sure to register the endpoint as the last one to not shadow existing endpoints. + +The last step is to set permissions on the newly created route. + +Go to `config/autoload/authorization.global.php` and add our route name (`user.view.identity`) under the `UserRole::ROLE_GUEST` key. +This will give access to every user, including guests, to view other accounts (for the sake of simplicity). + +### Writing tests + +Because every new piece of code should be tested, we will write some tests for this endpoint also. + +In the `test/Functional` folder create a new php class `IdentityTest.php`: + +```php +get('/user/'); + + $this->assertResponseNotFound($response); + } + + public function testInvalidIdentityReturnsNotFound(): void + { + $response = $this->get('/user/invalid_identity'); + $messages = json_decode($response->getBody()->getContents(), true); + + $this->assertResponseNotFound($response); + $this->assertNotEmpty($messages); + $this->assertIsArray($messages); + $this->assertNotEmpty($messages['error']['messages'][0]); + $this->assertIsString($messages['error']['messages'][0]); + $this->assertSame(Message::USER_NOT_FOUND, $messages['error']['messages'][0]); + } + + public function testValidIdentityReturnsUser(): void + { + $this->createUser([ + 'identity' => 'valid_user', + ]); + + $response = $this->get('/user/valid_user'); + + $this->assertResponseOk($response); + $user = json_decode($response->getBody()->getContents(), true); + + $this->assertSame('valid_user', $user['identity']); + } +} +``` + +Planning and coding a new feature can be challenging at times, but reviewing our existing code or tutorials can serve as a source of inspiration. diff --git a/docs/book/v7/tutorials/token-authentication.md b/docs/book/v7/tutorials/token-authentication.md new file mode 100644 index 00000000..daabecae --- /dev/null +++ b/docs/book/v7/tutorials/token-authentication.md @@ -0,0 +1,361 @@ +# Token authentication + +## What is token authentication? + +Token authentication means making a request to an API endpoint while also sending a special header that contains an access token. +The access token was previously generated by (usually) the same API as the one you are sending requests to, and it consists of an alphanumeric string. + +## How does it work? + +To protect specific resources, clients need to be authenticated with user/admin roles. +These roles are identified from the access token sent via the `Authorization` header. + +When Dotkernel API receives a request, it tries to read the access token. + +If it does not find an access token, client has `guest` role: + +- if the requested endpoint needs no authentication, the requested resource is returned +- else, a `403 Forbidden` response is returned + +Else, client's account is identified and client has `admin`/`user` role (the one assigned in their account) + +- if the requested endpoint is accessible to the client, the requested resource is returned +- else, a `403 Forbidden` response is returned + +Dotkernel API provides out-of-the-box both an `admin` and a `user` account. + +### Credentials + +The admin account with **role** set to both `superuser` and `admin` with the following credentials: + +- **identity**: `admin` +- **password**: `dotadmin` + +The user account with **role** set to both `user` and `guest` with the following credentials: + +- **identify**: `test@dotkernel.com` +- **password**: `dotkernel` + +## Flow + +- client sends API request with credentials +- API returns a JSON object containing a new access and refresh token +- client sends API request using `Authentication` header containing the previously generated access token +- API returns requested resource + +### Note + +> The first two steps need to be executed only once. +> Access token should be stored and reused for all upcoming requests. +> Refresh token should be stored and used to refresh the expired access token. + +For a better overview of the flow, see the below image: + +![Token authentication flow](https://docs.dotkernel.org/img/api/v7/token-authentication.png) + +## Generate admin access token + +Send a `POST` request to the `/security/generate-token` endpoint with `Content-Type` header set to `application/json`. + +Set the request body to: + +```json +{ + "grant_type": "password", + "client_id": "admin", + "client_secret": "admin", + "scope": "api", + "username": "", + "password": "" +} +``` + +### Note + +> Replace `` with your admin account's `identity` and `` with your admin account's `password`. +> Both fields come from table `admin`. + +### Test using curl + +Execute the below command: + +```shell +curl --location 'https://api.dotkernel.net/security/generate-token' \ +--header 'Content-Type: application/json' \ +--data-raw '{ + "grant_type": "password", + "client_id": "admin", + "client_secret": "admin", + "scope": "api", + "username": "admin", + "password": "dotkernel" +}' +``` + +## Generate a user access token + +Send a `POST` request to the `/security/generate-token` endpoint with `Content-Type` header set to `application/json`. + +Set the request body to: + +```json +{ + "grant_type": "password", + "client_id": "frontend", + "client_secret": "frontend", + "scope": "api", + "username": "", + "password": "" +} +``` + +### Note + +> Replace `` with your user account's `identity` and `` with your user account's `password`. +> Both fields come from table `user`. + +### Test using curl + +Execute the below command: + +```shell +curl --location 'https://api.dotkernel.net/security/generate-token' \ +--header 'Content-Type: application/json' \ +--data-raw '{ + "grant_type": "password", + "client_id": "frontend", + "client_secret": "frontend", + "scope": "api", + "username": "test@dotkernel.com", + "password": "dotkernel" +}' +``` + +### Response on success + +You should see a `200 OK` response with the following JSON body: + +```json +{ + "token_type": "Bearer", + "expires_in": 86400, + "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.e...wuE39ON1mS5mnTKfA_dSpSWxOmNQdny_AKIbc1qZjMfS24qSUV8HIoOw", + "refresh_token": "def502005a035c8dfe5456d27e85069813a4f8...0b844e843cd62865662a0e723165752dfd7012491502d3d819c2a61d" +} +``` + +Field description: + +- `token_type`: token type to be set when sending the `Authorization` header (example: `Authorization: Bearer eyJ0e...`) +- `expires_in`: access token lifetime (modify in: `config/autoload/local.php` > `authentication`.`access_token_expire`) +- `access_token`: generated access token (store it for later use) +- `refresh_token`: generated refresh token (store it for regenerating expired access token) + +### Response on failure + +You should see a `400 Bad Request` response with the following JSON body: + +```json +{ + "error": "Invalid credentials.", + "error_description": "Invalid credentials.", + "message": "Invalid credentials." +} +``` + +## Refresh admin access token + +Send a `POST` request to the `/security/refresh-token` endpoint with `Content-Type` header set to `application/json`. + +Set the request body to: + +```json +{ + "grant_type": "refresh_token", + "client_id": "admin", + "client_secret": "admin", + "scope": "api", + "refresh_token": "" +} +``` + +### Test using curl + +Execute the below command: + +```shell +curl --location 'https://api.dotkernel.net/security/refresh-token' \ +--header 'Content-Type: application/json' \ +--data-raw '{ + "grant_type": "refresh_token", + "client_id": "admin", + "client_secret": "admin", + "scope": "api", + "refresh_token": "" +}' +``` + +### Note + +> Make sure you replace `` with the refresh token generated with the access token. + +## Refresh user access token + +Send a `POST` request to the `/security/refresh-token` endpoint with `Content-Type` header set to `application/json`. + +Set the request body to: + +```json +{ + "grant_type": "refresh_token", + "client_id": "frontend", + "client_secret": "frontend", + "scope": "api", + "refresh_token": "" +} +``` + +### Test using curl + +Execute the below command: + +```shell +curl --location 'https://api.dotkernel.net/security/refresh-token' \ +--header 'Content-Type: application/json' \ +--data-raw '{ + "grant_type": "refresh_token", + "client_id": "frontend", + "client_secret": "frontend", + "scope": "api", + "refresh_token": "" +}' +``` + +### Note + +> Make sure you replace `` with the refresh token generated with the access token. + +### Response on success + +You should see a `200 OK` response with the following JSON body: + +```json +{ + "token_type": "Bearer", + "expires_in": 86400, + "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.e...wuE39ON1mS5mnTKfA_dSpSWxOmNQdny_AKIbc1qZjMfS24qSUV8HIoOw", + "refresh_token": "def502005a035c8dfe5456d27e85069813a4f8...0b844e843cd62865662a0e723165752dfd7012491502d3d819c2a61d" +} +``` + +Field description: + +- `token_type`: token type to be set when sending the `Authorization` header (example: `Authorization: Bearer eyJ0e...`) +- `expires_in`: access token lifetime (change here: `config/autoload/local.php` `authentication`->`access_token_expire`) +- `access_token`: generated access token (store it for later use) +- `refresh_token`: generated refresh token (store it for regenerating expired access token) + +### Response on failure + +You should see a `401 Unauthorized` response with the following JSON body: + +```json +{ + "error": "invalid_request", + "error_description": "The refresh token is invalid.", + "hint": "Cannot decrypt the refresh token", + "message": "The refresh token is invalid." +} +``` + +## Test admin authentication flow + +### Step 1: Fail to fetch protected API content + +Try to view your admin account by executing: + +```shell +curl --location 'https://api.dotkernel.net/admin/my-account' +``` + +You should get a `403 Forbidden` JSON response. + +### Step 2: Generate an access token + +Generate an admin access token by executing: + +```shell +curl --location 'https://api.dotkernel.net/security/generate-token' \ +--header 'Content-Type: application/json' \ +--data-raw '{ + "grant_type": "password", + "client_id": "admin", + "client_secret": "admin", + "scope": "api", + "username": "admin", + "password": "dotkernel" +}' +``` + +You should get a `200 OK` JSON response. + +Store the value of `access_token` for later use. + +### Step 3: Successfully fetch protected API content + +Try again viewing your admin account by executing: + +```shell +curl --location 'https://api.dotkernel.net/admin/my-account' \ +--header 'Authorization: Bearer ' +``` + +Replace `` with the previously stored access token. + +You should get a `200 OK` JSON response with the requested resource in the body. + +## Test user authentication flow + +### Step 1: Fail to fetch protected API content + +Try to view your admin account by executing: + +```shell +curl --location 'https://api.dotkernel.net/user/my-account' +``` + +You should get a `403 Forbidden` JSON response. + +### Step 2: Generate an access token + +Generate an admin access token by executing: + +```shell +curl --location 'https://api.dotkernel.net/security/generate-token' \ +--header 'Content-Type: application/json' \ +--data-raw '{ + "grant_type": "password", + "client_id": "frontend", + "client_secret": "frontend", + "scope": "api", + "username": "test@dotkernel.com", + "password": "dotkernel" +}' +``` + +You should get a `200 OK` JSON response. + +Store the value of `access_token` for later use. + +### Step 3: Successfully fetch protected API content + +Try again viewing your admin account by executing: + +```shell +curl --location 'https://api.dotkernel.net/user/my-account' \ +--header 'Authorization: Bearer ' +``` + +Replace `` with the previously stored access token. + +You should get a `200 OK` JSON response with the requested resource in the body. diff --git a/docs/book/v7/upgrading/UPGRADE-6.0.md b/docs/book/v7/upgrading/UPGRADE-6.0.md new file mode 100644 index 00000000..344eed42 --- /dev/null +++ b/docs/book/v7/upgrading/UPGRADE-6.0.md @@ -0,0 +1,24 @@ +# Upgrading from 5.x to 6.0 + +> You can find a complete list in [Changelog](https://github.com/dotkernel/api/blob/7.0/CHANGELOG.md) + +* Move common logic to Core module [https://github.com/dotkernel/api/pull/358](https://github.com/dotkernel/api/pull/358) +* Refactored Handlers [https://github.com/dotkernel/api/pull/385](https://github.com/dotkernel/api/pull/385) +* Inject `InputFilters` in handlers [https://github.com/dotkernel/api/pull/389](https://github.com/dotkernel/api/pull/389) +* Implemented route grouping [https://github.com/dotkernel/api/pull/391](https://github.com/dotkernel/api/pull/391) +* Service refactoring [https://github.com/dotkernel/api/pull/396](https://github.com/dotkernel/api/pull/396) +* Autogenerate `OAuth2` keys when cloning the project [https://github.com/dotkernel/api/pull/398](https://github.com/dotkernel/api/pull/398) +* Refresh Postman documentation [https://github.com/dotkernel/api/pull/400](https://github.com/dotkernel/api/pull/400) +* Merge `Admin.Core` into `API.Core` [https://github.com/dotkernel/api/pull/401](https://github.com/dotkernel/api/pull/401) +* Implemented `mezzio/mezzio-problem-details` [https://github.com/dotkernel/api/pull/402](https://github.com/dotkernel/api/pull/402) +* Update pre-run.sh [https://github.com/dotkernel/api/pull/404](https://github.com/dotkernel/api/pull/404) +* Update `GetIndexResourceHandler.php` [https://github.com/dotkernel/api/pull/408](https://github.com/dotkernel/api/pull/408) +* Update `local.php.dist` [https://github.com/dotkernel/api/pull/409](https://github.com/dotkernel/api/pull/409) +* Fixed error handling [https://github.com/dotkernel/api/pull/412](https://github.com/dotkernel/api/pull/412) +* Implemented `ResourceProviderMiddleware` and added `ResourceGuardInterface` [https://github.com/dotkernel/api/pull/403](https://github.com/dotkernel/api/pull/403) +* Updated logic in `ContentNegotiationMiddleware` [https://github.com/dotkernel/api/pull/413](https://github.com/dotkernel/api/pull/413) +* `AuthenticationMiddleware` no longer extends `AuthenticationMiddleware` from `mezzio/mezzio-authentication` [https://github.com/dotkernel/api/pull/418](https://github.com/dotkernel/api/pull/418) +* Update `qodana_code_quality.yml` [https://github.com/dotkernel/api/pull/416](https://github.com/dotkernel/api/pull/416) +* Replaced `Twig` with custom templating solution [https://github.com/dotkernel/api/pull/419](https://github.com/dotkernel/api/pull/419) +* Increased `PHPStan` level to 8 [https://github.com/dotkernel/api/pull/421](https://github.com/dotkernel/api/pull/421) +* Split the `/security/token` endpoint into two separate endpoints [https://github.com/dotkernel/api/pull/423](https://github.com/dotkernel/api/pull/423) diff --git a/docs/book/v7/upgrading/UPGRADE-7.0.md b/docs/book/v7/upgrading/UPGRADE-7.0.md new file mode 100644 index 00000000..4c5f0580 --- /dev/null +++ b/docs/book/v7/upgrading/UPGRADE-7.0.md @@ -0,0 +1,9 @@ +# Upgrading from 6.x to 7.0 + +> You can find a complete list in [Changelog](https://github.com/dotkernel/api/blob/7.0/CHANGELOG.md) + +* Use native UUIDs in database via `ramsey/uuid` [https://github.com/dotkernel/api/pull/456](https://github.com/dotkernel/api/pull/456) +* updated readme, oss [https://github.com/dotkernel/api/pull/461](https://github.com/dotkernel/api/pull/461) +* PostgreSQL implementation [https://github.com/dotkernel/api/pull/462](https://github.com/dotkernel/api/pull/462) +* Remove `MethodDeprecation` implementation [https://github.com/dotkernel/api/pull/470](https://github.com/dotkernel/api/pull/470) +* Clarify instructions regarding multiple connections in `config/autoload/local.php.dist` [https://github.com/dotkernel/api/pull/472](https://github.com/dotkernel/api/pull/472) diff --git a/docs/book/v7/upgrading/upgrading.md b/docs/book/v7/upgrading/upgrading.md new file mode 100644 index 00000000..ba17e9a1 --- /dev/null +++ b/docs/book/v7/upgrading/upgrading.md @@ -0,0 +1,19 @@ +# Upgrades + +Dotkernel API does not provide an automatic upgrade path. +Instead, the recommended procedure is to manually implement each modification listed in [releases](https://github.com/dotkernel/api/releases). +Additionally, release info can also be accessed as an [RSS](https://github.com/dotkernel/api/releases.atom) feed. + +## Upgrade procedure + +Once you clone Dotkernel API, you will find a [CHANGELOG.md](https://github.com/dotkernel/api/blob/5.0/CHANGELOG.md) file in the root of the project. +This file contains a list of already implemented features in reverse chronological order. +You can use this file to track the version of your copy of Dotkernel API. + +For each new release you need to implement the modifications from its pull requests in your project. +It is recommended to copy the release info into your project's CHANGELOG.md file. +This allows you to track your API's version and keep your project up to date with future releases. + +## Version to version upgrading + +Starting from [version 5.3](UPGRADE-6.0.md) the upgrading procedure is detailed version to version. diff --git a/mkdocs.yml b/mkdocs.yml index c7124f96..12c8a9a8 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -2,13 +2,79 @@ docs_dir: docs/book site_dir: docs/html extra: project: API - current_version: v6 + current_version: v7 versions: - - v4 - - v5 + - v7 - v6 + - v5 + - v4 nav: - Home: index.md + - v7: + - Introduction: v7/introduction/introduction.md + - Overview: + - "Server Requirements": v7/introduction/server-requirements.md + - "File Structure": v7/introduction/file-structure.md + - "Packages": v7/introduction/packages.md + - "PSRs": v7/introduction/psr.md + - Installation: + - "Getting Started": v7/installation/getting-started.md + - "Composer": v7/installation/composer.md + - "Configuration Files": v7/installation/configuration-files.md + - "Doctrine ORM": v7/installation/doctrine-orm.md + - "Test the Installation": v7/installation/test-the-installation.md + - "FAQ": v7/installation/faq.md + - Upgrading: + - "Upgrade procedure": v7/upgrading/upgrading.md + - "Upgrading 6.x to 7.0": v7/upgrading/UPGRADE-7.0.md + - "Upgrading 5.x to 6.0": v7/upgrading/UPGRADE-6.0.md + - Flow: + - "Middleware Flow": v7/flow/middleware-flow.md + - "Default Library Flow": v7/flow/default-library-flow.md + - "Library Flow for Email": v7/flow/library-flow-for-email.md + - Core Features: + - "Authentication": v7/core-features/authentication.md + - "Authorization": v7/core-features/authorization.md + - "Content Validation": v7/core-features/content-validation.md + - "Exceptions": v7/core-features/exceptions.md + - "Dependency Injection": v7/core-features/dependency-injection.md + - "Error reporting": v7/core-features/error-reporting.md + - "Rendering and Sending emails": v7/core-features/rendering-and-sending-emails.md + - Extended features: + - "Core and App": v7/extended-features/core-and-app.md + - "New Handler Structure": v7/extended-features/handler-structure.md + - "Route Grouping": v7/extended-features/route-grouping.md + - "Problem Details": v7/extended-features/problem-details.md + - "Injectable Input Filters": v7/extended-features/injectable-input-filters.md + - Commands: + - "Create admin account": v7/commands/create-admin-account.md + - "Generate database migrations": v7/commands/generate-database-migrations.md + - "Display available endpoints": v7/commands/display-available-endpoints.md + - "Generate tokens": v7/commands/generate-tokens.md + - Tutorials: + - "Setting up CORS": v7/tutorials/cors.md + - "Creating a book module": v7/tutorials/create-book-module.md + - "Creating a book module using DotMaker": v7/tutorials/create-book-module-via-dot-maker.md + - "Token authentication": v7/tutorials/token-authentication.md + - "API Evolution": v7/tutorials/api-evolution.md + - "Find user by identity": v7/tutorials/find-user-by-identity.md + - Transition from API Tools: + - "Laminas API Tools vs Dotkernel API": v7/transition-from-api-tools/api-tools-vs-dotkernel-api.md + - "Transition Approach": v7/transition-from-api-tools/transition-approach.md + - "Discovery Phase": v7/transition-from-api-tools/discovery-phase.md + - OpenAPI: + - "Introduction": v7/openapi/introduction.md + - "Initialized Components": v7/openapi/initialized-components.md + - "Write Documentation": v7/openapi/write-documentation.md + - "Generate Documentation": v7/openapi/generate-documentation.md + - "Render Documentation": v7/openapi/render-documentation.md + - "Use Documentation": v7/openapi/use-documentation.md + - "Getting Help": v7/openapi/getting-help.md + - Basic Security: + - "Basic Security": v7/security/basic-security.md + - "OAuth2 Security": v7/security/oauth2-security.md + - Reference: + - "Anonymize Accounts": v7/reference/account-anonymization.md - v6: - Introduction: v6/introduction/introduction.md - Overview: