diff --git a/LICENSE.md b/LICENSE.md index 6ca0aac8..7ca70e08 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2024 DotKernel (https://www.dotkernel.com) +Copyright (c) 2024 Dotkernel (https://www.dotkernel.com) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the diff --git a/README.md b/README.md index d2831812..bb8d7712 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -# DotKernel API +# Dotkernel API -DotKernel API is a PHP skeleton app for building REST APIs using [Laminas](https://github.com/laminas) and [Mezzio](https://github.com/mezzio) components and implements standards like PSR-3, PSR-4, PSR-7, PSR-11 and PSR-15. +Dotkernel API is a PHP skeleton app for building REST APIs using [Laminas](https://github.com/laminas) and [Mezzio](https://github.com/mezzio) components and implements standards like PSR-3, PSR-4, PSR-7, PSR-11 and PSR-15. Dotkernel API is an alternative for legacy Laminas API Tools (formerly Apigility) applications, and is based on Enrico Zimuel's [Zend Expressive API - Skeleton example](https://github.com/ezimuel/zend-expressive-api). @@ -122,13 +122,13 @@ Sending a GET request to the [home page](http://0.0.0.0:8080/) should output the ```text { - "message": "DotKernel API version 5" + "message": "Dotkernel API version 5" } ``` ## Documentation -In order to access DotKernel API documentation, check the provided [readme file](documentation/README.md). +In order to access Dotkernel API documentation, check the provided [readme file](documentation/README.md). Additionally, each CLI command available has it's own documentation: diff --git a/composer.json b/composer.json index 6107a3cf..ee1ab84e 100644 --- a/composer.json +++ b/composer.json @@ -1,7 +1,7 @@ { "name": "dotkernel/api", "type": "project", - "description": "DotKernel API", + "description": "Dotkernel API", "license": "MIT", "homepage": "https://www.dotkernel.org", "support": { @@ -19,7 +19,7 @@ ], "authors": [ { - "name": "DotKernel Team", + "name": "Dotkernel Team", "email": "team@dotkernel.com" } ], @@ -51,47 +51,48 @@ "php": "~8.2.0 || ~8.3.0", "ext-gd": "*", "ext-json": "*", - "dotkernel/dot-cache": "^4.0", - "dotkernel/dot-cli": "^3.5.0", - "dotkernel/dot-data-fixtures": "^1.2.2", - "dotkernel/dot-dependency-injection": "^1.0", + "dotkernel/dot-cache": "^4.3", + "dotkernel/dot-cli": "^3.9.0", + "dotkernel/dot-data-fixtures": "^1.4.0", + "dotkernel/dot-dependency-injection": "^1.2", "dotkernel/dot-errorhandler": "^4.0.0", - "dotkernel/dot-mail": "^5.1.0", - "dotkernel/dot-response-header": "^3.2.3", - "laminas/laminas-component-installer": "^3.4.0", - "laminas/laminas-config-aggregator": "^1.14.0", - "laminas/laminas-hydrator": "^4.15.0", - "laminas/laminas-inputfilter": "^2.29.0", - "laminas/laminas-stdlib": "^3.19.0", - "mezzio/mezzio": "^3.19.0", - "mezzio/mezzio-authentication-oauth2": "^2.8.0", - "mezzio/mezzio-authorization-acl": "^1.10.0", - "mezzio/mezzio-authorization-rbac": "^1.7.0", - "mezzio/mezzio-cors": "^1.11.1", - "mezzio/mezzio-fastroute": "^3.11.0", - "mezzio/mezzio-hal": "^2.9", - "mezzio/mezzio-problem-details": "^1.13.1", - "mezzio/mezzio-twigrenderer": "^2.15.0", + "dotkernel/dot-mail": "^5.3.0", + "dotkernel/dot-response-header": "^3.4.1", + "laminas/laminas-component-installer": "^3.5.0", + "laminas/laminas-config-aggregator": "^1.18.0", + "laminas/laminas-hydrator": "^4.16.0", + "laminas/laminas-inputfilter": "^2.31.0", + "laminas/laminas-stdlib": "^3.20.0", + "mezzio/mezzio": "^3.20.1", + "mezzio/mezzio-authentication-oauth2": "^2.11.0", + "mezzio/mezzio-authorization-acl": "^1.11.0", + "mezzio/mezzio-authorization-rbac": "^1.8.0", + "mezzio/mezzio-cors": "^1.13.0", + "mezzio/mezzio-fastroute": "^3.12.0", + "mezzio/mezzio-hal": "^2.10", + "mezzio/mezzio-problem-details": "^1.15.0", + "mezzio/mezzio-twigrenderer": "^2.17.0", "ramsey/uuid-doctrine": "^2.1.0", - "roave/psr-container-doctrine": "^5.2.1", - "symfony/filesystem": "^7.0.3", - "zircote/swagger-php": "^4.10" + "roave/psr-container-doctrine": "^5.2.2", + "symfony/filesystem": "^7.2.0", + "zircote/swagger-php": "^5.0.6" }, "require-dev": { - "laminas/laminas-coding-standard": "^3.0", - "laminas/laminas-development-mode": "^3.12.0", - "mezzio/mezzio-tooling": "^2.9.0", - "phpstan/phpstan": "^2.0", - "phpstan/phpstan-doctrine": "^2.0", - "phpstan/phpstan-phpunit": "^2.0", - "phpunit/phpunit": "^10.5.10", + "laminas/laminas-coding-standard": "^3.0.1", + "laminas/laminas-development-mode": "^3.13.0", + "mezzio/mezzio-tooling": "^2.10.1", + "phpstan/phpstan": "^2.1.7", + "phpstan/phpstan-doctrine": "^2.0.2", + "phpstan/phpstan-phpunit": "^2.0.4", + "phpunit/phpunit": "^10.5.45", "roave/security-advisories": "dev-latest", - "symfony/var-dumper": "^7.1" + "symfony/var-dumper": "^7.2.3" }, "autoload": { "psr-4": { "Api\\Admin\\": "src/Admin/src/", "Api\\App\\": "src/App/src/", + "Api\\Security\\": "src/Security/src/", "Api\\User\\": "src/User/src/", "Core\\Admin\\": "src/Core/src/Admin/src/", "Core\\App\\": "src/Core/src/App/src/", @@ -105,12 +106,6 @@ } }, "scripts": { - "post-update-cmd": [ - "php bin/composer-post-install-script.php" - ], - "development-disable": "laminas-development-mode disable", - "development-enable": "laminas-development-mode enable", - "development-status": "laminas-development-mode status", "check": [ "@cs-check", "@test", @@ -119,6 +114,12 @@ "clear-config-cache": "php bin/clear-config-cache.php", "cs-check": "phpcs", "cs-fix": "phpcbf", + "development-disable": "laminas-development-mode disable", + "development-enable": "laminas-development-mode enable", + "development-status": "laminas-development-mode status", + "post-update-cmd": [ + "php bin/composer-post-install-script.php" + ], "serve": "php -S 0.0.0.0:8080 -t public/", "static-analysis": "phpstan analyse --memory-limit 1G", "test": "phpunit --colors=always" diff --git a/config/autoload/authorization.global.php b/config/autoload/authorization.global.php index 517d15f5..0200936f 100644 --- a/config/autoload/authorization.global.php +++ b/config/autoload/authorization.global.php @@ -30,50 +30,50 @@ 'permissions' => [ AdminRole::ROLE_SUPERUSER => [], AdminRole::ROLE_ADMIN => [ - 'admin.my-account.update', - 'admin.my-account.view', - 'admin.create', - 'admin.delete', - 'admin.list', - 'admin.update', - 'admin.view', - 'admin.role.list', - 'admin.role.view', - 'user.activate', - 'user.create', - 'user.list', - 'user.delete', - 'user.view', - 'user.update', - 'user.avatar.create', - 'user.avatar.delete', - 'user.avatar.view', - 'user.role.list', - 'user.role.view', - 'error.report', - 'home', + 'admin::list-admin', + 'admin::create-admin', + 'admin::delete-admin', + 'admin::view-admin', + 'admin::update-admin', + 'admin::list-role', + 'admin::view-role', + 'admin::view-account', + 'admin::update-account', + 'user::list-user', + 'user::create-user', + 'user::delete-user', + 'user::view-user', + 'user::update-user', + 'user::delete-user-avatar', + 'user::view-user-avatar', + 'user::create-user-avatar', + 'user::list-role', + 'user::view-role', + 'user::activate-user', + 'user::deactivate-user', + 'app::create-error-report', + 'app::view-index', ], UserRole::ROLE_USER => [ - 'user.my-account.delete', - 'user.my-account.update', - 'user.my-account.view', - 'user.my-avatar.create', - 'user.my-avatar.delete', - 'user.my-avatar.view', + 'user::delete-account', + 'user::view-account', + 'user::update-account', + 'user::delete-account-avatar', + 'user::view-account-avatar', + 'user::create-account-avatar', ], UserRole::ROLE_GUEST => [ - 'account.activate.request', - 'account.activate', - 'account.register', - 'account.modify-password', - 'account.recover-identity', - 'account.reset-password.validate', - 'account.reset-password.request', - 'security.generate-token', - 'security.refresh-token', - 'error.report', - 'home', - 'user.create', + 'app::create-error-report', + 'app::view-index', + 'user::activate-account', + 'user::request-activate-account', + 'user::recover-account', + 'user::check-account-reset-password', + 'user::update-account-reset-password', + 'user::create-account-reset-password', + 'user::create-account', + 'security::token', + 'security::token', ], ], ], diff --git a/config/autoload/cli.global.php b/config/autoload/cli.global.php index 1f88428c..16a9b9ca 100644 --- a/config/autoload/cli.global.php +++ b/config/autoload/cli.global.php @@ -11,7 +11,7 @@ return [ 'dot_cli' => [ 'version' => '1.0.0', - 'name' => 'DotKernel CLI', + 'name' => 'Dotkernel CLI', 'commands' => [ DemoCommand::getDefaultName() => DemoCommand::class, RouteListCommand::getDefaultName() => RouteListCommand::class, diff --git a/config/autoload/local.php.dist b/config/autoload/local.php.dist index 196f09db..59221147 100644 --- a/config/autoload/local.php.dist +++ b/config/autoload/local.php.dist @@ -20,7 +20,7 @@ $databases = [ return [ 'application' => [ - 'name' => 'DotKernel API', + 'name' => 'Dotkernel API', 'url' => $baseUrl, 'versioning' => [ 'documentation_url' => 'https://docs.dotkernel.org/api-documentation/v5/core-features/versioning', diff --git a/config/autoload/mail.global.php b/config/autoload/mail.global.php new file mode 100644 index 00000000..9ef3b308 --- /dev/null +++ b/config/autoload/mail.global.php @@ -0,0 +1,80 @@ + [ + //the key is the mail service name, this is the default one, which does not extend any configuration + 'default' => [ + //message configuration + 'message_options' => [ + //from email address of the email + 'from' => '', + //from name to be displayed instead of from address + 'from_name' => '', + //reply-to email address of the email + 'reply_to' => '', + //replyTo name to be displayed instead of the address + 'reply_to_name' => '', + //destination email address as string or a list of email addresses + 'to' => [], + //copy destination addresses + 'cc' => [], + //hidden copy destination addresses + 'bcc' => [], + //email subject + 'subject' => '', + //body options - content can be plain text, HTML + 'body' => [ + 'content' => '', + 'charset' => 'utf-8', + ], + //attachments config + 'attachments' => [ + 'files' => [], + 'dir' => [ + 'iterate' => false, + 'path' => 'data/mail/attachments', + 'recursive' => false, + ], + ], + ], + /** + * the mail transport to use can be any class implementing + * Symfony\Component\Mailer\Transport\TransportInterface + * + * for standard mail transports, you can use these aliases: + * - sendmail => Symfony\Component\Mailer\Transport\SendmailTransport + * - esmtp => Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport + * + * defaults to sendmail + **/ + 'transport' => 'sendmail', + //options that will be used only if esmtp adapter is used + 'smtp_options' => [ + //hostname or IP address of the mail server + 'host' => '', + //port of the mail server - 587 or 465 for secure connections + 'port' => 587, + 'connection_config' => [ + //the smtp authentication identity + 'username' => '', + //the smtp authentication credential + 'password' => '', + //to disable auto_tls set tls key to false + //it's not recommended to disable TLS while connecting to an SMTP server + 'tls' => null, + ], + ], + ], + // option to log the SENT emails + 'log' => [ + 'sent' => getcwd() . '/log/mail/sent.log', + ], + ], +]; diff --git a/config/config.php b/config/config.php index 5ccb6224..ec61fbd1 100644 --- a/config/config.php +++ b/config/config.php @@ -53,6 +53,7 @@ class_exists(Mezzio\Tooling\ConfigProvider::class) Core\User\ConfigProvider::class, Api\Admin\ConfigProvider::class, Api\App\ConfigProvider::class, + Api\Security\ConfigProvider::class, Api\User\ConfigProvider::class, // Load application config in a pre-defined order in such a way that local settings diff --git a/config/pipeline.php b/config/pipeline.php index 3c5cbff2..015c3ccd 100644 --- a/config/pipeline.php +++ b/config/pipeline.php @@ -2,11 +2,12 @@ declare(strict_types=1); -use Api\App\Handler\NotFoundHandler; +use Api\App\Handler\GetNotFoundViewHandler; use Api\App\Middleware\AuthenticationMiddleware; use Api\App\Middleware\AuthorizationMiddleware; use Api\App\Middleware\ContentNegotiationMiddleware; use Api\App\Middleware\DeprecationMiddleware; +use Api\App\Middleware\ResponseMiddleware; use Dot\ErrorHandler\ErrorHandlerInterface; use Dot\ResponseHeader\Middleware\ResponseHeaderMiddleware; use Mezzio\Application; @@ -79,10 +80,12 @@ // - route-based validation // - etc. + $app->pipe(ResponseMiddleware::class); + // Register the dispatch middleware in the middleware pipeline $app->pipe(DispatchMiddleware::class); // At this point, if no Response is returned by any middleware, the // NotFoundHandler kicks in; alternately, you can provide other fallback // middleware to execute. - $app->pipe(NotFoundHandler::class); + $app->pipe(GetNotFoundViewHandler::class); }; diff --git a/documentation/DotKernel_API.postman_collection.json b/documentation/Dotkernel_API.postman_collection.json similarity index 99% rename from documentation/DotKernel_API.postman_collection.json rename to documentation/Dotkernel_API.postman_collection.json index ddb5361f..4295146d 100644 --- a/documentation/DotKernel_API.postman_collection.json +++ b/documentation/Dotkernel_API.postman_collection.json @@ -1,8 +1,8 @@ { "info": { "_postman_id": "5f4c1b92-b1e1-4f8e-840f-f92b6f4cb670", - "name": "DotKernel_API", - "description": "DotKernel API documentation.", + "name": "Dotkernel_API", + "description": "Dotkernel API documentation.", "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", "_exporter_id": "3494496" }, @@ -1356,7 +1356,7 @@ "variable": [ { "key": "APPLICATION_NAME", - "value": "DotKernel API" + "value": "Dotkernel API" }, { "key": "RANDOM_HASH", diff --git a/documentation/DotKernel_API.postman_environment.json b/documentation/Dotkernel_API.postman_environment.json similarity index 92% rename from documentation/DotKernel_API.postman_environment.json rename to documentation/Dotkernel_API.postman_environment.json index 277caf59..ff51409e 100644 --- a/documentation/DotKernel_API.postman_environment.json +++ b/documentation/Dotkernel_API.postman_environment.json @@ -1,6 +1,6 @@ { "id": "3ee44887-32cc-4427-891a-4336eff67d80", - "name": "DotKernel_API", + "name": "Dotkernel_API", "values": [ { "key": "APPLICATION_URL", diff --git a/documentation/README.md b/documentation/README.md index 0f6968cf..a49bbe44 100644 --- a/documentation/README.md +++ b/documentation/README.md @@ -1,44 +1,49 @@ -# DotKernel API Documentation +# Dotkernel API Documentation -You can access DotKernel API documentation by importing the provided collection and environment files into Postman. +You can access Dotkernel API documentation by importing the provided collection and environment files into Postman. ## Requirements + * [Postman](https://www.postman.com/downloads/) ## Setup -At this point, we assume you already have Postman installed. The following steps will be performed in Postman. + +At this point, we assume you already have Postman installed. +The following steps will be performed in Postman. ### Import project files + * click **File** -> **Import** -> **Upload Files** -* navigate to [documentation](/documentation) directory -* select both [DotKernel_API.postman_collection.json](/documentation/DotKernel_API.postman_collection.json) and [DotKernel_API.postman_environment.json](/documentation/DotKernel_API.postman_environment.json) +* navigate to the [documentation](/documentation) directory +* select both [Dotkernel_API.postman_collection.json](/documentation/Dotkernel_API.postman_collection.json) and [Dotkernel_API.postman_environment.json](/documentation/Dotkernel_API.postman_environment.json) * click **Import** -You should see a new collection (`DotKernel_API`) added to your collection list, containing the documentation of all DotKernel API endpoints. +You should see a new collection (`Dotkernel_API`) added to your collection list, containing the documentation of all Dotkernel API endpoints. -Also, you should see a new environment (`DotKernel_API`) added to your environments. +Also, you should see a new environment (`Dotkernel_API`) added to your environments. This contains a variable, called `APPLICATION_URL` set to `http://0.0.0.0:8080`. If your application runs on a different URL/port, modify this variable accordingly. ## Usage -DotKernel API Endpoints are secured with OAuth2, this means that calling an endpoint requires an access token being sent via the `Authorization` header (edit collection root directory and look under `Authorization` tab). +Dotkernel API Endpoints are secured with OAuth2, this means that calling an endpoint requires an access token being sent via the `Authorization` header (edit collection root directory and look under `Authorization` tab). ### Add a new request + * right-click on the parent directory you want to create the request inside, then click **Add Request** * enter name and description for your request * select the proper request method: - * **DELETE**: if you are deleting an item - * **GET**: if you are viewing an item or a list of items - * **PATCH**: if you are (partially) updating an item - * **PUT**: depending on if it exists or not, update or create an item - * **POST**: if you are creating an item + * **DELETE**: if you are deleting an item + * **GET**: if you are viewing an item or a list of items + * **PATCH**: if you are (partially) updating an item + * **PUT**: depending on if it exists or not, update or create an item + * **POST**: if you are creating an item * if needed, add query parameters (`Params` tab) * enter request URL (eg: `{{APPLICATION_URL}}/example`): you can use the existing `APPLICATION_URL` environment variable by placing it between double curly braces * select body (`Body` tab) format based on the data your endpoint expects: - * use **none** if no data will be sent to this endpoint - * use **form-data** if besides form data, this endpoint accepts file attachments as well - * use **raw** (also, set Content-Type to **JSON**) for creating/updating items + * use **none** if no data will be sent to this endpoint + * use **form-data** if besides form data, this endpoint accepts file attachments as well + * use **raw** (also, set Content-Type to **JSON**) for creating/updating items New requests added to the collection will not require adding the `Authorization` header because by default it is inherited from parent directories (under `Authorization` tab: `Type` is set to `Inherit auth from parent`). If your request should be accessible by guest users, you need to set `Type` to `No Auth`. diff --git a/documentation/command/admin-create.md b/documentation/command/admin-create.md index 8536ab20..4a837434 100644 --- a/documentation/command/admin-create.md +++ b/documentation/command/admin-create.md @@ -1,28 +1,36 @@ -# Creating admin accounts in DotKernel API +# Creating admin accounts in Dotkernel API ## Usage Run the following command in your application’s root directory: -`php ./bin/cli.php admin:create -i {IDENTITY} -p {PASSWORD}` +```shell +php ./bin/cli.php admin:create -i {IDENTITY} -p {PASSWORD} +``` OR -`php ./bin/cli.php admin:create --identity {IDENTITY} --password {PASSWORD}` +```shell +php ./bin/cli.php admin:create --identity {IDENTITY} --password {PASSWORD} +``` after replacing: -* {IDENTITY} with a valid username OR email address -* {PASSWORD} with a valid password -**NOTE:** -* if the specified identity or password 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 role of admin +* _{IDENTITY}_ with a valid username OR email address +* _{PASSWORD}_ with a valid password + +> If the specified identity or password 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 role of admin. If the submitted data is valid, the outputted response is: -`Admin account has been created.` + +```text +Admin account has been created. +``` The new admin account is ready to use. You can get more help with this command by running: -`php ./bin/cli.php help admin:create` +```shell +php ./bin/cli.php help admin:create +``` diff --git a/documentation/command/migrations-diff.md b/documentation/command/migrations-diff.md index c10a929c..51107319 100644 --- a/documentation/command/migrations-diff.md +++ b/documentation/command/migrations-diff.md @@ -4,7 +4,9 @@ Run the following command in your application’s root directory: -`vendor/bin/doctrine-migrations diff` +```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). @@ -15,12 +17,15 @@ The command to be executed without dropping these tables looks like this: On Windows (use double quotes): -`vendor/bin/doctrine-migrations diff --filter-expression="/^(?!oauth_)/"` +```shell +vendor/bin/doctrine-migrations diff --filter-expression="/^(?!oauth_)/" +``` On Linux/macOS (use single quotes): -`vendor/bin/doctrine-migrations diff --filter-expression='/^(?!oauth_)/'` - +```shell +vendor/bin/doctrine-migrations diff --filter-expression='/^(?!oauth_)/' +``` ## Filtering multiple unmapped table patterns @@ -29,12 +34,15 @@ For example, if you need to filter tables prefixed with `foo_` and `bar_`, then On Windows: -`vendor/bin/doctrine-migrations diff --filter-expression="/^(?!foo_|bar_)/"` +```shell +vendor/bin/doctrine-migrations diff --filter-expression="/^(?!foo_|bar_)/" +``` On Linux/macOS: -`vendor/bin/doctrine-migrations diff --filter-expression='/^(?!foo_|bar_)/'` - +```shell +vendor/bin/doctrine-migrations diff --filter-expression='/^(?!foo_|bar_)/' +``` ## Troubleshooting @@ -42,13 +50,15 @@ On Windows, running the command in PowerShell might still add the `DROP TABLE oa 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: -`vendor/bin/doctrine-migrations help diff` +```shell +vendor/bin/doctrine-migrations help diff +``` diff --git a/documentation/command/route-list.md b/documentation/command/route-list.md index 1dc5558c..5529a49b 100644 --- a/documentation/command/route-list.md +++ b/documentation/command/route-list.md @@ -1,60 +1,60 @@ -# Displaying DotKernel API endpoints using dot-cli +# Displaying Dotkernel API endpoints using dot-cli ## Usage Run the following command in your application’s root directory: - php ./bin/cli.php route:list +```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 -+--------+---------------------------------+--------------------------------+ -| Method | Name | Path | -+--------+---------------------------------+--------------------------------+ -| DELETE | admin.delete | /admin/{uuid} | -| DELETE | user.my-account.delete | /user/my-account | -| DELETE | user.my-avatar.delete | /user/my-avatar | -| DELETE | user.delete | /user/{uuid} | -| DELETE | user.avatar.delete | /user/{uuid}/avatar | -| GET | home | / | -| GET | account.reset-password.validate | /account/reset-password/{hash} | -| GET | admin.list | /admin | -| GET | admin.my-account.view | /admin/my-account | -| GET | admin.role.list | /admin/role | -| GET | admin.role.view | /admin/role/{uuid} | -| GET | admin.view | /admin/{uuid} | -| GET | user.list | /user | -| GET | user.my-account.view | /user/my-account | -| GET | user.my-avatar.view | /user/my-avatar | -| GET | user.role.list | /user/role | -| GET | user.role.view | /user/role/{uuid} | -| GET | user.view | /user/{uuid} | -| GET | user.avatar.view | /user/{uuid}/avatar | -| PATCH | account.activate | /account/activate/{hash} | -| PATCH | account.modify-password | /account/reset-password/{hash} | -| PATCH | admin.my-account.update | /admin/my-account | -| PATCH | admin.update | /admin/{uuid} | -| PATCH | user.my-account.update | /user/my-account | -| PATCH | user.update | /user/{uuid} | -| POST | account.activate.request | /account/activate | -| POST | account.recover-identity | /account/recover-identity | -| POST | account.register | /account/register | -| POST | account.reset-password.request | /account/reset-password | -| POST | admin.create | /admin | -| POST | error.report | /error-report | -| POST | security.generate-token | /security/generate-token | -| POST | security.refresh-token | /security/refresh-token | -| POST | user.create | /user | -| POST | user.my-avatar.create | /user/my-avatar | -| POST | user.activate | /user/{uuid}/activate | -| POST | user.avatar.create | /user/{uuid}/avatar | -+--------+---------------------------------+--------------------------------+ -``` + +| Request method | Route name | Route path | +|-----------------|---------------------------------|--------------------------------| +| DELETE | admin.delete | /admin/{uuid} | +| DELETE | user.my-account.delete | /user/my-account | +| DELETE | user.my-avatar.delete | /user/my-avatar | +| DELETE | user.delete | /user/{uuid} | +| DELETE | user.avatar.delete | /user/{uuid}/avatar | +| GET | home | / | +| GET | account.reset-password.validate | /account/reset-password/{hash} | +| GET | admin.list | /admin | +| GET | admin.my-account.view | /admin/my-account | +| GET | admin.role.list | /admin/role | +| GET | admin.role.view | /admin/role/{uuid} | +| GET | admin.view | /admin/{uuid} | +| GET | user.list | /user | +| GET | user.my-account.view | /user/my-account | +| GET | user.my-avatar.view | /user/my-avatar | +| GET | user.role.list | /user/role | +| GET | user.role.view | /user/role/{uuid} | +| GET | user.view | /user/{uuid} | +| GET | user.avatar.view | /user/{uuid}/avatar | +| PATCH | account.activate | /account/activate/{hash} | +| PATCH | account.modify-password | /account/reset-password/{hash} | +| PATCH | admin.my-account.update | /admin/my-account | +| PATCH | admin.update | /admin/{uuid} | +| PATCH | user.my-account.update | /user/my-account | +| PATCH | user.update | /user/{uuid} | +| POST | account.activate.request | /account/activate | +| POST | account.recover-identity | /account/recover-identity | +| POST | account.register | /account/register | +| POST | account.reset-password.request | /account/reset-password | +| POST | admin.create | /admin | +| POST | error.report | /error-report | +| POST | security.generate-token | /security/generate-token | +| POST | security.refresh-token | /security/refresh-token | +| POST | user.create | /user | +| POST | user.my-avatar.create | /user/my-avatar | +| POST | user.activate | /user/{uuid}/activate | +| POST | user.avatar.create | /user/{uuid}/avatar | ## Filtering results The following filters can be applied when displaying the routes 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]` @@ -63,4 +63,6 @@ The filters are case-insensitive and can be combined. Get more help by running this command: - php ./bin/cli.php route:list --help +```shell +php ./bin/cli.php route:list --help +``` diff --git a/documentation/command/token-generate.md b/documentation/command/token-generate.md index 681d142a..7f3322a6 100644 --- a/documentation/command/token-generate.md +++ b/documentation/command/token-generate.md @@ -1,55 +1,64 @@ -# Generating tokens in DotKernel API +# 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: - php ./bin/cli.php token:generate +```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: - php ./bin/cli.php token:generate --help - +```shell +php ./bin/cli.php token:generate --help +``` ### Generate error reporting token You can generate an error reporting token by executing the following command: - php ./bin/cli.php token:generate error-reporting +```shell +php ./bin/cli.php token:generate error-reporting +``` The output should look similar to this: - Error reporting token: - - 0123456789abcdef0123456789abcdef01234567 +```text +Error reporting token: + + 0123456789abcdef0123456789abcdef01234567 +``` Copy the generated token. -Open `config/autoload/error-handling.global.php` and paste the copied token as shown below: - - return [ - ... - ErrorReportServiceInterface::class => [ - ... - 'tokens' => [ - '0123456789abcdef0123456789abcdef01234567', - ], - ... - ] +Open `config/autoload/error-handling.global.php` and paste the copied token under the `tokens` key, as shown below: + +```php +return [ + // ... + ErrorReportServiceInterface::class => [ + // ... + 'tokens' => [ + '0123456789abcdef0123456789abcdef01234567', + ], + // ... ] +] +``` Save and close `config/autoload/error-handling.global.php`. -**Note**: - -If your application is NOT in development mode, make sure you clear your config cache by executing: +> If your application is NOT in development mode, make sure you clear your config cache by executing: - php ./bin/clear-config-cache.php +```shell +php ./bin/clear-config-cache.php +``` diff --git a/src/Admin/src/ConfigProvider.php b/src/Admin/src/ConfigProvider.php index 485da2f4..f9cac17f 100644 --- a/src/Admin/src/ConfigProvider.php +++ b/src/Admin/src/ConfigProvider.php @@ -8,11 +8,15 @@ use Api\Admin\Collection\AdminRoleCollection; use Api\Admin\Command\AdminCreateCommand; use Api\Admin\Factory\AdminCreateCommandFactory; -use Api\Admin\Handler\AdminAccountHandler; -use Api\Admin\Handler\AdminCollectionHandler; -use Api\Admin\Handler\AdminHandler; -use Api\Admin\Handler\AdminRoleCollectionHandler; -use Api\Admin\Handler\AdminRoleHandler; +use Api\Admin\Handler\Account\GetAdminAccountResourceHandler; +use Api\Admin\Handler\Account\PatchAdminAccountResourceHandler; +use Api\Admin\Handler\Admin\DeleteAdminResourceHandler; +use Api\Admin\Handler\Admin\GetAdminCollectionHandler; +use Api\Admin\Handler\Admin\GetAdminResourceHandler; +use Api\Admin\Handler\Admin\PatchAdminResourceHandler; +use Api\Admin\Handler\Admin\PostAdminResourceHandler; +use Api\Admin\Handler\Admin\Role\GetAdminRoleCollectionHandler; +use Api\Admin\Handler\Admin\Role\GetAdminRoleResourceHandler; use Api\Admin\Service\AdminRoleService; use Api\Admin\Service\AdminRoleServiceInterface; use Api\Admin\Service\AdminService; @@ -42,24 +46,32 @@ public function getDependencies(): array { return [ 'delegators' => [ - Application::class => [RoutesDelegator::class], - AdminAccountHandler::class => [HandlerDelegatorFactory::class], - AdminCollectionHandler::class => [HandlerDelegatorFactory::class], - AdminHandler::class => [HandlerDelegatorFactory::class], - AdminRoleCollectionHandler::class => [HandlerDelegatorFactory::class], - AdminRoleHandler::class => [HandlerDelegatorFactory::class], + Application::class => [RoutesDelegator::class], + DeleteAdminResourceHandler::class => [HandlerDelegatorFactory::class], + GetAdminAccountResourceHandler::class => [HandlerDelegatorFactory::class], + GetAdminCollectionHandler::class => [HandlerDelegatorFactory::class], + GetAdminResourceHandler::class => [HandlerDelegatorFactory::class], + GetAdminRoleCollectionHandler::class => [HandlerDelegatorFactory::class], + GetAdminRoleResourceHandler::class => [HandlerDelegatorFactory::class], + PatchAdminAccountResourceHandler::class => [HandlerDelegatorFactory::class], + PatchAdminResourceHandler::class => [HandlerDelegatorFactory::class], + PostAdminResourceHandler::class => [HandlerDelegatorFactory::class], ], 'factories' => [ - AdminAccountHandler::class => AttributedServiceFactory::class, - AdminCollectionHandler::class => AttributedServiceFactory::class, - AdminCreateCommand::class => AdminCreateCommandFactory::class, - AdminHandler::class => AttributedServiceFactory::class, - AdminRepository::class => AttributedRepositoryFactory::class, - AdminRoleCollectionHandler::class => AttributedServiceFactory::class, - AdminRoleHandler::class => AttributedServiceFactory::class, - AdminRoleRepository::class => AttributedRepositoryFactory::class, - AdminRoleService::class => AttributedServiceFactory::class, - AdminService::class => AttributedServiceFactory::class, + AdminCreateCommand::class => AdminCreateCommandFactory::class, + AdminRepository::class => AttributedRepositoryFactory::class, + AdminRoleRepository::class => AttributedRepositoryFactory::class, + AdminRoleService::class => AttributedServiceFactory::class, + AdminService::class => AttributedServiceFactory::class, + DeleteAdminResourceHandler::class => AttributedServiceFactory::class, + GetAdminAccountResourceHandler::class => AttributedServiceFactory::class, + GetAdminCollectionHandler::class => AttributedServiceFactory::class, + GetAdminResourceHandler::class => AttributedServiceFactory::class, + GetAdminRoleCollectionHandler::class => AttributedServiceFactory::class, + GetAdminRoleResourceHandler::class => AttributedServiceFactory::class, + PatchAdminAccountResourceHandler::class => AttributedServiceFactory::class, + PatchAdminResourceHandler::class => AttributedServiceFactory::class, + PostAdminResourceHandler::class => AttributedServiceFactory::class, ], 'aliases' => [ AdminRoleServiceInterface::class => AdminRoleService::class, @@ -71,10 +83,10 @@ public function getDependencies(): array public function getHalConfig(): array { return [ - AppConfigProvider::getCollection(AdminCollection::class, 'admin.list', 'admins'), - AppConfigProvider::getCollection(AdminRoleCollection::class, 'admin.role.list', 'roles'), - AppConfigProvider::getResource(Admin::class, 'admin.view'), - AppConfigProvider::getResource(AdminRole::class, 'admin.role.view'), + AppConfigProvider::getCollection(AdminCollection::class, 'admin::list-admin', 'admins'), + AppConfigProvider::getCollection(AdminRoleCollection::class, 'admin::list-role', 'roles'), + AppConfigProvider::getResource(Admin::class, 'admin::view-admin'), + AppConfigProvider::getResource(AdminRole::class, 'admin::view-role'), ]; } } diff --git a/src/Admin/src/Handler/Account/GetAdminAccountResourceHandler.php b/src/Admin/src/Handler/Account/GetAdminAccountResourceHandler.php new file mode 100644 index 00000000..306c6baa --- /dev/null +++ b/src/Admin/src/Handler/Account/GetAdminAccountResourceHandler.php @@ -0,0 +1,18 @@ +createResponse($request, $request->getAttribute(Admin::class)); + } +} diff --git a/src/Admin/src/Handler/AdminAccountHandler.php b/src/Admin/src/Handler/Account/PatchAdminAccountResourceHandler.php similarity index 77% rename from src/Admin/src/Handler/AdminAccountHandler.php rename to src/Admin/src/Handler/Account/PatchAdminAccountResourceHandler.php index d72ad13e..fbaf475f 100644 --- a/src/Admin/src/Handler/AdminAccountHandler.php +++ b/src/Admin/src/Handler/Account/PatchAdminAccountResourceHandler.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Api\Admin\Handler; +namespace Api\Admin\Handler\Account; use Api\Admin\InputFilter\UpdateAdminInputFilter; use Api\Admin\Service\AdminServiceInterface; @@ -15,7 +15,7 @@ use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; -class AdminAccountHandler extends AbstractHandler +class PatchAdminAccountResourceHandler extends AbstractHandler { #[Inject( AdminServiceInterface::class, @@ -25,17 +25,12 @@ public function __construct( ) { } - public function get(ServerRequestInterface $request): ResponseInterface - { - return $this->createResponse($request, $request->getAttribute(Admin::class)); - } - /** * @throws BadRequestException * @throws ConflictException * @throws NotFoundException */ - public function patch(ServerRequestInterface $request): ResponseInterface + public function handle(ServerRequestInterface $request): ResponseInterface { $inputFilter = (new UpdateAdminInputFilter())->setData((array) $request->getParsedBody()); if (! $inputFilter->isValid()) { diff --git a/src/Admin/src/Handler/Admin/DeleteAdminResourceHandler.php b/src/Admin/src/Handler/Admin/DeleteAdminResourceHandler.php new file mode 100644 index 00000000..2374ec2d --- /dev/null +++ b/src/Admin/src/Handler/Admin/DeleteAdminResourceHandler.php @@ -0,0 +1,35 @@ +adminService->findOneBy(['uuid' => $request->getAttribute('uuid')]); + + $this->adminService->deleteAdmin($admin); + + return $this->noContentResponse(); + } +} diff --git a/src/Admin/src/Handler/AdminCollectionHandler.php b/src/Admin/src/Handler/Admin/GetAdminCollectionHandler.php similarity index 78% rename from src/Admin/src/Handler/AdminCollectionHandler.php rename to src/Admin/src/Handler/Admin/GetAdminCollectionHandler.php index 81f3b2de..827d08ad 100644 --- a/src/Admin/src/Handler/AdminCollectionHandler.php +++ b/src/Admin/src/Handler/Admin/GetAdminCollectionHandler.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Api\Admin\Handler; +namespace Api\Admin\Handler\Admin; use Api\Admin\Service\AdminServiceInterface; use Api\App\Exception\BadRequestException; @@ -11,7 +11,7 @@ use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; -class AdminCollectionHandler extends AbstractHandler +class GetAdminCollectionHandler extends AbstractHandler { #[Inject( AdminServiceInterface::class, @@ -24,7 +24,7 @@ public function __construct( /** * @throws BadRequestException */ - public function get(ServerRequestInterface $request): ResponseInterface + public function handle(ServerRequestInterface $request): ResponseInterface { return $this->createResponse($request, $this->adminService->getAdmins($request->getQueryParams())); } diff --git a/src/Admin/src/Handler/Admin/GetAdminResourceHandler.php b/src/Admin/src/Handler/Admin/GetAdminResourceHandler.php new file mode 100644 index 00000000..fff78ade --- /dev/null +++ b/src/Admin/src/Handler/Admin/GetAdminResourceHandler.php @@ -0,0 +1,33 @@ +adminService->findOneBy(['uuid' => $request->getAttribute('uuid')]); + + return $this->createResponse($request, $admin); + } +} diff --git a/src/Admin/src/Handler/Admin/PatchAdminResourceHandler.php b/src/Admin/src/Handler/Admin/PatchAdminResourceHandler.php new file mode 100644 index 00000000..1749154b --- /dev/null +++ b/src/Admin/src/Handler/Admin/PatchAdminResourceHandler.php @@ -0,0 +1,44 @@ +setData((array) $request->getParsedBody()); + if (! $inputFilter->isValid()) { + throw (new BadRequestException())->setMessages($inputFilter->getMessages()); + } + + $admin = $this->adminService->findOneBy(['uuid' => $request->getAttribute('uuid')]); + $this->adminService->updateAdmin($admin, $inputFilter->getValues()); + + return $this->createResponse($request, $admin); + } +} diff --git a/src/Admin/src/Handler/Admin/PostAdminResourceHandler.php b/src/Admin/src/Handler/Admin/PostAdminResourceHandler.php new file mode 100644 index 00000000..4f792de9 --- /dev/null +++ b/src/Admin/src/Handler/Admin/PostAdminResourceHandler.php @@ -0,0 +1,43 @@ +setData((array) $request->getParsedBody()); + if (! $inputFilter->isValid()) { + throw (new BadRequestException())->setMessages($inputFilter->getMessages()); + } + + $admin = $this->adminService->createAdmin($inputFilter->getValues()); + + return $this->createdResponse($request, $admin); + } +} diff --git a/src/Admin/src/Handler/AdminRoleCollectionHandler.php b/src/Admin/src/Handler/Admin/Role/GetAdminRoleCollectionHandler.php similarity index 78% rename from src/Admin/src/Handler/AdminRoleCollectionHandler.php rename to src/Admin/src/Handler/Admin/Role/GetAdminRoleCollectionHandler.php index d230e3ef..78b75ef5 100644 --- a/src/Admin/src/Handler/AdminRoleCollectionHandler.php +++ b/src/Admin/src/Handler/Admin/Role/GetAdminRoleCollectionHandler.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Api\Admin\Handler; +namespace Api\Admin\Handler\Admin\Role; use Api\Admin\Service\AdminRoleServiceInterface; use Api\App\Exception\BadRequestException; @@ -11,7 +11,7 @@ use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; -class AdminRoleCollectionHandler extends AbstractHandler +class GetAdminRoleCollectionHandler extends AbstractHandler { #[Inject( AdminRoleServiceInterface::class, @@ -24,7 +24,7 @@ public function __construct( /** * @throws BadRequestException */ - public function get(ServerRequestInterface $request): ResponseInterface + public function handle(ServerRequestInterface $request): ResponseInterface { return $this->createResponse($request, $this->roleService->getAdminRoles($request->getQueryParams())); } diff --git a/src/Admin/src/Handler/AdminRoleHandler.php b/src/Admin/src/Handler/Admin/Role/GetAdminRoleResourceHandler.php similarity index 79% rename from src/Admin/src/Handler/AdminRoleHandler.php rename to src/Admin/src/Handler/Admin/Role/GetAdminRoleResourceHandler.php index 0a73c90d..5d026fd7 100644 --- a/src/Admin/src/Handler/AdminRoleHandler.php +++ b/src/Admin/src/Handler/Admin/Role/GetAdminRoleResourceHandler.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Api\Admin\Handler; +namespace Api\Admin\Handler\Admin\Role; use Api\Admin\Service\AdminRoleServiceInterface; use Api\App\Exception\NotFoundException; @@ -11,7 +11,7 @@ use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; -class AdminRoleHandler extends AbstractHandler +class GetAdminRoleResourceHandler extends AbstractHandler { #[Inject( AdminRoleServiceInterface::class, @@ -24,7 +24,7 @@ public function __construct( /** * @throws NotFoundException */ - public function get(ServerRequestInterface $request): ResponseInterface + public function handle(ServerRequestInterface $request): ResponseInterface { $role = $this->roleService->findOneBy(['uuid' => $request->getAttribute('uuid')]); diff --git a/src/Admin/src/Handler/AdminHandler.php b/src/Admin/src/Handler/AdminHandler.php deleted file mode 100644 index 822afb8d..00000000 --- a/src/Admin/src/Handler/AdminHandler.php +++ /dev/null @@ -1,84 +0,0 @@ -adminService->findOneBy(['uuid' => $request->getAttribute('uuid')]); - - $this->adminService->deleteAdmin($admin); - - return $this->noContentResponse(); - } - - /** - * @throws NotFoundException - */ - public function get(ServerRequestInterface $request): ResponseInterface - { - $admin = $this->adminService->findOneBy(['uuid' => $request->getAttribute('uuid')]); - - return $this->createResponse($request, $admin); - } - - /** - * @throws BadRequestException - * @throws ConflictException - * @throws NotFoundException - */ - public function patch(ServerRequestInterface $request): ResponseInterface - { - $inputFilter = (new UpdateAdminInputFilter())->setData((array) $request->getParsedBody()); - if (! $inputFilter->isValid()) { - throw (new BadRequestException())->setMessages($inputFilter->getMessages()); - } - - $admin = $this->adminService->findOneBy(['uuid' => $request->getAttribute('uuid')]); - $this->adminService->updateAdmin($admin, $inputFilter->getValues()); - - return $this->createResponse($request, $admin); - } - - /** - * @throws BadRequestException - * @throws ConflictException - * @throws NotFoundException - */ - public function post(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); - } -} diff --git a/src/Admin/src/OpenAPI.php b/src/Admin/src/OpenAPI.php index b2a71e3d..96048f35 100644 --- a/src/Admin/src/OpenAPI.php +++ b/src/Admin/src/OpenAPI.php @@ -6,9 +6,15 @@ use Api\Admin\Collection\AdminCollection; use Api\Admin\Collection\AdminRoleCollection; -use Api\Admin\Handler\AdminAccountHandler; -use Api\Admin\Handler\AdminHandler; -use Api\Admin\Handler\AdminRoleHandler; +use Api\Admin\Handler\Account\GetAdminAccountResourceHandler; +use Api\Admin\Handler\Account\PatchAdminAccountResourceHandler; +use Api\Admin\Handler\Admin\DeleteAdminResourceHandler; +use Api\Admin\Handler\Admin\GetAdminCollectionHandler; +use Api\Admin\Handler\Admin\GetAdminResourceHandler; +use Api\Admin\Handler\Admin\PatchAdminResourceHandler; +use Api\Admin\Handler\Admin\PostAdminResourceHandler; +use Api\Admin\Handler\Admin\Role\GetAdminRoleCollectionHandler; +use Api\Admin\Handler\Admin\Role\GetAdminRoleResourceHandler; use Core\Admin\Entity\Admin; use Core\Admin\Entity\AdminRole; use Core\Admin\Enum\AdminStatusEnum; @@ -17,127 +23,7 @@ use OpenApi\Attributes as OA; /** - * @see AdminAccountHandler::get() - */ -#[OA\Get( - path: '/admin/my-account', - description: 'Authenticated (super)admin fetches their own account data', - summary: 'Admin fetches their own account', - security: [['AuthToken' => []]], - tags: ['Admin'], - responses: [ - new OA\Response( - response: StatusCodeInterface::STATUS_OK, - description: 'My admin account', - content: new OA\JsonContent(ref: '#/components/schemas/Admin'), - ), - ], -)] - -/** - * @see AdminAccountHandler::patch() - */ -#[OA\Patch( - path: '/admin/my-account', - description: 'Authenticated (super)admin updates their own account data', - summary: 'Admin updates their own account', - security: [['AuthToken' => []]], - requestBody: new OA\RequestBody( - description: 'Update my admin account request', - required: true, - content: new OA\JsonContent( - properties: [ - new OA\Property(property: 'password', type: 'string'), - new OA\Property(property: 'passwordConfirm', type: 'string'), - new OA\Property(property: 'firstName', type: 'string'), - new OA\Property(property: 'lastName', type: 'string'), - new OA\Property( - property: 'roles', - type: 'array', - items: new OA\Items( - required: ['uuid'], - properties: [ - new OA\Property(property: 'uuid', type: 'string'), - ], - ), - ), - ], - type: 'object', - ), - ), - tags: ['Admin'], - responses: [ - new OA\Response( - response: StatusCodeInterface::STATUS_OK, - description: 'My admin account', - content: new OA\JsonContent(ref: '#/components/schemas/Admin'), - ), - ], -)] - -/** - * @see AdminHandler::delete() - */ -#[OA\Delete( - path: '/admin/{uuid}', - description: 'Authenticated (super)admin deletes an admin account identified by its UUID', - summary: 'Admin deletes an admin account', - security: [['AuthToken' => []]], - tags: ['Admin'], - parameters: [ - new OA\Parameter( - name: 'uuid', - description: 'Admin UUID', - in: 'path', - required: true, - schema: new OA\Schema(type: 'string'), - ), - ], - responses: [ - new OA\Response( - response: StatusCodeInterface::STATUS_NO_CONTENT, - description: 'Admin account has been deleted', - ), - new OA\Response( - response: StatusCodeInterface::STATUS_NOT_FOUND, - description: 'Not Found', - ), - ], -)] - -/** - * @see AdminHandler::get() - */ -#[OA\Get( - path: '/admin/{uuid}', - description: 'Authenticated (super)admin fetches an admin account identified by its UUID', - summary: 'Admin fetches an admin account', - security: [['AuthToken' => []]], - tags: ['Admin'], - parameters: [ - new OA\Parameter( - name: 'uuid', - description: 'Admin UUID', - in: 'path', - required: true, - schema: new OA\Schema(type: 'string'), - ), - ], - responses: [ - new OA\Response( - response: StatusCodeInterface::STATUS_OK, - description: 'Admin account', - content: new OA\JsonContent(ref: '#/components/schemas/Admin'), - ), - new OA\Response( - response: StatusCodeInterface::STATUS_NOT_FOUND, - description: 'Not Found', - ), - ], -)] - -/** - * @see AdminHandler::getCollection() + * @see GetAdminCollectionHandler::handle() */ #[OA\Get( path: '/admin', @@ -208,18 +94,20 @@ )] /** - * @see AdminHandler::patch() + * @see PostAdminResourceHandler::handle() */ -#[OA\Patch( - path: '/admin/{uuid}', - description: 'Authenticated (super)admin updates an existing admin account', - summary: 'Admin updates an admin account', +#[OA\Post( + path: '/admin', + description: 'Authenticated (super)admin creates a new admin account', + summary: 'Admin creates an admin account', security: [['AuthToken' => []]], requestBody: new OA\RequestBody( - description: 'Update admin account request', + description: 'Create admin account request', required: true, content: new OA\JsonContent( + required: ['identity', 'password', 'passwordConfirm', 'firstName', 'lastName', 'roles'], properties: [ + new OA\Property(property: 'identity', type: 'string'), new OA\Property(property: 'password', type: 'string'), new OA\Property(property: 'passwordConfirm', type: 'string'), new OA\Property(property: 'firstName', type: 'string'), @@ -240,6 +128,39 @@ ), ), tags: ['Admin'], + responses: [ + new OA\Response( + response: StatusCodeInterface::STATUS_CREATED, + description: 'Admin account created', + content: new OA\JsonContent(ref: '#/components/schemas/Admin'), + ), + new OA\Response( + response: StatusCodeInterface::STATUS_BAD_REQUEST, + description: 'Bad Request', + content: new OA\JsonContent(ref: '#/components/schemas/ErrorMessage'), + ), + new OA\Response( + response: StatusCodeInterface::STATUS_CONFLICT, + description: 'Conflict', + content: new OA\JsonContent(ref: '#/components/schemas/ErrorMessage'), + ), + new OA\Response( + response: StatusCodeInterface::STATUS_NOT_FOUND, + description: 'Not Found', + content: new OA\JsonContent(ref: '#/components/schemas/ErrorMessage'), + ), + ], +)] + +/** + * @see DeleteAdminResourceHandler::handle() + */ +#[OA\Delete( + path: '/admin/{uuid}', + description: 'Authenticated (super)admin deletes an admin account identified by its UUID', + summary: 'Admin deletes an admin account', + security: [['AuthToken' => []]], + tags: ['Admin'], parameters: [ new OA\Parameter( name: 'uuid', @@ -251,43 +172,60 @@ ], responses: [ new OA\Response( - response: StatusCodeInterface::STATUS_OK, - description: 'Admin account updated', - content: new OA\JsonContent(ref: '#/components/schemas/Admin'), + response: StatusCodeInterface::STATUS_NO_CONTENT, + description: 'Admin account has been deleted', ), new OA\Response( - response: StatusCodeInterface::STATUS_BAD_REQUEST, - description: 'Bad Request', - content: new OA\JsonContent(ref: '#/components/schemas/ErrorMessage'), + response: StatusCodeInterface::STATUS_NOT_FOUND, + description: 'Not Found', ), + ], +)] + +/** + * @see GetAdminResourceHandler::handle() + */ +#[OA\Get( + path: '/admin/{uuid}', + description: 'Authenticated (super)admin fetches an admin account identified by its UUID', + summary: 'Admin fetches an admin account', + security: [['AuthToken' => []]], + tags: ['Admin'], + parameters: [ + new OA\Parameter( + name: 'uuid', + description: 'Admin UUID', + in: 'path', + required: true, + schema: new OA\Schema(type: 'string'), + ), + ], + responses: [ new OA\Response( - response: StatusCodeInterface::STATUS_CONFLICT, - description: 'Conflict', - content: new OA\JsonContent(ref: '#/components/schemas/ErrorMessage'), + response: StatusCodeInterface::STATUS_OK, + description: 'Admin account', + content: new OA\JsonContent(ref: '#/components/schemas/Admin'), ), new OA\Response( response: StatusCodeInterface::STATUS_NOT_FOUND, description: 'Not Found', - content: new OA\JsonContent(ref: '#/components/schemas/ErrorMessage'), ), ], )] /** - * @see AdminHandler::post() + * @see PatchAdminResourceHandler::handle() */ -#[OA\Post( - path: '/admin', - description: 'Authenticated (super)admin creates a new admin account', - summary: 'Admin creates an admin account', +#[OA\Patch( + path: '/admin/{uuid}', + description: 'Authenticated (super)admin updates an existing admin account', + summary: 'Admin updates an admin account', security: [['AuthToken' => []]], requestBody: new OA\RequestBody( - description: 'Create admin account request', + description: 'Update admin account request', required: true, content: new OA\JsonContent( - required: ['identity', 'password', 'passwordConfirm', 'firstName', 'lastName', 'roles'], properties: [ - new OA\Property(property: 'identity', type: 'string'), new OA\Property(property: 'password', type: 'string'), new OA\Property(property: 'passwordConfirm', type: 'string'), new OA\Property(property: 'firstName', type: 'string'), @@ -308,10 +246,19 @@ ), ), tags: ['Admin'], + parameters: [ + new OA\Parameter( + name: 'uuid', + description: 'Admin UUID', + in: 'path', + required: true, + schema: new OA\Schema(type: 'string'), + ), + ], responses: [ new OA\Response( - response: StatusCodeInterface::STATUS_CREATED, - description: 'Admin account created', + response: StatusCodeInterface::STATUS_OK, + description: 'Admin account updated', content: new OA\JsonContent(ref: '#/components/schemas/Admin'), ), new OA\Response( @@ -333,38 +280,7 @@ )] /** - * @see AdminRoleHandler::get() - */ -#[OA\Get( - path: '/admin/role/{uuid}', - description: 'Authenticated (super)admin fetches an admin role identified by its UUID', - summary: 'Admin fetches an admin role', - security: [['AuthToken' => []]], - tags: ['AdminRole'], - parameters: [ - new OA\Parameter( - name: 'uuid', - description: 'Admin role UUID', - in: 'path', - required: true, - schema: new OA\Schema(type: 'string'), - ), - ], - responses: [ - new OA\Response( - response: StatusCodeInterface::STATUS_OK, - description: 'Admin role', - content: new OA\JsonContent(ref: '#/components/schemas/AdminRole'), - ), - new OA\Response( - response: StatusCodeInterface::STATUS_NOT_FOUND, - description: 'Not Found', - ), - ], -)] - -/** - * @see AdminRoleHandler::getCollection() + * @see GetAdminRoleCollectionHandler::handle() */ #[OA\Get( path: '/admin/role', @@ -431,6 +347,96 @@ ], )] +/** + * @see GetAdminRoleResourceHandler::handle() + */ +#[OA\Get( + path: '/admin/role/{uuid}', + description: 'Authenticated (super)admin fetches an admin role identified by its UUID', + summary: 'Admin fetches an admin role', + security: [['AuthToken' => []]], + tags: ['AdminRole'], + parameters: [ + new OA\Parameter( + name: 'uuid', + description: 'Admin role UUID', + in: 'path', + required: true, + schema: new OA\Schema(type: 'string'), + ), + ], + responses: [ + new OA\Response( + response: StatusCodeInterface::STATUS_OK, + description: 'Admin role', + content: new OA\JsonContent(ref: '#/components/schemas/AdminRole'), + ), + new OA\Response( + response: StatusCodeInterface::STATUS_NOT_FOUND, + description: 'Not Found', + ), + ], +)] + +/** + * @see GetAdminAccountResourceHandler::handle() + */ +#[OA\Get( + path: '/admin/account', + description: 'Authenticated (super)admin fetches their own account data', + summary: 'Admin fetches their own account', + security: [['AuthToken' => []]], + tags: ['Admin'], + responses: [ + new OA\Response( + response: StatusCodeInterface::STATUS_OK, + description: 'My admin account', + content: new OA\JsonContent(ref: '#/components/schemas/Admin'), + ), + ], +)] + +/** + * @see PatchAdminAccountResourceHandler::handle() + */ +#[OA\Patch( + path: '/admin/account', + description: 'Authenticated (super)admin updates their own account data', + summary: 'Admin updates their own account', + security: [['AuthToken' => []]], + requestBody: new OA\RequestBody( + description: 'Update my admin account request', + required: true, + content: new OA\JsonContent( + properties: [ + new OA\Property(property: 'password', type: 'string'), + new OA\Property(property: 'passwordConfirm', type: 'string'), + new OA\Property(property: 'firstName', type: 'string'), + new OA\Property(property: 'lastName', type: 'string'), + new OA\Property( + property: 'roles', + type: 'array', + items: new OA\Items( + required: ['uuid'], + properties: [ + new OA\Property(property: 'uuid', type: 'string'), + ], + ), + ), + ], + type: 'object', + ), + ), + tags: ['Admin'], + responses: [ + new OA\Response( + response: StatusCodeInterface::STATUS_OK, + description: 'My admin account', + content: new OA\JsonContent(ref: '#/components/schemas/Admin'), + ), + ], +)] + /** * @see Admin */ @@ -530,6 +536,7 @@ new OA\Schema(ref: '#/components/schemas/Collection'), ], )] + /** * @see AdminRoleCollection */ diff --git a/src/Admin/src/RoutesDelegator.php b/src/Admin/src/RoutesDelegator.php index 06090a5b..2c315423 100644 --- a/src/Admin/src/RoutesDelegator.php +++ b/src/Admin/src/RoutesDelegator.php @@ -4,11 +4,15 @@ namespace Api\Admin; -use Api\Admin\Handler\AdminAccountHandler; -use Api\Admin\Handler\AdminCollectionHandler; -use Api\Admin\Handler\AdminHandler; -use Api\Admin\Handler\AdminRoleCollectionHandler; -use Api\Admin\Handler\AdminRoleHandler; +use Api\Admin\Handler\Account\GetAdminAccountResourceHandler; +use Api\Admin\Handler\Account\PatchAdminAccountResourceHandler; +use Api\Admin\Handler\Admin\DeleteAdminResourceHandler; +use Api\Admin\Handler\Admin\GetAdminCollectionHandler; +use Api\Admin\Handler\Admin\GetAdminResourceHandler; +use Api\Admin\Handler\Admin\PatchAdminResourceHandler; +use Api\Admin\Handler\Admin\PostAdminResourceHandler; +use Api\Admin\Handler\Admin\Role\GetAdminRoleCollectionHandler; +use Api\Admin\Handler\Admin\Role\GetAdminRoleResourceHandler; use Mezzio\Application; use Psr\Container\ContainerInterface; @@ -23,53 +27,18 @@ public function __invoke(ContainerInterface $container, string $serviceName, cal $uuid = \Api\App\RoutesDelegator::REGEXP_UUID; - $app->get( - '/admin/my-account', - AdminAccountHandler::class, - 'admin.my-account.view' - ); - $app->patch( - '/admin/my-account', - AdminAccountHandler::class, - 'admin.my-account.update' - ); + $app->get('/admin', GetAdminCollectionHandler::class, 'admin::list-admin'); + $app->post('/admin', PostAdminResourceHandler::class, 'admin::create-admin'); - $app->post( - '/admin', - AdminHandler::class, - 'admin.create' - ); - $app->delete( - '/admin/' . $uuid, - AdminHandler::class, - 'admin.delete' - ); - $app->get( - '/admin', - AdminCollectionHandler::class, - 'admin.list' - ); - $app->patch( - '/admin/' . $uuid, - AdminHandler::class, - 'admin.update' - ); - $app->get( - '/admin/' . $uuid, - AdminHandler::class, - 'admin.view' - ); + $app->delete('/admin/' . $uuid, DeleteAdminResourceHandler::class, 'admin::delete-admin'); + $app->get('/admin/' . $uuid, GetAdminResourceHandler::class, 'admin::view-admin'); + $app->patch('/admin/' . $uuid, PatchAdminResourceHandler::class, 'admin::update-admin'); - $app->get( - '/admin/role', - AdminRoleCollectionHandler::class, - 'admin.role.list' - ); - $app->get( - '/admin/role/' . $uuid, - AdminRoleHandler::class, - 'admin.role.view' - ); + $app->get('/admin/role', GetAdminRoleCollectionHandler::class, 'admin::list-role'); + $app->get('/admin/role/' . $uuid, GetAdminRoleResourceHandler::class, 'admin::view-role'); + + $app->get('/admin/account', GetAdminAccountResourceHandler::class, 'admin::view-account'); + $app->patch('/admin/account', PatchAdminAccountResourceHandler::class, 'admin::update-account'); return $app; } diff --git a/src/App/src/Command/RouteListCommand.php b/src/App/src/Command/RouteListCommand.php index a9eaf075..c034986e 100644 --- a/src/App/src/Command/RouteListCommand.php +++ b/src/App/src/Command/RouteListCommand.php @@ -13,12 +13,15 @@ use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; -use function array_map; +use function count; use function ksort; use function sprintf; use function str_contains; +use function str_pad; use function str_replace; +use const STR_PAD_LEFT; + #[AsCommand( name: 'route:list', description: 'List API routes', @@ -64,7 +67,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int continue; } - $routes[sprintf('%s:%s', $route->getName(), $method)] = [ + $routes[sprintf('%s-%s', $route->getPath(), $method)] = [ 'name' => $route->getName(), 'path' => $route->getPath(), 'method' => $method, @@ -73,17 +76,19 @@ protected function execute(InputInterface $input, OutputInterface $output): int } ksort($routes); - (new Table($output)) - ->setHeaders(['Method', 'Name', 'Path']) - ->setRows(array_map(function ($route) { - $path = str_replace(RoutesDelegator::REGEXP_UUID, '{uuid}', $route['path']); - return [ - $route['method'], - $route['name'], - $path, - ]; - }, $routes)) - ->render(); + $index = 1; + $table = (new Table($output)) + ->setHeaders([' #', 'Request method', 'Route name', 'Route path']) + ->setHeaderTitle(sprintf('%d Routes', count($routes))); + foreach ($routes as $route) { + $table->addRow([ + str_pad((string) $index++, 4, ' ', STR_PAD_LEFT), + $route['method'], + $route['name'], + str_replace(RoutesDelegator::REGEXP_UUID, '{uuid}', $route['path']), + ]); + } + $table->render(); return Command::SUCCESS; } diff --git a/src/App/src/ConfigProvider.php b/src/App/src/ConfigProvider.php index d5a4b993..04516261 100644 --- a/src/App/src/ConfigProvider.php +++ b/src/App/src/ConfigProvider.php @@ -10,12 +10,12 @@ use Api\App\Factory\HandlerDelegatorFactory; use Api\App\Factory\RouteListCommandFactory; use Api\App\Factory\TokenGenerateCommandFactory; -use Api\App\Handler\ErrorReportHandler; +use Api\App\Handler\PostErrorReportResourceHandler; use Api\App\Middleware\AuthenticationMiddleware; use Api\App\Middleware\AuthorizationMiddleware; use Api\App\Middleware\ContentNegotiationMiddleware; use Api\App\Middleware\DeprecationMiddleware; -use Api\App\Middleware\ErrorResponseMiddleware; +use Api\App\Middleware\ResponseMiddleware; use Api\App\Service\ErrorReportService; use Api\App\Service\ErrorReportServiceInterface; use Dot\DependencyInjection\Factory\AttributedServiceFactory; @@ -50,24 +50,24 @@ public function getDependencies(): array { return [ 'delegators' => [ - Application::class => [RoutesDelegator::class], - ErrorReportHandler::class => [HandlerDelegatorFactory::class], + Application::class => [RoutesDelegator::class], + PostErrorReportResourceHandler::class => [HandlerDelegatorFactory::class], ], 'factories' => [ - 'dot-mail.options.default' => MailOptionsAbstractFactory::class, - 'dot-mail.service.default' => MailServiceAbstractFactory::class, - AuthenticationMiddleware::class => AuthenticationMiddlewareFactory::class, - AuthorizationMiddleware::class => AttributedServiceFactory::class, - ContentNegotiationMiddleware::class => AttributedServiceFactory::class, - DeprecationMiddleware::class => AttributedServiceFactory::class, - Environment::class => TwigEnvironmentFactory::class, - ErrorReportHandler::class => AttributedServiceFactory::class, - ErrorReportService::class => AttributedServiceFactory::class, - ErrorResponseMiddleware::class => AttributedServiceFactory::class, - RouteListCommand::class => RouteListCommandFactory::class, - TokenGenerateCommand::class => TokenGenerateCommandFactory::class, - TwigExtension::class => TwigExtensionFactory::class, - TwigRenderer::class => TwigRendererFactory::class, + 'dot-mail.options.default' => MailOptionsAbstractFactory::class, + 'dot-mail.service.default' => MailServiceAbstractFactory::class, + AuthenticationMiddleware::class => AuthenticationMiddlewareFactory::class, + AuthorizationMiddleware::class => AttributedServiceFactory::class, + ContentNegotiationMiddleware::class => AttributedServiceFactory::class, + DeprecationMiddleware::class => AttributedServiceFactory::class, + Environment::class => TwigEnvironmentFactory::class, + PostErrorReportResourceHandler::class => AttributedServiceFactory::class, + ErrorReportService::class => AttributedServiceFactory::class, + ResponseMiddleware::class => AttributedServiceFactory::class, + RouteListCommand::class => RouteListCommandFactory::class, + TokenGenerateCommand::class => TokenGenerateCommandFactory::class, + TwigExtension::class => TwigExtensionFactory::class, + TwigRenderer::class => TwigRendererFactory::class, ], 'aliases' => [ Authentication\AuthenticationInterface::class => Authentication\OAuth2\OAuth2Adapter::class, @@ -83,6 +83,9 @@ public function getHalConfig(): array return []; } + /** + * @param class-string $collectionClass + */ public static function getCollection(string $collectionClass, string $route, string $collectionRelation): array { return [ @@ -93,6 +96,9 @@ public static function getCollection(string $collectionClass, string $route, str ]; } + /** + * @param class-string $resourceClass + */ public static function getResource( string $resourceClass, string $route, diff --git a/src/App/src/Handler/AbstractHandler.php b/src/App/src/Handler/AbstractHandler.php index 8cf3525d..fc0936b5 100644 --- a/src/App/src/Handler/AbstractHandler.php +++ b/src/App/src/Handler/AbstractHandler.php @@ -5,65 +5,25 @@ namespace Api\App\Handler; use Api\App\Collection\CollectionInterface; -use Api\App\Exception\BadRequestException; -use Api\App\Exception\ConflictException; -use Api\App\Exception\ExpiredException; -use Api\App\Exception\ForbiddenException; -use Api\App\Exception\MethodNotAllowedException; -use Api\App\Exception\NotFoundException; -use Api\App\Exception\RuntimeException; -use Api\App\Exception\UnauthorizedException; use Core\App\Entity\EntityInterface; -use Core\App\Message; -use Dot\Mail\Exception\MailException; -use Exception; use Fig\Http\Message\StatusCodeInterface; use Laminas\Diactoros\Response\EmptyResponse; use Laminas\Diactoros\Response\JsonResponse; use Mezzio\Hal\HalResponseFactory; use Mezzio\Hal\ResourceGenerator; -use Mezzio\Hal\ResourceGenerator\Exception\OutOfBoundsException; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\RequestHandlerInterface; use function assert; use function is_array; -use function method_exists; -use function strtolower; abstract class AbstractHandler implements RequestHandlerInterface { protected ?HalResponseFactory $responseFactory = null; protected ?ResourceGenerator $resourceGenerator = null; - public function handle(ServerRequestInterface $request): ResponseInterface - { - try { - $method = strtolower($request->getMethod()); - if (! method_exists($this, $method)) { - throw new MethodNotAllowedException(Message::METHOD_NOT_ALLOWED); - } - - return $this->$method($request); - } catch (ConflictException $exception) { - return $this->errorResponse($exception->getMessage(), StatusCodeInterface::STATUS_CONFLICT); - } catch (ForbiddenException $exception) { - return $this->errorResponse($exception->getMessage(), StatusCodeInterface::STATUS_FORBIDDEN); - } catch (ExpiredException $exception) { - return $this->errorResponse($exception->getMessage(), StatusCodeInterface::STATUS_GONE); - } catch (OutOfBoundsException | NotFoundException $exception) { - return $this->errorResponse($exception->getMessage(), StatusCodeInterface::STATUS_NOT_FOUND); - } catch (UnauthorizedException $exception) { - return $this->errorResponse($exception->getMessage(), StatusCodeInterface::STATUS_UNAUTHORIZED); - } catch (MethodNotAllowedException $exception) { - return $this->errorResponse($exception->getMessage(), StatusCodeInterface::STATUS_METHOD_NOT_ALLOWED); - } catch (BadRequestException $exception) { - return $this->errorResponse($exception->getMessages(), StatusCodeInterface::STATUS_BAD_REQUEST); - } catch (MailException | RuntimeException | Exception $exception) { - return $this->errorResponse($exception->getMessage()); - } - } + abstract public function handle(ServerRequestInterface $request): ResponseInterface; public function setResponseFactory(HalResponseFactory $responseFactory): self { @@ -79,18 +39,6 @@ public function setResourceGenerator(ResourceGenerator $resourceGenerator): self return $this; } - public function emptyResponse(int $status = StatusCodeInterface::STATUS_NO_CONTENT): ResponseInterface - { - return new EmptyResponse($status, ['Content-Type' => 'text/plain']); - } - - public function jsonResponse( - array|string $messages = [], - int $status = StatusCodeInterface::STATUS_OK - ): ResponseInterface { - return new JsonResponse($messages, $status); - } - public function createResponse( ServerRequestInterface $request, CollectionInterface|EntityInterface $instance @@ -111,35 +59,36 @@ public function createdResponse(ServerRequestInterface $request, EntityInterface return $response->withStatus(StatusCodeInterface::STATUS_CREATED); } - public function noContentResponse(): ResponseInterface - { - return $this->emptyResponse(); - } - - public function notFoundResponse(): ResponseInterface + public function emptyResponse(int $status = StatusCodeInterface::STATUS_NO_CONTENT): ResponseInterface { - return $this->emptyResponse(StatusCodeInterface::STATUS_NOT_FOUND); + return new EmptyResponse($status, ['Content-Type' => 'text/plain']); } - public function errorResponse( + public function infoResponse( array|string $messages = [], - int $status = StatusCodeInterface::STATUS_INTERNAL_SERVER_ERROR + int $status = StatusCodeInterface::STATUS_OK ): ResponseInterface { return $this->jsonResponse([ - 'error' => [ + 'info' => [ 'messages' => is_array($messages) ? $messages : [$messages], ], ], $status); } - public function infoResponse( + public function jsonResponse( array|string $messages = [], int $status = StatusCodeInterface::STATUS_OK ): ResponseInterface { - return $this->jsonResponse([ - 'info' => [ - 'messages' => is_array($messages) ? $messages : [$messages], - ], - ], $status); + return new JsonResponse($messages, $status); + } + + public function noContentResponse(): ResponseInterface + { + return $this->emptyResponse(); + } + + public function notFoundResponse(): ResponseInterface + { + return $this->emptyResponse(StatusCodeInterface::STATUS_NOT_FOUND); } } diff --git a/src/App/src/Handler/HomeHandler.php b/src/App/src/Handler/GetIndexResourceHandler.php similarity index 60% rename from src/App/src/Handler/HomeHandler.php rename to src/App/src/Handler/GetIndexResourceHandler.php index 3567376e..a9238f08 100644 --- a/src/App/src/Handler/HomeHandler.php +++ b/src/App/src/Handler/GetIndexResourceHandler.php @@ -6,16 +6,17 @@ use Api\App\Attribute\ResourceDeprecation; use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; #[ResourceDeprecation( sunset: '2038-01-01', link: 'https://docs.dotkernel.org/api-documentation/v5/core-features/versioning', deprecationReason: 'Resource deprecation example.', )] -class HomeHandler extends AbstractHandler +class GetIndexResourceHandler extends AbstractHandler { - public function get(): ResponseInterface + public function handle(ServerRequestInterface $request): ResponseInterface { - return $this->jsonResponse(['message' => 'DotKernel API version 5']); + return $this->jsonResponse(['message' => 'Dotkernel API version 5']); } } diff --git a/src/App/src/Handler/NotFoundHandler.php b/src/App/src/Handler/GetNotFoundViewHandler.php similarity index 84% rename from src/App/src/Handler/NotFoundHandler.php rename to src/App/src/Handler/GetNotFoundViewHandler.php index c108ad7f..5d1e7366 100644 --- a/src/App/src/Handler/NotFoundHandler.php +++ b/src/App/src/Handler/GetNotFoundViewHandler.php @@ -7,7 +7,7 @@ use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; -class NotFoundHandler extends AbstractHandler +class GetNotFoundViewHandler extends AbstractHandler { public function handle(ServerRequestInterface $request): ResponseInterface { diff --git a/src/App/src/Handler/ErrorReportHandler.php b/src/App/src/Handler/PostErrorReportResourceHandler.php similarity index 89% rename from src/App/src/Handler/ErrorReportHandler.php rename to src/App/src/Handler/PostErrorReportResourceHandler.php index 6939f54c..d9e500ef 100644 --- a/src/App/src/Handler/ErrorReportHandler.php +++ b/src/App/src/Handler/PostErrorReportResourceHandler.php @@ -15,7 +15,7 @@ use Psr\Http\Message\ServerRequestInterface; use RuntimeException; -class ErrorReportHandler extends AbstractHandler +class PostErrorReportResourceHandler extends AbstractHandler { #[Inject( ErrorReportServiceInterface::class, @@ -35,7 +35,7 @@ public function __construct( link: 'https://docs.dotkernel.org/api-documentation/v5/core-features/versioning', deprecationReason: 'Method deprecation example.', )] - public function post(ServerRequestInterface $request): ResponseInterface + public function handle(ServerRequestInterface $request): ResponseInterface { $this->errorReportService ->checkRequest($request) diff --git a/src/App/src/Middleware/DeprecationMiddleware.php b/src/App/src/Middleware/DeprecationMiddleware.php index 4ea7741e..f7939edb 100644 --- a/src/App/src/Middleware/DeprecationMiddleware.php +++ b/src/App/src/Middleware/DeprecationMiddleware.php @@ -30,7 +30,6 @@ use function implode; use function is_string; use function sprintf; -use function strtoupper; class DeprecationMiddleware implements MiddlewareInterface { @@ -79,7 +78,7 @@ public function process( } $this->validateAttributes($attributes); - $attribute = $this->getAttribute($attributes, $request->getMethod()); + $attribute = $this->getAttribute($attributes); if (null === $attribute) { return $response; } @@ -96,17 +95,22 @@ public function process( return $response; } - private function getAttribute(array $attributes, string $requestMethod): ?array + private function getAttribute(array $attributes): ?array { - $attribute = array_values(array_filter($attributes, function (array $attribute): bool { - return $attribute['deprecationType'] === self::RESOURCE_DEPRECATION_ATTRIBUTE; - }))[0] ?? null; + $attribute = array_values( + array_filter( + $attributes, + fn (array $attribute): bool => $attribute['deprecationType'] === self::RESOURCE_DEPRECATION_ATTRIBUTE + ) + )[0] ?? null; if (null === $attribute) { - $attribute = array_values(array_filter($attributes, function (array $attr) use ($requestMethod): bool { - return $attr['deprecationType'] === self::METHOD_DEPRECATION_ATTRIBUTE && - strtoupper($attr['identifier']) === strtoupper($requestMethod); - }))[0] ?? null; + $attribute = array_values( + array_filter( + $attributes, + fn (array $attribute): bool => $attribute['deprecationType'] === self::METHOD_DEPRECATION_ATTRIBUTE + ) + )[0] ?? null; } return $attribute; diff --git a/src/App/src/Middleware/ResponseMiddleware.php b/src/App/src/Middleware/ResponseMiddleware.php new file mode 100644 index 00000000..244f8766 --- /dev/null +++ b/src/App/src/Middleware/ResponseMiddleware.php @@ -0,0 +1,72 @@ +handle($request); + } catch (ConflictException $exception) { + return $this->errorResponse($exception->getMessage(), StatusCodeInterface::STATUS_CONFLICT); + } catch (ForbiddenException $exception) { + return $this->errorResponse($exception->getMessage(), StatusCodeInterface::STATUS_FORBIDDEN); + } catch (ExpiredException $exception) { + return $this->errorResponse($exception->getMessage(), StatusCodeInterface::STATUS_GONE); + } catch (OutOfBoundsException | NotFoundException $exception) { + return $this->errorResponse($exception->getMessage(), StatusCodeInterface::STATUS_NOT_FOUND); + } catch (UnauthorizedException $exception) { + return $this->errorResponse($exception->getMessage(), StatusCodeInterface::STATUS_UNAUTHORIZED); + } catch (MethodNotAllowedException $exception) { + return $this->errorResponse($exception->getMessage(), StatusCodeInterface::STATUS_METHOD_NOT_ALLOWED); + } catch (BadRequestException $exception) { + return $this->errorResponse($exception->getMessages(), StatusCodeInterface::STATUS_BAD_REQUEST); + } catch (MailException | RuntimeException | Exception $exception) { + return $this->errorResponse($exception->getMessage()); + } + } + + public function errorResponse( + array|string $messages = [], + int $status = StatusCodeInterface::STATUS_INTERNAL_SERVER_ERROR + ): ResponseInterface { + return new JsonResponse([ + 'error' => [ + 'messages' => is_array($messages) ? $messages : [$messages], + ], + ], $status); + } +} diff --git a/src/App/src/OpenAPI.php b/src/App/src/OpenAPI.php index 65869a46..75f8f002 100644 --- a/src/App/src/OpenAPI.php +++ b/src/App/src/OpenAPI.php @@ -4,13 +4,12 @@ namespace Api\App; -use Api\App\Handler\ErrorReportHandler; -use Api\App\Handler\HomeHandler; +use Api\App\Handler\GetIndexResourceHandler; +use Api\App\Handler\PostErrorReportResourceHandler; use Fig\Http\Message\StatusCodeInterface; -use Mezzio\Authentication\OAuth2\TokenEndpointHandler; use OpenApi\Attributes as OA; -#[OA\Info(version: '1.0', title: 'DotKernel API')] +#[OA\Info(version: '1.0', title: 'Dotkernel API')] #[OA\Server(url: 'http://api.dotkernel.localhost', description: 'Local development server')] #[OA\SecurityScheme(securityScheme: 'AuthToken', type: 'http', in: 'header', bearerFormat: 'JWT', scheme: 'bearer')] #[OA\SecurityScheme(securityScheme: 'ErrorReportingToken', type: 'apiKey', name: 'Error-Reporting-Token', in: 'header')] @@ -21,7 +20,7 @@ )] /** - * @see HomeHandler::get() + * @see GetIndexResourceHandler::handle() */ #[OA\Get( path: '/', @@ -42,7 +41,7 @@ )] /** - * @see ErrorReportHandler::post() + * @see PostErrorReportResourceHandler::handle() */ #[OA\Post( path: '/error-report', @@ -85,117 +84,12 @@ ], )] -/** - * @see TokenEndpointHandler::handle() - */ -#[OA\Post( - path: '/security/generate-token', - description: 'Client generates access token using username and password', - summary: 'Generate access token', - requestBody: new OA\RequestBody( - description: 'Access token generation request', - required: true, - content: new OA\JsonContent( - properties: [ - new OA\Property(property: 'grant_type', type: 'string', default: 'password'), - new OA\Property(property: 'client_id', type: 'string', enum: ['admin', 'frontend']), - new OA\Property(property: 'client_secret', type: 'string', enum: ['admin', 'frontend']), - new OA\Property(property: 'scope', type: 'string', default: 'api'), - new OA\Property(property: 'username', type: 'string'), - new OA\Property(property: 'password', type: 'string'), - ], - type: 'object', - ), - ), - tags: ['AccessToken'], - responses: [ - new OA\Response( - response: StatusCodeInterface::STATUS_OK, - description: 'OK', - content: new OA\JsonContent(ref: '#/components/schemas/OAuth2SuccessMessage'), - ), - new OA\Response( - response: StatusCodeInterface::STATUS_BAD_REQUEST, - description: 'Bad Request', - content: new OA\JsonContent(ref: '#/components/schemas/OAuth2GenerateErrorMessage'), - ), - ], -)] - -/** - * @see TokenEndpointHandler::handle() - */ -#[OA\Post( - path: '/security/refresh-token', - description: 'Client refreshes access token using refresh token', - summary: 'Refresh access token', - requestBody: new OA\RequestBody( - description: 'Access token refresh request', - required: true, - content: new OA\JsonContent( - properties: [ - new OA\Property(property: 'grant_type', type: 'string', default: 'refresh_token'), - new OA\Property(property: 'client_id', type: 'string', enum: ['admin', 'frontend']), - new OA\Property(property: 'client_secret', type: 'string', enum: ['admin', 'frontend']), - new OA\Property(property: 'scope', type: 'string', default: 'api'), - new OA\Property(property: 'refresh_token', type: 'string'), - ], - type: 'object', - ), - ), - tags: ['AccessToken'], - responses: [ - new OA\Response( - response: StatusCodeInterface::STATUS_OK, - description: 'OK', - content: new OA\JsonContent(ref: '#/components/schemas/OAuth2SuccessMessage'), - ), - new OA\Response( - response: StatusCodeInterface::STATUS_UNAUTHORIZED, - description: 'Unauthorized', - content: new OA\JsonContent(ref: '#/components/schemas/OAuth2RefreshErrorMessage'), - ), - ], -)] - -#[OA\Schema( - schema: 'OAuth2GenerateErrorMessage', - properties: [ - new OA\Property(property: 'error', type: 'string'), - new OA\Property(property: 'error_description', type: 'string'), - new OA\Property(property: 'message', type: 'string'), - ], - type: 'object', -)] - -#[OA\Schema( - schema: 'OAuth2RefreshErrorMessage', - properties: [ - new OA\Property(property: 'hint', type: 'string'), - ], - type: 'object', - allOf: [ - new OA\Schema(ref: '#/components/schemas/OAuth2GenerateErrorMessage'), - ], -)] - -#[OA\Schema( - schema: 'OAuth2SuccessMessage', - properties: [ - new OA\Property(property: 'token_type', type: 'string', default: 'Bearer'), - new OA\Property(property: 'expires_in', type: 'integer', default: 86400), - new OA\Property(property: 'access_token', type: 'string'), - new OA\Property(property: 'refresh_token', type: 'string'), - ], - type: 'object', -)] - #[OA\Schema( schema: 'HomeMessage', properties: [ - new OA\Property(property: 'message', type: 'string', default: 'DotKernel API version 5'), + new OA\Property(property: 'message', type: 'string', default: 'Dotkernel API version 5'), ], - type: 'object' + type: 'object', )] #[OA\Schema( diff --git a/src/App/src/RoutesDelegator.php b/src/App/src/RoutesDelegator.php index 61f7a0a7..cd249966 100644 --- a/src/App/src/RoutesDelegator.php +++ b/src/App/src/RoutesDelegator.php @@ -4,11 +4,9 @@ namespace Api\App; -use Api\App\Handler\ErrorReportHandler; -use Api\App\Handler\HomeHandler; -use Api\App\Middleware\ErrorResponseMiddleware; +use Api\App\Handler\GetIndexResourceHandler; +use Api\App\Handler\PostErrorReportResourceHandler; use Mezzio\Application; -use Mezzio\Authentication\OAuth2\TokenEndpointHandler; use Psr\Container\ContainerInterface; use function assert; @@ -22,45 +20,11 @@ public function __invoke(ContainerInterface $container, string $serviceName, cal $app = $callback(); assert($app instanceof Application); - /** - * Home page - */ - $app->get( - '/', - HomeHandler::class, - 'home' - ); + // Home page + $app->get('/', GetIndexResourceHandler::class, 'app::view-index'); - /** - * OAuth authentication - */ - $app->post( - '/security/generate-token', - [ - ErrorResponseMiddleware::class, - TokenEndpointHandler::class, - ], - 'security.generate-token' - ); - $app->post( - '/security/refresh-token', - [ - ErrorResponseMiddleware::class, - TokenEndpointHandler::class, - ], - 'security.refresh-token' - ); - - /** - * Other application reports an error - */ - $app->post( - '/error-report', - [ - ErrorReportHandler::class, - ], - 'error.report' - ); + // Other application reports an error + $app->post('/error-report', PostErrorReportResourceHandler::class, 'app::create-error-report'); return $app; } diff --git a/src/Core/src/App/src/Fixture/AdminLoader.php b/src/Core/src/App/src/Fixture/AdminLoader.php index 48c060b1..89049c75 100644 --- a/src/Core/src/App/src/Fixture/AdminLoader.php +++ b/src/Core/src/App/src/Fixture/AdminLoader.php @@ -32,7 +32,7 @@ public function load(ObjectManager $manager): void $admin = (new Admin()) ->setIdentity('admin') ->usePassword('dotkernel') - ->setFirstName('DotKernel') + ->setFirstName('Dotkernel') ->setLastName('Admin') ->setStatus(AdminStatusEnum::Active) ->addRole($adminRole) diff --git a/src/Core/src/App/src/Message.php b/src/Core/src/App/src/Message.php index b0aecd32..4987f46e 100644 --- a/src/Core/src/App/src/Message.php +++ b/src/Core/src/App/src/Message.php @@ -39,6 +39,8 @@ class Message public const SERVICE_NOT_FOUND = 'Service %s not found in container.'; public const USER_ACTIVATED = 'This account has been activated.'; public const USER_ALREADY_ACTIVATED = 'This account is already active.'; + public const USER_ALREADY_DEACTIVATED = 'This account is already inactive.'; + public const USER_DEACTIVATED = 'This account has been deactivated.'; public const USER_NOT_ACTIVATED = 'User account must be activated first.'; public const USER_NOT_FOUND = 'User not found.'; public const USER_NOT_FOUND_BY_IDENTITY = 'Could not find account by identity \'%s\''; diff --git a/src/Security/src/ConfigProvider.php b/src/Security/src/ConfigProvider.php new file mode 100644 index 00000000..a8c54d7b --- /dev/null +++ b/src/Security/src/ConfigProvider.php @@ -0,0 +1,31 @@ + $this->getDependencies(), + ]; + } + + public function getDependencies(): array + { + return [ + 'delegators' => [ + Application::class => [RoutesDelegator::class], + ], + 'factories' => [ + ErrorResponseMiddleware::class => AttributedServiceFactory::class, + ], + ]; + } +} diff --git a/src/App/src/Middleware/ErrorResponseMiddleware.php b/src/Security/src/Middleware/ErrorResponseMiddleware.php similarity index 97% rename from src/App/src/Middleware/ErrorResponseMiddleware.php rename to src/Security/src/Middleware/ErrorResponseMiddleware.php index 74db3714..dac8fca1 100644 --- a/src/App/src/Middleware/ErrorResponseMiddleware.php +++ b/src/Security/src/Middleware/ErrorResponseMiddleware.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Api\App\Middleware; +namespace Api\Security\Middleware; use Dot\DependencyInjection\Attribute\Inject; use Fig\Http\Message\StatusCodeInterface; diff --git a/src/Security/src/OpenAPI.php b/src/Security/src/OpenAPI.php new file mode 100644 index 00000000..8a6e8731 --- /dev/null +++ b/src/Security/src/OpenAPI.php @@ -0,0 +1,118 @@ +post( + '/security/token', + [ErrorResponseMiddleware::class, TokenEndpointHandler::class], + 'security::token' + ); + + return $app; + } +} diff --git a/src/User/src/ConfigProvider.php b/src/User/src/ConfigProvider.php index 83774acc..8cb9ba26 100644 --- a/src/User/src/ConfigProvider.php +++ b/src/User/src/ConfigProvider.php @@ -8,17 +8,31 @@ use Api\App\Factory\HandlerDelegatorFactory; use Api\User\Collection\UserCollection; use Api\User\Collection\UserRoleCollection; -use Api\User\Handler\AccountActivateHandler; -use Api\User\Handler\AccountAvatarHandler; -use Api\User\Handler\AccountHandler; -use Api\User\Handler\AccountRecoveryHandler; -use Api\User\Handler\AccountResetPasswordHandler; -use Api\User\Handler\UserActivateHandler; -use Api\User\Handler\UserAvatarHandler; -use Api\User\Handler\UserCollectionHandler; -use Api\User\Handler\UserHandler; -use Api\User\Handler\UserRoleCollectionHandler; -use Api\User\Handler\UserRoleHandler; +use Api\User\Handler\Account\Avatar\DeleteUserAccountAvatarHandler; +use Api\User\Handler\Account\Avatar\GetUserAccountAvatarHandler; +use Api\User\Handler\Account\Avatar\PostUserAccountAvatarHandler; +use Api\User\Handler\Account\DeleteUserAccountResourceHandler; +use Api\User\Handler\Account\GetUserAccountResourceHandler; +use Api\User\Handler\Account\PatchUserAccountActivateHandler; +use Api\User\Handler\Account\PatchUserAccountResourceHandler; +use Api\User\Handler\Account\PostUserAccountActivateHandler; +use Api\User\Handler\Account\PostUserAccountRecoverHandler; +use Api\User\Handler\Account\PostUserAccountResourceHandler; +use Api\User\Handler\Account\ResetPassword\GetUserAccountResetPasswordHandler; +use Api\User\Handler\Account\ResetPassword\PatchUserAccountResetPasswordHandler; +use Api\User\Handler\Account\ResetPassword\PostUserAccountResetPasswordHandler; +use Api\User\Handler\User\Avatar\DeleteUserAvatarResourceHandler; +use Api\User\Handler\User\Avatar\GetUserAvatarResourceHandler; +use Api\User\Handler\User\Avatar\PostUserAvatarResourceHandler; +use Api\User\Handler\User\DeleteUserResourceHandler; +use Api\User\Handler\User\GetUserCollectionHandler; +use Api\User\Handler\User\GetUserResourceHandler; +use Api\User\Handler\User\PatchUserActivateHandler; +use Api\User\Handler\User\PatchUserDeactivateHandler; +use Api\User\Handler\User\PatchUserResourceHandler; +use Api\User\Handler\User\PostUserResourceHandler; +use Api\User\Handler\User\Role\GetUserRoleCollectionHandler; +use Api\User\Handler\User\Role\GetUserRoleResourceHandler; use Api\User\Service\UserAvatarService; use Api\User\Service\UserAvatarServiceInterface; use Api\User\Service\UserRoleService; @@ -54,40 +68,68 @@ public function getDependencies(): array { return [ 'delegators' => [ - Application::class => [RoutesDelegator::class], - AccountActivateHandler::class => [HandlerDelegatorFactory::class], - AccountAvatarHandler::class => [HandlerDelegatorFactory::class], - AccountHandler::class => [HandlerDelegatorFactory::class], - AccountRecoveryHandler::class => [HandlerDelegatorFactory::class], - AccountResetPasswordHandler::class => [HandlerDelegatorFactory::class], - UserActivateHandler::class => [HandlerDelegatorFactory::class], - UserAvatarHandler::class => [HandlerDelegatorFactory::class], - UserCollectionHandler::class => [HandlerDelegatorFactory::class], - UserHandler::class => [HandlerDelegatorFactory::class], - UserRoleCollectionHandler::class => [HandlerDelegatorFactory::class], - UserRoleHandler::class => [HandlerDelegatorFactory::class], + Application::class => [RoutesDelegator::class], + DeleteUserAccountAvatarHandler::class => [HandlerDelegatorFactory::class], + DeleteUserAccountResourceHandler::class => [HandlerDelegatorFactory::class], + DeleteUserAvatarResourceHandler::class => [HandlerDelegatorFactory::class], + DeleteUserResourceHandler::class => [HandlerDelegatorFactory::class], + GetUserAccountAvatarHandler::class => [HandlerDelegatorFactory::class], + GetUserAccountResetPasswordHandler::class => [HandlerDelegatorFactory::class], + GetUserAccountResourceHandler::class => [HandlerDelegatorFactory::class], + GetUserAvatarResourceHandler::class => [HandlerDelegatorFactory::class], + GetUserCollectionHandler::class => [HandlerDelegatorFactory::class], + GetUserResourceHandler::class => [HandlerDelegatorFactory::class], + GetUserRoleCollectionHandler::class => [HandlerDelegatorFactory::class], + GetUserRoleResourceHandler::class => [HandlerDelegatorFactory::class], + PatchUserAccountActivateHandler::class => [HandlerDelegatorFactory::class], + PatchUserAccountResetPasswordHandler::class => [HandlerDelegatorFactory::class], + PatchUserAccountResourceHandler::class => [HandlerDelegatorFactory::class], + PatchUserActivateHandler::class => [HandlerDelegatorFactory::class], + PatchUserDeactivateHandler::class => [HandlerDelegatorFactory::class], + PatchUserResourceHandler::class => [HandlerDelegatorFactory::class], + PostUserAccountActivateHandler::class => [HandlerDelegatorFactory::class], + PostUserAccountAvatarHandler::class => [HandlerDelegatorFactory::class], + PostUserAccountRecoverHandler::class => [HandlerDelegatorFactory::class], + PostUserAccountResetPasswordHandler::class => [HandlerDelegatorFactory::class], + PostUserAccountResourceHandler::class => [HandlerDelegatorFactory::class], + PostUserAvatarResourceHandler::class => [HandlerDelegatorFactory::class], + PostUserResourceHandler::class => [HandlerDelegatorFactory::class], ], 'factories' => [ - AccountActivateHandler::class => AttributedServiceFactory::class, - AccountAvatarHandler::class => AttributedServiceFactory::class, - AccountHandler::class => AttributedServiceFactory::class, - AccountRecoveryHandler::class => AttributedServiceFactory::class, - AccountResetPasswordHandler::class => AttributedServiceFactory::class, - UserActivateHandler::class => AttributedServiceFactory::class, - UserAvatarEventListener::class => AttributedServiceFactory::class, - UserAvatarHandler::class => AttributedServiceFactory::class, - UserCollectionHandler::class => AttributedServiceFactory::class, - UserHandler::class => AttributedServiceFactory::class, - UserRoleCollectionHandler::class => AttributedServiceFactory::class, - UserRoleHandler::class => AttributedServiceFactory::class, - UserService::class => AttributedServiceFactory::class, - UserRoleService::class => AttributedServiceFactory::class, - UserAvatarService::class => AttributedServiceFactory::class, - UserAvatarRepository::class => AttributedRepositoryFactory::class, - UserDetailRepository::class => AttributedRepositoryFactory::class, - UserRepository::class => AttributedRepositoryFactory::class, - UserResetPasswordRepository::class => AttributedRepositoryFactory::class, - UserRoleRepository::class => AttributedRepositoryFactory::class, + UserAvatarEventListener::class => AttributedServiceFactory::class, + UserService::class => AttributedServiceFactory::class, + UserRoleService::class => AttributedServiceFactory::class, + UserAvatarService::class => AttributedServiceFactory::class, + DeleteUserAccountAvatarHandler::class => AttributedServiceFactory::class, + DeleteUserAccountResourceHandler::class => AttributedServiceFactory::class, + DeleteUserAvatarResourceHandler::class => AttributedServiceFactory::class, + DeleteUserResourceHandler::class => AttributedServiceFactory::class, + GetUserAccountAvatarHandler::class => AttributedServiceFactory::class, + GetUserAccountResetPasswordHandler::class => AttributedServiceFactory::class, + GetUserAccountResourceHandler::class => AttributedServiceFactory::class, + GetUserAvatarResourceHandler::class => AttributedServiceFactory::class, + GetUserCollectionHandler::class => AttributedServiceFactory::class, + GetUserResourceHandler::class => AttributedServiceFactory::class, + GetUserRoleCollectionHandler::class => AttributedServiceFactory::class, + GetUserRoleResourceHandler::class => AttributedServiceFactory::class, + PatchUserAccountActivateHandler::class => AttributedServiceFactory::class, + PatchUserAccountResetPasswordHandler::class => AttributedServiceFactory::class, + PatchUserAccountResourceHandler::class => AttributedServiceFactory::class, + PatchUserActivateHandler::class => AttributedServiceFactory::class, + PatchUserDeactivateHandler::class => AttributedServiceFactory::class, + PatchUserResourceHandler::class => AttributedServiceFactory::class, + PostUserAccountActivateHandler::class => AttributedServiceFactory::class, + PostUserAccountAvatarHandler::class => AttributedServiceFactory::class, + PostUserAccountRecoverHandler::class => AttributedServiceFactory::class, + PostUserAccountResetPasswordHandler::class => AttributedServiceFactory::class, + PostUserAccountResourceHandler::class => AttributedServiceFactory::class, + PostUserAvatarResourceHandler::class => AttributedServiceFactory::class, + PostUserResourceHandler::class => AttributedServiceFactory::class, + UserAvatarRepository::class => AttributedRepositoryFactory::class, + UserDetailRepository::class => AttributedRepositoryFactory::class, + UserRepository::class => AttributedRepositoryFactory::class, + UserResetPasswordRepository::class => AttributedRepositoryFactory::class, + UserRoleRepository::class => AttributedRepositoryFactory::class, ], 'aliases' => [ UserAvatarServiceInterface::class => UserAvatarService::class, @@ -100,11 +142,11 @@ public function getDependencies(): array public function getHalConfig(): array { return [ - AppConfigProvider::getCollection(UserCollection::class, 'user.list', 'users'), - AppConfigProvider::getCollection(UserRoleCollection::class, 'user.role.list', 'roles'), - AppConfigProvider::getResource(User::class, 'user.view'), - AppConfigProvider::getResource(UserRole::class, 'user.role.view'), - AppConfigProvider::getResource(UserAvatar::class, 'user.avatar.view'), + AppConfigProvider::getCollection(UserCollection::class, 'user::list-user', 'users'), + AppConfigProvider::getCollection(UserRoleCollection::class, 'user::list-role', 'roles'), + AppConfigProvider::getResource(User::class, 'user::view-user'), + AppConfigProvider::getResource(UserRole::class, 'user::view-role'), + AppConfigProvider::getResource(UserAvatar::class, 'user::view-user-avatar'), ]; } diff --git a/src/User/src/Handler/Account/Avatar/DeleteUserAccountAvatarHandler.php b/src/User/src/Handler/Account/Avatar/DeleteUserAccountAvatarHandler.php new file mode 100644 index 00000000..e9d575db --- /dev/null +++ b/src/User/src/Handler/Account/Avatar/DeleteUserAccountAvatarHandler.php @@ -0,0 +1,40 @@ +getAttribute(User::class); + if (! $user->hasAvatar()) { + throw new NotFoundException(Message::AVATAR_MISSING); + } + + $this->userAvatarService->removeAvatar($user); + + return $this->noContentResponse(); + } +} diff --git a/src/User/src/Handler/Account/Avatar/GetUserAccountAvatarHandler.php b/src/User/src/Handler/Account/Avatar/GetUserAccountAvatarHandler.php new file mode 100644 index 00000000..1d3d5a1d --- /dev/null +++ b/src/User/src/Handler/Account/Avatar/GetUserAccountAvatarHandler.php @@ -0,0 +1,38 @@ +getAttribute(User::class); + if (! $user->hasAvatar()) { + throw new NotFoundException(Message::AVATAR_MISSING); + } + + return $this->createResponse($request, $user->getAvatar()); + } +} diff --git a/src/User/src/Handler/AccountAvatarHandler.php b/src/User/src/Handler/Account/Avatar/PostUserAccountAvatarHandler.php similarity index 52% rename from src/User/src/Handler/AccountAvatarHandler.php rename to src/User/src/Handler/Account/Avatar/PostUserAccountAvatarHandler.php index fd6433e9..82030523 100644 --- a/src/User/src/Handler/AccountAvatarHandler.php +++ b/src/User/src/Handler/Account/Avatar/PostUserAccountAvatarHandler.php @@ -2,20 +2,18 @@ declare(strict_types=1); -namespace Api\User\Handler; +namespace Api\User\Handler\Account\Avatar; use Api\App\Exception\BadRequestException; -use Api\App\Exception\NotFoundException; use Api\App\Handler\AbstractHandler; use Api\User\InputFilter\UpdateAvatarInputFilter; use Api\User\Service\UserAvatarServiceInterface; -use Core\App\Message; use Core\User\Entity\User; use Dot\DependencyInjection\Attribute\Inject; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; -class AccountAvatarHandler extends AbstractHandler +class PostUserAccountAvatarHandler extends AbstractHandler { #[Inject( UserAvatarServiceInterface::class, @@ -25,38 +23,10 @@ public function __construct( ) { } - /** - * @throws NotFoundException - */ - public function delete(ServerRequestInterface $request): ResponseInterface - { - $user = $request->getAttribute(User::class); - if (! $user->hasAvatar()) { - throw new NotFoundException(Message::AVATAR_MISSING); - } - - $this->userAvatarService->removeAvatar($user); - - return $this->noContentResponse(); - } - - /** - * @throws NotFoundException - */ - public function get(ServerRequestInterface $request): ResponseInterface - { - $user = $request->getAttribute(User::class); - if (! $user->hasAvatar()) { - throw new NotFoundException(Message::AVATAR_MISSING); - } - - return $this->createResponse($request, $user->getAvatar()); - } - /** * @throws BadRequestException */ - public function post(ServerRequestInterface $request): ResponseInterface + public function handle(ServerRequestInterface $request): ResponseInterface { $inputFilter = (new UpdateAvatarInputFilter())->setData($request->getUploadedFiles()); if (! $inputFilter->isValid()) { diff --git a/src/User/src/Handler/Account/DeleteUserAccountResourceHandler.php b/src/User/src/Handler/Account/DeleteUserAccountResourceHandler.php new file mode 100644 index 00000000..8f2e8b06 --- /dev/null +++ b/src/User/src/Handler/Account/DeleteUserAccountResourceHandler.php @@ -0,0 +1,34 @@ +userService->deleteUser($request->getAttribute(User::class)); + + return $this->noContentResponse(); + } +} diff --git a/src/User/src/Handler/Account/GetUserAccountResourceHandler.php b/src/User/src/Handler/Account/GetUserAccountResourceHandler.php new file mode 100644 index 00000000..6f547bdd --- /dev/null +++ b/src/User/src/Handler/Account/GetUserAccountResourceHandler.php @@ -0,0 +1,28 @@ +createResponse($request, $request->getAttribute(User::class)); + } +} diff --git a/src/User/src/Handler/Account/PatchUserAccountActivateHandler.php b/src/User/src/Handler/Account/PatchUserAccountActivateHandler.php new file mode 100644 index 00000000..7f13d826 --- /dev/null +++ b/src/User/src/Handler/Account/PatchUserAccountActivateHandler.php @@ -0,0 +1,41 @@ +userService->findOneBy(['hash' => $request->getAttribute('hash')]); + if ($user->isActive()) { + throw new ConflictException(Message::USER_ALREADY_ACTIVATED); + } + + $this->userService->activateUser($user); + + return $this->infoResponse(Message::USER_ACTIVATED); + } +} diff --git a/src/User/src/Handler/Account/PatchUserAccountResourceHandler.php b/src/User/src/Handler/Account/PatchUserAccountResourceHandler.php new file mode 100644 index 00000000..58d59f03 --- /dev/null +++ b/src/User/src/Handler/Account/PatchUserAccountResourceHandler.php @@ -0,0 +1,46 @@ +setValidationGroup(['password', 'passwordConfirm', 'detail']) + ->setData((array) $request->getParsedBody()); + if (! $inputFilter->isValid()) { + throw (new BadRequestException())->setMessages($inputFilter->getMessages()); + } + + $user = $this->userService->updateUser($request->getAttribute(User::class), $inputFilter->getValues()); + + return $this->createResponse($request, $user); + } +} diff --git a/src/User/src/Handler/AccountActivateHandler.php b/src/User/src/Handler/Account/PostUserAccountActivateHandler.php similarity index 71% rename from src/User/src/Handler/AccountActivateHandler.php rename to src/User/src/Handler/Account/PostUserAccountActivateHandler.php index 878902ed..ed86dd1e 100644 --- a/src/User/src/Handler/AccountActivateHandler.php +++ b/src/User/src/Handler/Account/PostUserAccountActivateHandler.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Api\User\Handler; +namespace Api\User\Handler\Account; use Api\App\Exception\BadRequestException; use Api\App\Exception\ConflictException; @@ -19,7 +19,7 @@ use function sprintf; -class AccountActivateHandler extends AbstractHandler +class PostUserAccountActivateHandler extends AbstractHandler { #[Inject( UserServiceInterface::class, @@ -29,29 +29,13 @@ public function __construct( ) { } - /** - * @throws ConflictException - * @throws NotFoundException - */ - public function patch(ServerRequestInterface $request): ResponseInterface - { - $user = $this->userService->findOneBy(['hash' => $request->getAttribute('hash')]); - if ($user->isActive()) { - throw new ConflictException(Message::USER_ALREADY_ACTIVATED); - } - - $this->userService->activateUser($user); - - return $this->infoResponse(Message::USER_ACTIVATED); - } - /** * @throws BadRequestException * @throws ConflictException * @throws MailException * @throws NotFoundException */ - public function post(ServerRequestInterface $request): ResponseInterface + public function handle(ServerRequestInterface $request): ResponseInterface { $inputFilter = (new ActivateAccountInputFilter())->setData((array) $request->getParsedBody()); if (! $inputFilter->isValid()) { diff --git a/src/User/src/Handler/AccountRecoveryHandler.php b/src/User/src/Handler/Account/PostUserAccountRecoverHandler.php similarity index 87% rename from src/User/src/Handler/AccountRecoveryHandler.php rename to src/User/src/Handler/Account/PostUserAccountRecoverHandler.php index 23532d75..502e4cf3 100644 --- a/src/User/src/Handler/AccountRecoveryHandler.php +++ b/src/User/src/Handler/Account/PostUserAccountRecoverHandler.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Api\User\Handler; +namespace Api\User\Handler\Account; use Api\App\Exception\BadRequestException; use Api\App\Exception\NotFoundException; @@ -15,7 +15,7 @@ use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; -class AccountRecoveryHandler extends AbstractHandler +class PostUserAccountRecoverHandler extends AbstractHandler { #[Inject( UserServiceInterface::class, @@ -30,7 +30,7 @@ public function __construct( * @throws MailException * @throws NotFoundException */ - public function post(ServerRequestInterface $request): ResponseInterface + public function handle(ServerRequestInterface $request): ResponseInterface { $inputFilter = (new RecoverIdentityInputFilter())->setData((array) $request->getParsedBody()); if (! $inputFilter->isValid()) { diff --git a/src/User/src/Handler/Account/PostUserAccountResourceHandler.php b/src/User/src/Handler/Account/PostUserAccountResourceHandler.php new file mode 100644 index 00000000..9b5607a1 --- /dev/null +++ b/src/User/src/Handler/Account/PostUserAccountResourceHandler.php @@ -0,0 +1,48 @@ +setValidationGroup(['identity', 'password', 'passwordConfirm', 'detail']) + ->setData((array) $request->getParsedBody()); + if (! $inputFilter->isValid()) { + throw (new BadRequestException())->setMessages($inputFilter->getMessages()); + } + + $user = $this->userService->createUser($inputFilter->getValues()); + $this->userService->sendActivationMail($user); + + return $this->createdResponse($request, $user); + } +} diff --git a/src/User/src/Handler/Account/ResetPassword/GetUserAccountResetPasswordHandler.php b/src/User/src/Handler/Account/ResetPassword/GetUserAccountResetPasswordHandler.php new file mode 100644 index 00000000..4af88caf --- /dev/null +++ b/src/User/src/Handler/Account/ResetPassword/GetUserAccountResetPasswordHandler.php @@ -0,0 +1,46 @@ +getAttribute('hash'); + + $userResetPassword = $this->userService->findResetPasswordByHash($hash); + if (! $userResetPassword->isValid()) { + throw new ExpiredException(sprintf(Message::RESET_PASSWORD_EXPIRED, $hash)); + } + if ($userResetPassword->isCompleted()) { + throw new ExpiredException(sprintf(Message::RESET_PASSWORD_USED, $hash)); + } + + return $this->infoResponse(sprintf(Message::RESET_PASSWORD_VALID, $hash)); + } +} diff --git a/src/User/src/Handler/Account/ResetPassword/PatchUserAccountResetPasswordHandler.php b/src/User/src/Handler/Account/ResetPassword/PatchUserAccountResetPasswordHandler.php new file mode 100644 index 00000000..751a2f15 --- /dev/null +++ b/src/User/src/Handler/Account/ResetPassword/PatchUserAccountResetPasswordHandler.php @@ -0,0 +1,65 @@ +getAttribute('hash'); + + $userResetPassword = $this->userService->findResetPasswordByHash($hash); + if (! $userResetPassword->isValid()) { + throw new ExpiredException(sprintf(Message::RESET_PASSWORD_EXPIRED, $hash)); + } + if ($userResetPassword->isCompleted()) { + throw new ConflictException(sprintf(Message::RESET_PASSWORD_USED, $hash)); + } + + $inputFilter = (new UpdatePasswordInputFilter())->setData((array) $request->getParsedBody()); + if (! $inputFilter->isValid()) { + throw (new BadRequestException())->setMessages($inputFilter->getMessages()); + } + + $this->userService->updateUser( + $userResetPassword->markAsCompleted()->getUser(), + $inputFilter->getValues() + ); + + $this->userService->sendResetPasswordCompletedMail($userResetPassword->getUser()); + + return $this->infoResponse(Message::RESET_PASSWORD_OK); + } +} diff --git a/src/User/src/Handler/Account/ResetPassword/PostUserAccountResetPasswordHandler.php b/src/User/src/Handler/Account/ResetPassword/PostUserAccountResetPasswordHandler.php new file mode 100644 index 00000000..e394a680 --- /dev/null +++ b/src/User/src/Handler/Account/ResetPassword/PostUserAccountResetPasswordHandler.php @@ -0,0 +1,61 @@ +setData((array) $request->getParsedBody()); + if (! $inputFilter->isValid()) { + throw (new BadRequestException())->setMessages($inputFilter->getMessages()); + } + + if (! empty($inputFilter->getValue('email'))) { + $user = $this->userService->findByEmail($inputFilter->getValue('email')); + } elseif (! empty($inputFilter->getValue('identity'))) { + $user = $this->userService->findByIdentity($inputFilter->getValue('identity')); + } else { + $user = null; + } + + if (! $user instanceof User) { + throw new NotFoundException(Message::USER_NOT_FOUND); + } + + $this->userService->updateUser($user->createResetPassword()); + $this->userService->sendResetPasswordRequestedMail($user); + + return $this->infoResponse(Message::MAIL_SENT_RESET_PASSWORD, StatusCodeInterface::STATUS_CREATED); + } +} diff --git a/src/User/src/Handler/AccountHandler.php b/src/User/src/Handler/AccountHandler.php deleted file mode 100644 index d49be0e6..00000000 --- a/src/User/src/Handler/AccountHandler.php +++ /dev/null @@ -1,85 +0,0 @@ -userService->deleteUser($request->getAttribute(User::class)); - - return $this->noContentResponse(); - } - - public function get(ServerRequestInterface $request): ResponseInterface - { - return $this->createResponse($request, $request->getAttribute(User::class)); - } - - /** - * @throws BadRequestException - * @throws ConflictException - * @throws NotFoundException - */ - public function patch(ServerRequestInterface $request): ResponseInterface - { - $inputFilter = (new UpdateUserInputFilter()) - ->setValidationGroup(['password', 'passwordConfirm', 'detail']) - ->setData((array) $request->getParsedBody()); - if (! $inputFilter->isValid()) { - throw (new BadRequestException())->setMessages($inputFilter->getMessages()); - } - - $user = $this->userService->updateUser($request->getAttribute(User::class), $inputFilter->getValues()); - - return $this->createResponse($request, $user); - } - - /** - * @throws BadRequestException - * @throws ConflictException - * @throws MailException - * @throws NotFoundException - */ - public function post(ServerRequestInterface $request): ResponseInterface - { - $inputFilter = (new CreateUserInputFilter()) - ->setValidationGroup(['identity', 'password', 'passwordConfirm', 'detail']) - ->setData((array) $request->getParsedBody()); - if (! $inputFilter->isValid()) { - throw (new BadRequestException())->setMessages($inputFilter->getMessages()); - } - - $user = $this->userService->createUser($inputFilter->getValues()); - $this->userService->sendActivationMail($user); - - return $this->createdResponse($request, $user); - } -} diff --git a/src/User/src/Handler/AccountResetPasswordHandler.php b/src/User/src/Handler/AccountResetPasswordHandler.php deleted file mode 100644 index 3bb0f06b..00000000 --- a/src/User/src/Handler/AccountResetPasswordHandler.php +++ /dev/null @@ -1,118 +0,0 @@ -getAttribute('hash'); - - $userResetPassword = $this->userService->findResetPasswordByHash($hash); - if (! $userResetPassword->isValid()) { - throw new ExpiredException(sprintf(Message::RESET_PASSWORD_EXPIRED, $hash)); - } - if ($userResetPassword->isCompleted()) { - throw new ExpiredException(sprintf(Message::RESET_PASSWORD_USED, $hash)); - } - - return $this->infoResponse(sprintf(Message::RESET_PASSWORD_VALID, $hash)); - } - - /** - * @throws BadRequestException - * @throws ConflictException - * @throws ExpiredException - * @throws MailException - * @throws NotFoundException - */ - public function patch(ServerRequestInterface $request): ResponseInterface - { - $hash = $request->getAttribute('hash'); - - $userResetPassword = $this->userService->findResetPasswordByHash($hash); - if (! $userResetPassword->isValid()) { - throw new ExpiredException(sprintf(Message::RESET_PASSWORD_EXPIRED, $hash)); - } - if ($userResetPassword->isCompleted()) { - throw new ConflictException(sprintf(Message::RESET_PASSWORD_USED, $hash)); - } - - $inputFilter = (new UpdatePasswordInputFilter())->setData((array) $request->getParsedBody()); - if (! $inputFilter->isValid()) { - throw (new BadRequestException())->setMessages($inputFilter->getMessages()); - } - - $this->userService->updateUser( - $userResetPassword->markAsCompleted()->getUser(), - $inputFilter->getValues() - ); - - $this->userService->sendResetPasswordCompletedMail($userResetPassword->getUser()); - - return $this->infoResponse(Message::RESET_PASSWORD_OK); - } - - /** - * @throws BadRequestException - * @throws ConflictException - * @throws MailException - * @throws NotFoundException - */ - public function post(ServerRequestInterface $request): ResponseInterface - { - $inputFilter = (new ResetPasswordInputFilter())->setData((array) $request->getParsedBody()); - if (! $inputFilter->isValid()) { - throw (new BadRequestException())->setMessages($inputFilter->getMessages()); - } - - if (! empty($inputFilter->getValue('email'))) { - $user = $this->userService->findByEmail($inputFilter->getValue('email')); - } elseif (! empty($inputFilter->getValue('identity'))) { - $user = $this->userService->findByIdentity($inputFilter->getValue('identity')); - } else { - $user = null; - } - - if (! $user instanceof User) { - throw new NotFoundException(Message::USER_NOT_FOUND); - } - - $this->userService->updateUser($user->createResetPassword()); - $this->userService->sendResetPasswordRequestedMail($user); - - return $this->infoResponse(Message::MAIL_SENT_RESET_PASSWORD, StatusCodeInterface::STATUS_CREATED); - } -} diff --git a/src/User/src/Handler/User/Avatar/DeleteUserAvatarResourceHandler.php b/src/User/src/Handler/User/Avatar/DeleteUserAvatarResourceHandler.php new file mode 100644 index 00000000..c85e03f3 --- /dev/null +++ b/src/User/src/Handler/User/Avatar/DeleteUserAvatarResourceHandler.php @@ -0,0 +1,42 @@ +userService->findOneBy(['uuid' => $request->getAttribute('uuid')]); + if (! $user->hasAvatar()) { + throw new NotFoundException(Message::AVATAR_MISSING); + } + + $this->userAvatarService->removeAvatar($user); + + return $this->noContentResponse(); + } +} diff --git a/src/User/src/Handler/User/Avatar/GetUserAvatarResourceHandler.php b/src/User/src/Handler/User/Avatar/GetUserAvatarResourceHandler.php new file mode 100644 index 00000000..f53fe8ce --- /dev/null +++ b/src/User/src/Handler/User/Avatar/GetUserAvatarResourceHandler.php @@ -0,0 +1,37 @@ +userService->findOneBy(['uuid' => $request->getAttribute('uuid')]); + if (! $user->hasAvatar()) { + throw new NotFoundException(Message::AVATAR_MISSING); + } + + return $this->createResponse($request, $user->getAvatar()); + } +} diff --git a/src/User/src/Handler/UserAvatarHandler.php b/src/User/src/Handler/User/Avatar/PostUserAvatarResourceHandler.php similarity index 56% rename from src/User/src/Handler/UserAvatarHandler.php rename to src/User/src/Handler/User/Avatar/PostUserAvatarResourceHandler.php index fdc07686..050f2bdb 100644 --- a/src/User/src/Handler/UserAvatarHandler.php +++ b/src/User/src/Handler/User/Avatar/PostUserAvatarResourceHandler.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Api\User\Handler; +namespace Api\User\Handler\User\Avatar; use Api\App\Exception\BadRequestException; use Api\App\Exception\NotFoundException; @@ -10,12 +10,11 @@ use Api\User\InputFilter\UpdateAvatarInputFilter; use Api\User\Service\UserAvatarServiceInterface; use Api\User\Service\UserServiceInterface; -use Core\App\Message; use Dot\DependencyInjection\Attribute\Inject; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; -class UserAvatarHandler extends AbstractHandler +class PostUserAvatarResourceHandler extends AbstractHandler { #[Inject( UserServiceInterface::class, @@ -27,39 +26,11 @@ public function __construct( ) { } - /** - * @throws NotFoundException - */ - public function delete(ServerRequestInterface $request): ResponseInterface - { - $user = $this->userService->findOneBy(['uuid' => $request->getAttribute('uuid')]); - if (! $user->hasAvatar()) { - throw new NotFoundException(Message::AVATAR_MISSING); - } - - $this->userAvatarService->removeAvatar($user); - - return $this->noContentResponse(); - } - - /** - * @throws NotFoundException - */ - public function get(ServerRequestInterface $request): ResponseInterface - { - $user = $this->userService->findOneBy(['uuid' => $request->getAttribute('uuid')]); - if (! $user->hasAvatar()) { - throw new NotFoundException(Message::AVATAR_MISSING); - } - - return $this->createResponse($request, $user->getAvatar()); - } - /** * @throws BadRequestException * @throws NotFoundException */ - public function post(ServerRequestInterface $request): ResponseInterface + public function handle(ServerRequestInterface $request): ResponseInterface { $inputFilter = (new UpdateAvatarInputFilter())->setData($request->getUploadedFiles()); if (! $inputFilter->isValid()) { diff --git a/src/User/src/Handler/User/DeleteUserResourceHandler.php b/src/User/src/Handler/User/DeleteUserResourceHandler.php new file mode 100644 index 00000000..324b34a2 --- /dev/null +++ b/src/User/src/Handler/User/DeleteUserResourceHandler.php @@ -0,0 +1,37 @@ +userService->findOneBy(['uuid' => $request->getAttribute('uuid')]); + + $this->userService->deleteUser($user); + + return $this->noContentResponse(); + } +} diff --git a/src/User/src/Handler/UserCollectionHandler.php b/src/User/src/Handler/User/GetUserCollectionHandler.php similarity index 78% rename from src/User/src/Handler/UserCollectionHandler.php rename to src/User/src/Handler/User/GetUserCollectionHandler.php index 50f08692..46fdc395 100644 --- a/src/User/src/Handler/UserCollectionHandler.php +++ b/src/User/src/Handler/User/GetUserCollectionHandler.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Api\User\Handler; +namespace Api\User\Handler\User; use Api\App\Exception\BadRequestException; use Api\App\Handler\AbstractHandler; @@ -11,7 +11,7 @@ use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; -class UserCollectionHandler extends AbstractHandler +class GetUserCollectionHandler extends AbstractHandler { #[Inject( UserServiceInterface::class, @@ -24,7 +24,7 @@ public function __construct( /** * @throws BadRequestException */ - public function get(ServerRequestInterface $request): ResponseInterface + public function handle(ServerRequestInterface $request): ResponseInterface { return $this->createResponse($request, $this->userService->getUsers($request->getQueryParams())); } diff --git a/src/User/src/Handler/User/GetUserResourceHandler.php b/src/User/src/Handler/User/GetUserResourceHandler.php new file mode 100644 index 00000000..8cab1dc8 --- /dev/null +++ b/src/User/src/Handler/User/GetUserResourceHandler.php @@ -0,0 +1,33 @@ +userService->findOneBy(['uuid' => $request->getAttribute('uuid')]); + + return $this->createResponse($request, $user); + } +} diff --git a/src/User/src/Handler/UserActivateHandler.php b/src/User/src/Handler/User/PatchUserActivateHandler.php similarity index 86% rename from src/User/src/Handler/UserActivateHandler.php rename to src/User/src/Handler/User/PatchUserActivateHandler.php index d684998b..e92b68cc 100644 --- a/src/User/src/Handler/UserActivateHandler.php +++ b/src/User/src/Handler/User/PatchUserActivateHandler.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Api\User\Handler; +namespace Api\User\Handler\User; use Api\App\Exception\ConflictException; use Api\App\Exception\NotFoundException; @@ -14,7 +14,7 @@ use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; -class UserActivateHandler extends AbstractHandler +class PatchUserActivateHandler extends AbstractHandler { #[Inject( UserServiceInterface::class, @@ -29,7 +29,7 @@ public function __construct( * @throws MailException * @throws NotFoundException */ - public function patch(ServerRequestInterface $request): ResponseInterface + public function handle(ServerRequestInterface $request): ResponseInterface { $user = $this->userService->findOneBy(['uuid' => $request->getAttribute('uuid')]); if ($user->isActive()) { diff --git a/src/User/src/Handler/User/PatchUserDeactivateHandler.php b/src/User/src/Handler/User/PatchUserDeactivateHandler.php new file mode 100644 index 00000000..9aa37756 --- /dev/null +++ b/src/User/src/Handler/User/PatchUserDeactivateHandler.php @@ -0,0 +1,41 @@ +userService->findOneBy(['uuid' => $request->getAttribute('uuid')]); + if ($user->isPending()) { + throw new ConflictException(Message::USER_ALREADY_DEACTIVATED); + } + + $this->userService->deactivateUser($user); + + return $this->infoResponse(Message::USER_DEACTIVATED); + } +} diff --git a/src/User/src/Handler/User/PatchUserResourceHandler.php b/src/User/src/Handler/User/PatchUserResourceHandler.php new file mode 100644 index 00000000..09e9b4fc --- /dev/null +++ b/src/User/src/Handler/User/PatchUserResourceHandler.php @@ -0,0 +1,44 @@ +setData((array) $request->getParsedBody()); + if (! $inputFilter->isValid()) { + throw (new BadRequestException())->setMessages($inputFilter->getMessages()); + } + + $user = $this->userService->findOneBy(['uuid' => $request->getAttribute('uuid')]); + $this->userService->updateUser($user, $inputFilter->getValues()); + + return $this->createResponse($request, $user); + } +} diff --git a/src/User/src/Handler/User/PostUserResourceHandler.php b/src/User/src/Handler/User/PostUserResourceHandler.php new file mode 100644 index 00000000..85b2401b --- /dev/null +++ b/src/User/src/Handler/User/PostUserResourceHandler.php @@ -0,0 +1,50 @@ +setData((array) $request->getParsedBody()); + if (! $inputFilter->isValid()) { + throw (new BadRequestException())->setMessages($inputFilter->getMessages()); + } + + $user = $this->userService->createUser($inputFilter->getValues()); + if ($user->isPending()) { + $this->userService->sendActivationMail($user); + } elseif ($user->isActive()) { + $this->userService->sendWelcomeMail($user); + } + + return $this->createdResponse($request, $user); + } +} diff --git a/src/User/src/Handler/UserRoleCollectionHandler.php b/src/User/src/Handler/User/Role/GetUserRoleCollectionHandler.php similarity index 78% rename from src/User/src/Handler/UserRoleCollectionHandler.php rename to src/User/src/Handler/User/Role/GetUserRoleCollectionHandler.php index 91f60bfb..605e133c 100644 --- a/src/User/src/Handler/UserRoleCollectionHandler.php +++ b/src/User/src/Handler/User/Role/GetUserRoleCollectionHandler.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Api\User\Handler; +namespace Api\User\Handler\User\Role; use Api\App\Exception\BadRequestException; use Api\App\Handler\AbstractHandler; @@ -11,7 +11,7 @@ use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; -class UserRoleCollectionHandler extends AbstractHandler +class GetUserRoleCollectionHandler extends AbstractHandler { #[Inject( UserRoleServiceInterface::class, @@ -24,7 +24,7 @@ public function __construct( /** * @throws BadRequestException */ - public function get(ServerRequestInterface $request): ResponseInterface + public function handle(ServerRequestInterface $request): ResponseInterface { return $this->createResponse($request, $this->roleService->getRoles($request->getQueryParams())); } diff --git a/src/User/src/Handler/UserRoleHandler.php b/src/User/src/Handler/User/Role/GetUserRoleResourceHandler.php similarity index 79% rename from src/User/src/Handler/UserRoleHandler.php rename to src/User/src/Handler/User/Role/GetUserRoleResourceHandler.php index 66417dc5..1756fd94 100644 --- a/src/User/src/Handler/UserRoleHandler.php +++ b/src/User/src/Handler/User/Role/GetUserRoleResourceHandler.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Api\User\Handler; +namespace Api\User\Handler\User\Role; use Api\App\Exception\NotFoundException; use Api\App\Handler\AbstractHandler; @@ -11,7 +11,7 @@ use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; -class UserRoleHandler extends AbstractHandler +class GetUserRoleResourceHandler extends AbstractHandler { #[Inject( UserRoleServiceInterface::class, @@ -24,7 +24,7 @@ public function __construct( /** * @throws NotFoundException */ - public function get(ServerRequestInterface $request): ResponseInterface + public function handle(ServerRequestInterface $request): ResponseInterface { $role = $this->roleService->findOneBy(['uuid' => $request->getAttribute('uuid')]); diff --git a/src/User/src/Handler/UserHandler.php b/src/User/src/Handler/UserHandler.php deleted file mode 100644 index 5a9124b9..00000000 --- a/src/User/src/Handler/UserHandler.php +++ /dev/null @@ -1,93 +0,0 @@ -userService->findOneBy(['uuid' => $request->getAttribute('uuid')]); - - $this->userService->deleteUser($user); - - return $this->noContentResponse(); - } - - /** - * @throws NotFoundException - */ - public function get(ServerRequestInterface $request): ResponseInterface - { - $user = $this->userService->findOneBy(['uuid' => $request->getAttribute('uuid')]); - - return $this->createResponse($request, $user); - } - - /** - * @throws BadRequestException - * @throws ConflictException - * @throws NotFoundException - */ - public function patch(ServerRequestInterface $request): ResponseInterface - { - $inputFilter = (new UpdateUserInputFilter())->setData((array) $request->getParsedBody()); - if (! $inputFilter->isValid()) { - throw (new BadRequestException())->setMessages($inputFilter->getMessages()); - } - - $user = $this->userService->findOneBy(['uuid' => $request->getAttribute('uuid')]); - $this->userService->updateUser($user, $inputFilter->getValues()); - - return $this->createResponse($request, $user); - } - - /** - * @throws BadRequestException - * @throws ConflictException - * @throws MailException - * @throws NotFoundException - */ - public function post(ServerRequestInterface $request): ResponseInterface - { - $inputFilter = (new CreateUserInputFilter())->setData((array) $request->getParsedBody()); - if (! $inputFilter->isValid()) { - throw (new BadRequestException())->setMessages($inputFilter->getMessages()); - } - - $user = $this->userService->createUser($inputFilter->getValues()); - if ($user->isPending()) { - $this->userService->sendActivationMail($user); - } elseif ($user->isActive()) { - $this->userService->sendWelcomeMail($user); - } - - return $this->createdResponse($request, $user); - } -} diff --git a/src/User/src/OpenAPI.php b/src/User/src/OpenAPI.php index b2fd118c..13c0f3db 100644 --- a/src/User/src/OpenAPI.php +++ b/src/User/src/OpenAPI.php @@ -4,15 +4,32 @@ namespace Api\User; -use Api\User\Handler\AccountActivateHandler; -use Api\User\Handler\AccountAvatarHandler; -use Api\User\Handler\AccountHandler; -use Api\User\Handler\AccountRecoveryHandler; -use Api\User\Handler\AccountResetPasswordHandler; -use Api\User\Handler\UserActivateHandler; -use Api\User\Handler\UserAvatarHandler; -use Api\User\Handler\UserHandler; -use Api\User\Handler\UserRoleHandler; +use Api\User\Collection\UserCollection; +use Api\User\Handler\Account\Avatar\DeleteUserAccountAvatarHandler; +use Api\User\Handler\Account\Avatar\GetUserAccountAvatarHandler; +use Api\User\Handler\Account\Avatar\PostUserAccountAvatarHandler; +use Api\User\Handler\Account\DeleteUserAccountResourceHandler; +use Api\User\Handler\Account\GetUserAccountResourceHandler; +use Api\User\Handler\Account\PatchUserAccountActivateHandler; +use Api\User\Handler\Account\PatchUserAccountResourceHandler; +use Api\User\Handler\Account\PostUserAccountActivateHandler; +use Api\User\Handler\Account\PostUserAccountRecoverHandler; +use Api\User\Handler\Account\PostUserAccountResourceHandler; +use Api\User\Handler\Account\ResetPassword\GetUserAccountResetPasswordHandler; +use Api\User\Handler\Account\ResetPassword\PatchUserAccountResetPasswordHandler; +use Api\User\Handler\Account\ResetPassword\PostUserAccountResetPasswordHandler; +use Api\User\Handler\User\Avatar\DeleteUserAvatarResourceHandler; +use Api\User\Handler\User\Avatar\GetUserAvatarResourceHandler; +use Api\User\Handler\User\Avatar\PostUserAvatarResourceHandler; +use Api\User\Handler\User\DeleteUserResourceHandler; +use Api\User\Handler\User\GetUserCollectionHandler; +use Api\User\Handler\User\GetUserResourceHandler; +use Api\User\Handler\User\PatchUserActivateHandler; +use Api\User\Handler\User\PatchUserDeactivateHandler; +use Api\User\Handler\User\PatchUserResourceHandler; +use Api\User\Handler\User\PostUserResourceHandler; +use Api\User\Handler\User\Role\GetUserRoleCollectionHandler; +use Api\User\Handler\User\Role\GetUserRoleResourceHandler; use Core\User\Entity\User; use Core\User\Entity\UserAvatar; use Core\User\Entity\UserDetail; @@ -25,68 +42,7 @@ use OpenApi\Attributes as OA; /** - * @see UserHandler::delete() - */ -#[OA\Delete( - path: '/user/{uuid}', - description: 'Authenticated (super)admin deletes (anonymizes) a user account identified by its UUID', - summary: 'Admin deletes (anonymizes) user account', - security: [['AuthToken' => []]], - tags: ['User'], - parameters: [ - new OA\Parameter( - name: 'uuid', - description: 'User UUID', - in: 'path', - required: true, - schema: new OA\Schema(type: 'string'), - ), - ], - responses: [ - new OA\Response( - response: StatusCodeInterface::STATUS_NO_CONTENT, - description: 'User account has been deleted (anonymized)', - ), - new OA\Response( - response: StatusCodeInterface::STATUS_NOT_FOUND, - description: 'Not Found', - ), - ], -)] - -/** - * @see UserHandler::get() - */ -#[OA\Get( - path: '/user/{uuid}', - description: 'Authenticated (super)admin fetches a user account identified by its UUID', - summary: 'Admin views user account', - security: [['AuthToken' => []]], - tags: ['User'], - parameters: [ - new OA\Parameter( - name: 'uuid', - description: 'User UUID', - in: 'path', - required: true, - schema: new OA\Schema(type: 'string'), - ), - ], - responses: [ - new OA\Response( - response: StatusCodeInterface::STATUS_OK, - description: 'User account', - content: new OA\JsonContent(ref: '#/components/schemas/User'), - ), - new OA\Response( - response: StatusCodeInterface::STATUS_NOT_FOUND, - description: 'Not Found', - ), - ], -)] - -/** - * @see UserHandler::getCollection() + * @see GetUserCollectionHandler::handle() */ #[OA\Get( path: '/user', @@ -155,17 +111,18 @@ )] /** - * @see UserHandler::patch() + * @see PostUserResourceHandler::handle() */ -#[OA\Patch( - path: '/user/{uuid}', - description: 'Authenticated (super)admin updates an existing user account', - summary: 'Admin updates user account', +#[OA\Post( + path: '/user', + description: 'Authenticated (super)admin creates a new user account', + summary: 'Admin creates user account', security: [['AuthToken' => []]], requestBody: new OA\RequestBody( - description: 'Update user account request', + description: 'Create user account request', required: true, content: new OA\JsonContent( + required: ['identity', 'password', 'passwordConfirm', 'roles'], properties: [ new OA\Property(property: 'identity', type: 'string'), new OA\Property(property: 'password', type: 'string'), @@ -173,6 +130,7 @@ new OA\Property(property: 'status', type: 'string', default: UserStatusEnum::Active), new OA\Property( property: 'detail', + required: ['email'], properties: [ new OA\Property(property: 'firstName', type: 'string'), new OA\Property(property: 'lastName', type: 'string'), @@ -195,6 +153,44 @@ ), ), tags: ['User'], + responses: [ + new OA\Response( + response: StatusCodeInterface::STATUS_CREATED, + description: 'User account created', + content: new OA\JsonContent(ref: '#/components/schemas/User'), + ), + new OA\Response( + response: StatusCodeInterface::STATUS_BAD_REQUEST, + description: 'Bad Request', + content: new OA\JsonContent(ref: '#/components/schemas/ErrorMessage'), + ), + new OA\Response( + response: StatusCodeInterface::STATUS_CONFLICT, + description: 'Conflict', + content: new OA\JsonContent(ref: '#/components/schemas/ErrorMessage'), + ), + new OA\Response( + response: StatusCodeInterface::STATUS_NOT_FOUND, + description: 'Not Found', + content: new OA\JsonContent(ref: '#/components/schemas/ErrorMessage'), + ), + new OA\Response( + response: StatusCodeInterface::STATUS_INTERNAL_SERVER_ERROR, + description: 'Mail error', + content: new OA\JsonContent(ref: '#/components/schemas/ErrorMessage'), + ), + ], +)] + +/** + * @see DeleteUserResourceHandler::handle() + */ +#[OA\Delete( + path: '/user/{uuid}', + description: 'Authenticated (super)admin deletes (anonymizes) a user account identified by its UUID', + summary: 'Admin deletes (anonymizes) user account', + security: [['AuthToken' => []]], + tags: ['User'], parameters: [ new OA\Parameter( name: 'uuid', @@ -206,41 +202,59 @@ ], responses: [ new OA\Response( - response: StatusCodeInterface::STATUS_OK, - description: 'User account updated', - content: new OA\JsonContent(ref: '#/components/schemas/User'), + response: StatusCodeInterface::STATUS_NO_CONTENT, + description: 'User account has been deleted (anonymized)', ), new OA\Response( - response: StatusCodeInterface::STATUS_BAD_REQUEST, - description: 'Bad Request', - content: new OA\JsonContent(ref: '#/components/schemas/ErrorMessage'), + response: StatusCodeInterface::STATUS_NOT_FOUND, + description: 'Not Found', + ), + ], +)] + +/** + * @see GetUserResourceHandler::handle() + */ +#[OA\Get( + path: '/user/{uuid}', + description: 'Authenticated (super)admin fetches a user account identified by its UUID', + summary: 'Admin views user account', + security: [['AuthToken' => []]], + tags: ['User'], + parameters: [ + new OA\Parameter( + name: 'uuid', + description: 'User UUID', + in: 'path', + required: true, + schema: new OA\Schema(type: 'string'), ), + ], + responses: [ new OA\Response( - response: StatusCodeInterface::STATUS_CONFLICT, - description: 'Conflict', - content: new OA\JsonContent(ref: '#/components/schemas/ErrorMessage'), + response: StatusCodeInterface::STATUS_OK, + description: 'User account', + content: new OA\JsonContent(ref: '#/components/schemas/User'), ), new OA\Response( response: StatusCodeInterface::STATUS_NOT_FOUND, description: 'Not Found', - content: new OA\JsonContent(ref: '#/components/schemas/ErrorMessage'), ), ], )] /** - * @see UserHandler::post() + * @see PatchUserResourceHandler::handle() */ -#[OA\Post( - path: '/user', - description: 'Authenticated (super)admin creates a new user account', - summary: 'Admin creates user account', +#[OA\Patch( + path: '/user/{uuid}', + description: 'Authenticated (super)admin updates an existing user account', + summary: 'Admin updates user account', security: [['AuthToken' => []]], requestBody: new OA\RequestBody( - description: 'Create user account request', + description: 'Update user account request', required: true, content: new OA\JsonContent( - required: ['identity', 'password', 'passwordConfirm', 'roles'], properties: [ new OA\Property(property: 'identity', type: 'string'), new OA\Property(property: 'password', type: 'string'), @@ -248,7 +262,6 @@ new OA\Property(property: 'status', type: 'string', default: UserStatusEnum::Active), new OA\Property( property: 'detail', - required: ['email'], properties: [ new OA\Property(property: 'firstName', type: 'string'), new OA\Property(property: 'lastName', type: 'string'), @@ -271,44 +284,6 @@ ), ), tags: ['User'], - responses: [ - new OA\Response( - response: StatusCodeInterface::STATUS_CREATED, - description: 'User account created', - content: new OA\JsonContent(ref: '#/components/schemas/User'), - ), - new OA\Response( - response: StatusCodeInterface::STATUS_BAD_REQUEST, - description: 'Bad Request', - content: new OA\JsonContent(ref: '#/components/schemas/ErrorMessage'), - ), - new OA\Response( - response: StatusCodeInterface::STATUS_CONFLICT, - description: 'Conflict', - content: new OA\JsonContent(ref: '#/components/schemas/ErrorMessage'), - ), - new OA\Response( - response: StatusCodeInterface::STATUS_NOT_FOUND, - description: 'Not Found', - content: new OA\JsonContent(ref: '#/components/schemas/ErrorMessage'), - ), - new OA\Response( - response: StatusCodeInterface::STATUS_INTERNAL_SERVER_ERROR, - description: 'Mail error', - content: new OA\JsonContent(ref: '#/components/schemas/ErrorMessage'), - ), - ], -)] - -/** - * @see UserActivateHandler::patch() - */ -#[OA\Patch( - path: '/user/{uuid}/activate', - description: 'Authenticated (super)admin activates an existing user account', - summary: 'Admin activates user account', - security: [['AuthToken' => []]], - tags: ['User'], parameters: [ new OA\Parameter( name: 'uuid', @@ -321,8 +296,13 @@ responses: [ new OA\Response( response: StatusCodeInterface::STATUS_OK, - description: 'User account activated', - content: new OA\JsonContent(ref: '#/components/schemas/InfoMessage'), + description: 'User account updated', + content: new OA\JsonContent(ref: '#/components/schemas/User'), + ), + new OA\Response( + response: StatusCodeInterface::STATUS_BAD_REQUEST, + description: 'Bad Request', + content: new OA\JsonContent(ref: '#/components/schemas/ErrorMessage'), ), new OA\Response( response: StatusCodeInterface::STATUS_CONFLICT, @@ -334,16 +314,11 @@ description: 'Not Found', content: new OA\JsonContent(ref: '#/components/schemas/ErrorMessage'), ), - new OA\Response( - response: StatusCodeInterface::STATUS_INTERNAL_SERVER_ERROR, - description: 'Mail error', - content: new OA\JsonContent(ref: '#/components/schemas/ErrorMessage'), - ), ], )] /** - * @see UserAvatarHandler::delete() + * @see DeleteUserAvatarResourceHandler::handle() */ #[OA\Delete( path: '/user/{uuid}/avatar', @@ -373,7 +348,7 @@ )] /** - * @see UserAvatarHandler::get() + * @see GetUserAvatarResourceHandler::handle() */ #[OA\Get( path: '/user/{uuid}/avatar', @@ -404,7 +379,7 @@ )] /** - * @see UserAvatarHandler::post() + * @see PostUserAvatarResourceHandler::handle() */ #[OA\Post( path: '/user/{uuid}/avatar', @@ -455,7 +430,75 @@ )] /** - * @see UserRoleHandler::get() + * @see GetUserRoleCollectionHandler::handle() + */ +#[OA\Get( + path: '/user/role', + description: 'Authenticated (super)admin fetches a list of user roles', + summary: 'Admin lists user roles', + security: [['AuthToken' => []]], + tags: ['UserRole'], + parameters: [ + new OA\Parameter( + name: 'page', + description: 'Page number', + in: 'query', + required: false, + schema: new OA\Schema(type: 'integer'), + example: 1, + ), + new OA\Parameter( + name: 'limit', + description: 'Limit', + in: 'query', + required: false, + schema: new OA\Schema(type: 'integer'), + example: 10, + ), + new OA\Parameter( + name: 'order', + description: 'Sort by field', + in: 'query', + required: false, + schema: new OA\Schema(type: 'string'), + examples: [ + new OA\Examples(example: 'role.name', summary: 'Name', value: 'role.name'), + new OA\Examples(example: 'role.created', summary: 'Created', value: 'role.created'), + new OA\Examples(example: 'role.updated', summary: 'Updated', value: 'role.updated'), + ], + ), + new OA\Parameter( + name: 'dir', + description: 'Sort direction', + in: 'query', + required: false, + schema: new OA\Schema(type: 'string'), + examples: [ + new OA\Examples(example: 'desc', summary: 'Sort descending', value: 'desc'), + new OA\Examples(example: 'asc', summary: 'Sort ascending', value: 'asc'), + ], + ), + ], + responses: [ + new OA\Response( + response: StatusCodeInterface::STATUS_OK, + description: 'List of user roles', + content: new OA\JsonContent(ref: '#/components/schemas/UserRoleCollection'), + ), + new OA\Response( + response: StatusCodeInterface::STATUS_BAD_REQUEST, + description: 'Bad Request', + content: new OA\JsonContent(ref: '#/components/schemas/ErrorMessage'), + ), + new OA\Response( + response: StatusCodeInterface::STATUS_NOT_FOUND, + description: 'Not Found', + ), + ], +)] + +/** + * @see GetUserRoleResourceHandler::handle() */ #[OA\Get( path: '/user/role/{uuid}', @@ -474,90 +517,101 @@ ], responses: [ new OA\Response( - response: StatusCodeInterface::STATUS_OK, - description: 'User role', - content: new OA\JsonContent(ref: '#/components/schemas/UserRole'), + response: StatusCodeInterface::STATUS_OK, + description: 'User role', + content: new OA\JsonContent(ref: '#/components/schemas/UserRole'), + ), + new OA\Response( + response: StatusCodeInterface::STATUS_NOT_FOUND, + description: 'Not Found', + ), + ], +)] + +/** + * @see PatchUserActivateHandler::handle() + */ +#[OA\Patch( + path: '/user/{uuid}/activate', + description: 'Authenticated (super)admin activates an existing user account', + summary: 'Admin activates user account', + security: [['AuthToken' => []]], + tags: ['User'], + parameters: [ + new OA\Parameter( + name: 'uuid', + description: 'User UUID', + in: 'path', + required: true, + schema: new OA\Schema(type: 'string'), + ), + ], + responses: [ + new OA\Response( + response: StatusCodeInterface::STATUS_OK, + description: 'User account activated', + content: new OA\JsonContent(ref: '#/components/schemas/InfoMessage'), + ), + new OA\Response( + response: StatusCodeInterface::STATUS_CONFLICT, + description: 'Conflict', + content: new OA\JsonContent(ref: '#/components/schemas/ErrorMessage'), ), new OA\Response( response: StatusCodeInterface::STATUS_NOT_FOUND, description: 'Not Found', + content: new OA\JsonContent(ref: '#/components/schemas/ErrorMessage'), + ), + new OA\Response( + response: StatusCodeInterface::STATUS_INTERNAL_SERVER_ERROR, + description: 'Mail error', + content: new OA\JsonContent(ref: '#/components/schemas/ErrorMessage'), ), ], )] /** - * @see UserRoleHandler::getCollection() + * @see PatchUserDeactivateHandler::handle() */ -#[OA\Get( - path: '/user/role', - description: 'Authenticated (super)admin fetches a list of user roles', - summary: 'Admin lists user roles', +#[OA\Patch( + path: '/user/{uuid}/deactivate', + description: 'Authenticated (super)admin deactivates an existing user account', + summary: 'Admin deactivates user account', security: [['AuthToken' => []]], - tags: ['UserRole'], + tags: ['User'], parameters: [ new OA\Parameter( - name: 'page', - description: 'Page number', - in: 'query', - required: false, - schema: new OA\Schema(type: 'integer'), - example: 1, - ), - new OA\Parameter( - name: 'limit', - description: 'Limit', - in: 'query', - required: false, - schema: new OA\Schema(type: 'integer'), - example: 10, - ), - new OA\Parameter( - name: 'order', - description: 'Sort by field', - in: 'query', - required: false, - schema: new OA\Schema(type: 'string'), - examples: [ - new OA\Examples(example: 'role.name', summary: 'Name', value: 'role.name'), - new OA\Examples(example: 'role.created', summary: 'Created', value: 'role.created'), - new OA\Examples(example: 'role.updated', summary: 'Updated', value: 'role.updated'), - ], - ), - new OA\Parameter( - name: 'dir', - description: 'Sort direction', - in: 'query', - required: false, + name: 'uuid', + description: 'User UUID', + in: 'path', + required: true, schema: new OA\Schema(type: 'string'), - examples: [ - new OA\Examples(example: 'desc', summary: 'Sort descending', value: 'desc'), - new OA\Examples(example: 'asc', summary: 'Sort ascending', value: 'asc'), - ], ), ], responses: [ new OA\Response( response: StatusCodeInterface::STATUS_OK, - description: 'List of user roles', - content: new OA\JsonContent(ref: '#/components/schemas/UserRoleCollection'), + description: 'User account deactivated', + content: new OA\JsonContent(ref: '#/components/schemas/InfoMessage'), ), new OA\Response( - response: StatusCodeInterface::STATUS_BAD_REQUEST, - description: 'Bad Request', + response: StatusCodeInterface::STATUS_CONFLICT, + description: 'Conflict', content: new OA\JsonContent(ref: '#/components/schemas/ErrorMessage'), ), new OA\Response( response: StatusCodeInterface::STATUS_NOT_FOUND, description: 'Not Found', + content: new OA\JsonContent(ref: '#/components/schemas/ErrorMessage'), ), ], )] /** - * @see AccountHandler::delete() + * @see DeleteUserAccountResourceHandler::handle() */ #[OA\Delete( - path: '/user/my-account', + path: '/user/account', description: 'Authenticated user deletes (anonymizes) their own account', summary: 'User deletes (anonymizes) their own account', security: [['AuthToken' => []]], @@ -575,10 +629,10 @@ )] /** - * @see AccountHandler::get() + * @see GetUserAccountResourceHandler::handle() */ #[OA\Get( - path: '/user/my-account', + path: '/user/account', description: 'Authenticated user fetches their own account data', summary: 'User fetches their own account', security: [['AuthToken' => []]], @@ -593,10 +647,10 @@ )] /** - * @see AccountHandler::patch() + * @see PatchUserAccountResourceHandler::handle() */ #[OA\Patch( - path: '/user/my-account', + path: '/user/account', description: 'Authenticated user updates their own account data', summary: 'User updates their own account', security: [['AuthToken' => []]], @@ -646,10 +700,10 @@ )] /** - * @see AccountHandler::post() + * @see PostUserAccountResourceHandler::handle() */ #[OA\Post( - path: '/account/register', + path: '/user/account', description: 'Register user account', summary: 'Unauthenticated user registers new user account', requestBody: new OA\RequestBody( @@ -706,10 +760,10 @@ )] /** - * @see AccountAvatarHandler::delete() + * @see DeleteUserAccountAvatarHandler::handle() */ #[OA\Delete( - path: '/user/my-avatar', + path: '/user/account/avatar', description: 'Authenticated user deletes their user avatar', summary: 'User deletes their own avatar', security: [['AuthToken' => []]], @@ -727,10 +781,10 @@ )] /** - * @see AccountAvatarHandler::get() + * @see GetUserAccountAvatarHandler::handle() */ #[OA\Get( - path: '/user/my-avatar', + path: '/user/account/avatar', description: 'Authenticated user fetches their own avatar', summary: 'User fetches their own avatar', security: [['AuthToken' => []]], @@ -749,10 +803,10 @@ )] /** - * @see AccountAvatarHandler::post() + * @see PostUserAccountAvatarHandler::handle() */ #[OA\Post( - path: '/user/my-avatar', + path: '/user/account/avatar', description: 'Authenticated user creates their own avatar', summary: 'User creates their own avatar', security: [['AuthToken' => []]], @@ -786,17 +840,17 @@ )] /** - * @see AccountResetPasswordHandler::get() + * @see PatchUserAccountActivateHandler::handle() */ -#[OA\Get( - path: '/account/reset-password/{hash}', - description: 'Unauthenticated user fetches a reset password by its hash', - summary: 'Unauthenticated user fetches reset password', - tags: ['ResetPassword'], +#[OA\Patch( + path: '/user/account/activate/{hash}', + description: 'Unauthenticated user activates their account using the hash from an activation link', + summary: 'Unauthenticated user activates their account', + tags: ['ActivateUser'], parameters: [ new OA\Parameter( name: 'hash', - description: 'Reset password hash', + description: 'User activation hash', in: 'path', required: true, schema: new OA\Schema(type: 'string'), @@ -805,52 +859,43 @@ responses: [ new OA\Response( response: StatusCodeInterface::STATUS_OK, - description: 'Reset password status', + description: 'Account activated', content: new OA\JsonContent(ref: '#/components/schemas/InfoMessage'), ), new OA\Response( - response: StatusCodeInterface::STATUS_NOT_FOUND, - description: 'Not Found', + response: StatusCodeInterface::STATUS_CONFLICT, + description: 'Conflict', + content: new OA\JsonContent(ref: '#/components/schemas/ErrorMessage'), ), new OA\Response( - response: StatusCodeInterface::STATUS_GONE, - description: 'Gone (expired)', + response: StatusCodeInterface::STATUS_NOT_FOUND, + description: 'Not Found', ), ], )] /** - * @see AccountResetPasswordHandler::patch() + * @see PostUserAccountActivateHandler::handle() */ -#[OA\Patch( - path: '/account/reset-password/{hash}', - description: 'Unauthenticated user modifies their password using a reset password identified by its hash', - summary: 'Unauthenticated user modifies their password', +#[OA\Post( + path: '/user/account/activate', + description: 'Unauthenticated user requests an account activation link by providing their email', + summary: 'Unauthenticated user requests to activate account', requestBody: new OA\RequestBody( - description: 'Modify password request', + description: 'Account activation request', required: true, content: new OA\JsonContent( properties: [ - new OA\Property(property: 'password', type: 'string'), - new OA\Property(property: 'passwordConfirm', type: 'string'), + new OA\Property(property: 'email', type: 'string'), ], type: 'object', ), ), - tags: ['ResetPassword'], - parameters: [ - new OA\Parameter( - name: 'hash', - description: 'Reset password hash', - in: 'path', - required: true, - schema: new OA\Schema(type: 'string'), - ), - ], + tags: ['ActivateUser'], responses: [ new OA\Response( - response: StatusCodeInterface::STATUS_OK, - description: 'Reset password status', + response: StatusCodeInterface::STATUS_CREATED, + description: 'Account activation requested', content: new OA\JsonContent(ref: '#/components/schemas/InfoMessage'), ), new OA\Response( @@ -867,11 +912,6 @@ response: StatusCodeInterface::STATUS_NOT_FOUND, description: 'Not Found', ), - new OA\Response( - response: StatusCodeInterface::STATUS_GONE, - description: 'Gone (expired)', - content: new OA\JsonContent(ref: '#/components/schemas/ErrorMessage'), - ), new OA\Response( response: StatusCodeInterface::STATUS_INTERNAL_SERVER_ERROR, description: 'Mail error', @@ -881,36 +921,27 @@ )] /** - * @see AccountResetPasswordHandler::post() + * @see PostUserAccountRecoverHandler::handle() */ #[OA\Post( - path: '/account/reset-password', - description: 'Unauthenticated user requests to reset their password by providing their email/identity', - summary: 'Unauthenticated user requests to modify their password', + path: '/account/recover-identity', + description: 'Unauthenticated user recovers their identity by providing their email', + summary: 'Unauthenticated user recovers their identity', requestBody: new OA\RequestBody( - description: 'Reset password request', + description: 'Recover identity request', required: true, content: new OA\JsonContent( - type: 'object', - oneOf: [ - new OA\Schema( - properties: [ - new OA\Property(property: 'email', type: 'string'), - ], - ), - new OA\Schema( - properties: [ - new OA\Property(property: 'identity', type: 'string'), - ], - ), + properties: [ + new OA\Property(property: 'email', type: 'string'), ], + type: 'object', ), ), - tags: ['ResetPassword'], + tags: ['RecoverIdentity'], responses: [ new OA\Response( response: StatusCodeInterface::STATUS_CREATED, - description: 'Reset password created', + description: 'Identity sent via email', content: new OA\JsonContent(ref: '#/components/schemas/InfoMessage'), ), new OA\Response( @@ -918,11 +949,6 @@ description: 'Bad Request', content: new OA\JsonContent(ref: '#/components/schemas/ErrorMessage'), ), - new OA\Response( - response: StatusCodeInterface::STATUS_CONFLICT, - description: 'Conflict', - content: new OA\JsonContent(ref: '#/components/schemas/ErrorMessage'), - ), new OA\Response( response: StatusCodeInterface::STATUS_NOT_FOUND, description: 'Not Found', @@ -936,58 +962,62 @@ )] /** - * @see AccountRecoveryHandler::post() + * @see GetUserAccountResetPasswordHandler::handle() */ -#[OA\Post( - path: '/account/recover-identity', - description: 'Unauthenticated user recovers their identity by providing their email', - summary: 'Unauthenticated user recovers their identity', - requestBody: new OA\RequestBody( - description: 'Recover identity request', - required: true, - content: new OA\JsonContent( - properties: [ - new OA\Property(property: 'email', type: 'string'), - ], - type: 'object', +#[OA\Get( + path: '/user/account/reset-password/{hash}', + description: 'Unauthenticated user fetches a reset password by its hash', + summary: 'Unauthenticated user fetches reset password', + tags: ['ResetPassword'], + parameters: [ + new OA\Parameter( + name: 'hash', + description: 'Reset password hash', + in: 'path', + required: true, + schema: new OA\Schema(type: 'string'), ), - ), - tags: ['RecoverIdentity'], + ], responses: [ new OA\Response( - response: StatusCodeInterface::STATUS_CREATED, - description: 'Identity sent via email', + response: StatusCodeInterface::STATUS_OK, + description: 'Reset password status', content: new OA\JsonContent(ref: '#/components/schemas/InfoMessage'), ), - new OA\Response( - response: StatusCodeInterface::STATUS_BAD_REQUEST, - description: 'Bad Request', - content: new OA\JsonContent(ref: '#/components/schemas/ErrorMessage'), - ), new OA\Response( response: StatusCodeInterface::STATUS_NOT_FOUND, description: 'Not Found', ), new OA\Response( - response: StatusCodeInterface::STATUS_INTERNAL_SERVER_ERROR, - description: 'Mail error', - content: new OA\JsonContent(ref: '#/components/schemas/ErrorMessage'), + response: StatusCodeInterface::STATUS_GONE, + description: 'Gone (expired)', ), ], )] /** - * @see AccountActivateHandler::patch() + * @see PatchUserAccountResetPasswordHandler::handle() */ #[OA\Patch( - path: '/account/activate/{hash}', - description: 'Unauthenticated user activates their account using the hash from an activation link', - summary: 'Unauthenticated user activates their account', - tags: ['ActivateUser'], + path: '/user/account/reset-password/{hash}', + description: 'Unauthenticated user modifies their password using a reset password identified by its hash', + summary: 'Unauthenticated user modifies their password', + requestBody: new OA\RequestBody( + description: 'Modify password request', + required: true, + content: new OA\JsonContent( + properties: [ + new OA\Property(property: 'password', type: 'string'), + new OA\Property(property: 'passwordConfirm', type: 'string'), + ], + type: 'object', + ), + ), + tags: ['ResetPassword'], parameters: [ new OA\Parameter( name: 'hash', - description: 'User activation hash', + description: 'Reset password hash', in: 'path', required: true, schema: new OA\Schema(type: 'string'), @@ -996,9 +1026,14 @@ responses: [ new OA\Response( response: StatusCodeInterface::STATUS_OK, - description: 'Account activated', + description: 'Reset password status', content: new OA\JsonContent(ref: '#/components/schemas/InfoMessage'), ), + new OA\Response( + response: StatusCodeInterface::STATUS_BAD_REQUEST, + description: 'Bad Request', + content: new OA\JsonContent(ref: '#/components/schemas/ErrorMessage'), + ), new OA\Response( response: StatusCodeInterface::STATUS_CONFLICT, description: 'Conflict', @@ -1008,31 +1043,50 @@ response: StatusCodeInterface::STATUS_NOT_FOUND, description: 'Not Found', ), + new OA\Response( + response: StatusCodeInterface::STATUS_GONE, + description: 'Gone (expired)', + content: new OA\JsonContent(ref: '#/components/schemas/ErrorMessage'), + ), + new OA\Response( + response: StatusCodeInterface::STATUS_INTERNAL_SERVER_ERROR, + description: 'Mail error', + content: new OA\JsonContent(ref: '#/components/schemas/ErrorMessage'), + ), ], )] /** - * @see AccountActivateHandler::post() + * @see PostUserAccountResetPasswordHandler::handle() */ #[OA\Post( - path: '/account/activate', - description: 'Unauthenticated user requests an account activation link by providing their email', - summary: 'Unauthenticated user requests to activate account', + path: '/user/account/reset-password', + description: 'Unauthenticated user requests to reset their password by providing their email/identity', + summary: 'Unauthenticated user requests to modify their password', requestBody: new OA\RequestBody( - description: 'Account activation request', + description: 'Reset password request', required: true, content: new OA\JsonContent( - properties: [ - new OA\Property(property: 'email', type: 'string'), - ], type: 'object', + oneOf: [ + new OA\Schema( + properties: [ + new OA\Property(property: 'email', type: 'string'), + ], + ), + new OA\Schema( + properties: [ + new OA\Property(property: 'identity', type: 'string'), + ], + ), + ], ), ), - tags: ['ActivateUser'], + tags: ['ResetPassword'], responses: [ new OA\Response( response: StatusCodeInterface::STATUS_CREATED, - description: 'Account activation requested', + description: 'Reset password created', content: new OA\JsonContent(ref: '#/components/schemas/InfoMessage'), ), new OA\Response( @@ -1220,6 +1274,9 @@ type: 'object', )] +/** + * @see UserCollection + */ #[OA\Schema( schema: 'UserCollection', properties: [ @@ -1243,6 +1300,9 @@ ], )] +/** + * @see UserRoleCollection + */ #[OA\Schema( schema: 'UserRoleCollection', properties: [ diff --git a/src/User/src/RoutesDelegator.php b/src/User/src/RoutesDelegator.php index 6dd2e506..fa700c6c 100644 --- a/src/User/src/RoutesDelegator.php +++ b/src/User/src/RoutesDelegator.php @@ -4,17 +4,31 @@ namespace Api\User; -use Api\User\Handler\AccountActivateHandler; -use Api\User\Handler\AccountAvatarHandler; -use Api\User\Handler\AccountHandler; -use Api\User\Handler\AccountRecoveryHandler; -use Api\User\Handler\AccountResetPasswordHandler; -use Api\User\Handler\UserActivateHandler; -use Api\User\Handler\UserAvatarHandler; -use Api\User\Handler\UserCollectionHandler; -use Api\User\Handler\UserHandler; -use Api\User\Handler\UserRoleCollectionHandler; -use Api\User\Handler\UserRoleHandler; +use Api\User\Handler\Account\Avatar\DeleteUserAccountAvatarHandler; +use Api\User\Handler\Account\Avatar\GetUserAccountAvatarHandler; +use Api\User\Handler\Account\Avatar\PostUserAccountAvatarHandler; +use Api\User\Handler\Account\DeleteUserAccountResourceHandler; +use Api\User\Handler\Account\GetUserAccountResourceHandler; +use Api\User\Handler\Account\PatchUserAccountActivateHandler; +use Api\User\Handler\Account\PatchUserAccountResourceHandler; +use Api\User\Handler\Account\PostUserAccountActivateHandler; +use Api\User\Handler\Account\PostUserAccountRecoverHandler; +use Api\User\Handler\Account\PostUserAccountResourceHandler; +use Api\User\Handler\Account\ResetPassword\GetUserAccountResetPasswordHandler; +use Api\User\Handler\Account\ResetPassword\PatchUserAccountResetPasswordHandler; +use Api\User\Handler\Account\ResetPassword\PostUserAccountResetPasswordHandler; +use Api\User\Handler\User\Avatar\DeleteUserAvatarResourceHandler; +use Api\User\Handler\User\Avatar\GetUserAvatarResourceHandler; +use Api\User\Handler\User\Avatar\PostUserAvatarResourceHandler; +use Api\User\Handler\User\DeleteUserResourceHandler; +use Api\User\Handler\User\GetUserCollectionHandler; +use Api\User\Handler\User\GetUserResourceHandler; +use Api\User\Handler\User\PatchUserActivateHandler; +use Api\User\Handler\User\PatchUserDeactivateHandler; +use Api\User\Handler\User\PatchUserResourceHandler; +use Api\User\Handler\User\PostUserResourceHandler; +use Api\User\Handler\User\Role\GetUserRoleCollectionHandler; +use Api\User\Handler\User\Role\GetUserRoleResourceHandler; use Mezzio\Application; use Psr\Container\ContainerInterface; @@ -29,146 +43,57 @@ public function __invoke(ContainerInterface $container, string $serviceName, cal $uuid = \Api\App\RoutesDelegator::REGEXP_UUID; - /** - * Admins manage user accounts - */ + // Accounts having higher than user permissions manage user accounts - $app->post( - '/user', - UserHandler::class, - 'user.create' - ); - $app->delete( - '/user/' . $uuid, - UserHandler::class, - 'user.delete' - ); - $app->get( - '/user', - UserCollectionHandler::class, - 'user.list' - ); - $app->patch( - '/user/' . $uuid, - UserHandler::class, - 'user.update' - ); - $app->get( - '/user/' . $uuid, - UserHandler::class, - 'user.view' - ); + $app->get('/user', GetUserCollectionHandler::class, 'user::list-user'); + $app->post('/user', PostUserResourceHandler::class, 'user::create-user'); - $app->patch( - '/user/' . $uuid . '/activate', - UserActivateHandler::class, - 'user.activate' - ); + $app->delete('/user/' . $uuid, DeleteUserResourceHandler::class, 'user::delete-user'); + $app->get('/user/' . $uuid, GetUserResourceHandler::class, 'user::view-user'); + $app->patch('/user/' . $uuid, PatchUserResourceHandler::class, 'user::update-user'); - $app->delete( - '/user/' . $uuid . '/avatar', - UserAvatarHandler::class, - 'user.avatar.delete' - ); - $app->get( - '/user/' . $uuid . '/avatar', - UserAvatarHandler::class, - 'user.avatar.view' - ); - $app->post( - '/user/' . $uuid . '/avatar', - UserAvatarHandler::class, - 'user.avatar.create' - ); + $app->delete('/user/' . $uuid . '/avatar', DeleteUserAvatarResourceHandler::class, 'user::delete-user-avatar'); + $app->get('/user/' . $uuid . '/avatar', GetUserAvatarResourceHandler::class, 'user::view-user-avatar'); + $app->post('/user/' . $uuid . '/avatar', PostUserAvatarResourceHandler::class, 'user::create-user-avatar'); - $app->get( - '/user/role', - UserRoleCollectionHandler::class, - 'user.role.list' - ); - $app->get( - '/user/role/' . $uuid, - UserRoleHandler::class, - 'user.role.view' - ); + $app->get('/user/role', GetUserRoleCollectionHandler::class, 'user::list-role'); + $app->get('/user/role/' . $uuid, GetUserRoleResourceHandler::class, 'user::view-role'); - /** - * Users manage their own accounts - */ + $app->patch('/user/' . $uuid . '/activate', PatchUserActivateHandler::class, 'user::activate-user'); + $app->patch('/user/' . $uuid . '/deactivate', PatchUserDeactivateHandler::class, 'user::deactivate-user'); - $app->delete( - '/user/my-account', - AccountHandler::class, - 'user.my-account.delete' - ); - $app->get( - '/user/my-account', - AccountHandler::class, - 'user.my-account.view' - ); - $app->patch( - '/user/my-account', - AccountHandler::class, - 'user.my-account.update' - ); + // Users manage their user accounts - $app->post( - '/user/my-avatar', - AccountAvatarHandler::class, - 'user.my-avatar.create' - ); - $app->delete( - '/user/my-avatar', - AccountAvatarHandler::class, - 'user.my-avatar.delete' - ); - $app->get( - '/user/my-avatar', - AccountAvatarHandler::class, - 'user.my-avatar.view' - ); + $app->delete('/user/account', DeleteUserAccountResourceHandler::class, 'user::delete-account'); + $app->get('/user/account', GetUserAccountResourceHandler::class, 'user::view-account'); + $app->patch('/user/account', PatchUserAccountResourceHandler::class, 'user::update-account'); + $app->post('/user/account', PostUserAccountResourceHandler::class, 'user::create-account'); - /** - * Unauthenticated users manage their accounts - */ + $app->delete('/user/account/avatar', DeleteUserAccountAvatarHandler::class, 'user::delete-account-avatar'); + $app->get('/user/account/avatar', GetUserAccountAvatarHandler::class, 'user::view-account-avatar'); + $app->post('/user/account/avatar', PostUserAccountAvatarHandler::class, 'user::create-account-avatar'); - $app->post( - '/account/register', - AccountHandler::class, - 'account.register' - ); + // Unauthenticated users manage their user accounts - $app->get( - '/account/reset-password/{hash}', - AccountResetPasswordHandler::class, - 'account.reset-password.validate' - ); - $app->patch( - '/account/reset-password/{hash}', - AccountResetPasswordHandler::class, - 'account.modify-password' - ); - $app->post( - '/account/reset-password', - AccountResetPasswordHandler::class, - 'account.reset-password.request' - ); + $app->patch('/user/account/activate/{hash}', PatchUserAccountActivateHandler::class, 'user::activate-account'); + $app->post('/user/account/activate', PostUserAccountActivateHandler::class, 'user::request-activate-account'); - $app->post( - '/account/recover-identity', - AccountRecoveryHandler::class, - 'account.recover-identity' - ); + $app->post('/user/account/recover', PostUserAccountRecoverHandler::class, 'user::recover-account'); + $app->get( + '/user/account/reset-password/{hash}', + GetUserAccountResetPasswordHandler::class, + 'user::check-account-reset-password' + ); $app->patch( - '/account/activate/{hash}', - AccountActivateHandler::class, - 'account.activate' + '/user/account/reset-password/{hash}', + PatchUserAccountResetPasswordHandler::class, + 'user::update-account-reset-password' ); $app->post( - '/account/activate', - AccountActivateHandler::class, - 'account.activate.request' + '/user/account/reset-password', + PostUserAccountResetPasswordHandler::class, + 'user::create-account-reset-password' ); return $app; diff --git a/src/User/src/Service/UserService.php b/src/User/src/Service/UserService.php index 1dd1c68c..7c3e0b5b 100644 --- a/src/User/src/Service/UserService.php +++ b/src/User/src/Service/UserService.php @@ -24,6 +24,7 @@ use Dot\Mail\Service\MailService; use Mezzio\Template\TemplateRendererInterface; use RuntimeException; +use Symfony\Component\Mailer\Exception\TransportExceptionInterface; use function date; use function in_array; @@ -62,6 +63,11 @@ public function activateUser(User $user): User return $this->userRepository->saveUser($user->activate()); } + public function deactivateUser(User $user): User + { + return $this->userRepository->saveUser($user->deactivate()); + } + /** * @throws ConflictException * @throws NotFoundException @@ -269,7 +275,7 @@ public function sendActivationMail(User $user): bool try { return $this->mailService->send()->isValid(); - } catch (MailException $exception) { + } catch (MailException | TransportExceptionInterface $exception) { $this->logger->err($exception->getMessage()); throw new MailException(sprintf(Message::MAIL_NOT_SENT_TO, $user->getDetail()->getEmail())); } @@ -293,7 +299,7 @@ public function sendResetPasswordRequestedMail(User $user): bool try { return $this->mailService->send()->isValid(); - } catch (MailException $exception) { + } catch (MailException | TransportExceptionInterface $exception) { $this->logger->err($exception->getMessage()); throw new MailException(sprintf(Message::MAIL_NOT_SENT_TO, $user->getDetail()->getEmail())); } @@ -317,7 +323,7 @@ public function sendResetPasswordCompletedMail(User $user): bool try { return $this->mailService->send()->isValid(); - } catch (MailException $exception) { + } catch (MailException | TransportExceptionInterface $exception) { $this->logger->err($exception->getMessage()); throw new MailException(sprintf(Message::MAIL_NOT_SENT_TO, $user->getDetail()->getEmail())); } @@ -339,7 +345,7 @@ public function sendWelcomeMail(User $user): bool try { return $this->mailService->send()->isValid(); - } catch (MailException $exception) { + } catch (MailException | TransportExceptionInterface $exception) { $this->logger->err($exception->getMessage()); throw new MailException(sprintf(Message::MAIL_NOT_SENT_TO, $user->getDetail()->getEmail())); } @@ -425,7 +431,7 @@ public function sendRecoverIdentityMail(User $user): bool try { return $this->mailService->send()->isValid(); - } catch (MailException $exception) { + } catch (MailException | TransportExceptionInterface $exception) { $this->logger->err($exception->getMessage()); throw new MailException(sprintf(Message::MAIL_NOT_SENT_TO, $user->getDetail()->getEmail())); } diff --git a/src/User/src/Service/UserServiceInterface.php b/src/User/src/Service/UserServiceInterface.php index 8597af08..596e6333 100644 --- a/src/User/src/Service/UserServiceInterface.php +++ b/src/User/src/Service/UserServiceInterface.php @@ -17,6 +17,8 @@ interface UserServiceInterface { public function activateUser(User $user): User; + public function deactivateUser(User $user): User; + /** * @throws ConflictException * @throws NotFoundException diff --git a/src/User/templates/user/activate.html.twig b/src/User/templates/user/activate.html.twig index 1ddf7cc4..fd46ff8f 100644 --- a/src/User/templates/user/activate.html.twig +++ b/src/User/templates/user/activate.html.twig @@ -1,4 +1,4 @@ Hi {{ user.name }},

You can activate your {{ config.application.name }} account by copying the link below to the "Activate my account" PATCH endpoint in Postman:
-{{ url('account.activate', {'hash': user.hash}) }} +{{ url('user::activate-account', {'hash': user.hash}) }} diff --git a/src/User/templates/user/reset-password-requested.html.twig b/src/User/templates/user/reset-password-requested.html.twig index 550d7272..551f991d 100644 --- a/src/User/templates/user/reset-password-requested.html.twig +++ b/src/User/templates/user/reset-password-requested.html.twig @@ -1,4 +1,4 @@ -{% set resetUrl = url('account.modify-password', {'hash': user.resetPasswords|last.hash}) %} +{% set resetUrl = url('user::update-account-reset-password', {'hash': user.resetPasswords|last.hash}) %}

Hi {{ user.detail.firstname ?? user.identity }},

diff --git a/src/User/templates/user/welcome.html.twig b/src/User/templates/user/welcome.html.twig index c716aaca..c3d2de54 100644 --- a/src/User/templates/user/welcome.html.twig +++ b/src/User/templates/user/welcome.html.twig @@ -1,3 +1,3 @@ Hi {{ user.name }},

-You can access your {{ config.application.name }} account here. +You can access your {{ config.application.name }} account here. diff --git a/test/Functional/AdminTest.php b/test/Functional/AdminTest.php index 11811250..3400fcb1 100644 --- a/test/Functional/AdminTest.php +++ b/test/Functional/AdminTest.php @@ -31,7 +31,7 @@ public function testUserCannotListAdminAccounts(): void $user = $this->createUser(); $this->loginAs($user->getIdentity(), self::DEFAULT_PASSWORD); - $response = $this->get('/admin/my-account'); + $response = $this->get('/admin/account'); $this->assertResponseForbidden($response); } @@ -105,7 +105,7 @@ public function testAdminCanListAdminAccounts(): void $admin = $this->createAdmin(); $this->loginAs($admin->getIdentity(), self::DEFAULT_PASSWORD, 'admin', 'admin'); - $response = $this->get('/admin/my-account'); + $response = $this->get('/admin/account'); $this->assertResponseOk($response); } @@ -255,7 +255,7 @@ public function testAdminCanViewPersonalAccount(): void $admin = $this->createAdmin(); $this->loginAs($admin->getIdentity(), self::DEFAULT_PASSWORD, 'admin', 'admin'); - $response = $this->get('/admin/my-account'); + $response = $this->get('/admin/account'); $data = json_decode($response->getBody()->getContents(), true); $this->assertResponseOk($response); @@ -279,7 +279,7 @@ public function testAdminCanUpdatePersonalAccount(): void 'lastName' => 'admin', ]; - $response = $this->patch('/admin/my-account', $updateData); + $response = $this->patch('/admin/account', $updateData); $data = json_decode($response->getBody()->getContents(), true); $this->assertResponseOk($response); diff --git a/test/Functional/AuthenticationTest.php b/test/Functional/AuthenticationTest.php index f8afc1bb..d100ca15 100644 --- a/test/Functional/AuthenticationTest.php +++ b/test/Functional/AuthenticationTest.php @@ -37,7 +37,7 @@ public function testAuthenticateAdmin(): void { $this->createAdmin(); - $response = $this->post('/security/generate-token', $this->getValidAdminAccessTokenCredentials()); + $response = $this->post('/security/token', $this->getValidAdminAccessTokenCredentials()); $data = json_decode($response->getBody()->getContents(), true); @@ -59,7 +59,7 @@ public function testAuthenticateUser(): void { $this->createUser(); - $response = $this->post('/security/generate-token', $this->getValidFrontendAccessTokenCredentials()); + $response = $this->post('/security/token', $this->getValidFrontendAccessTokenCredentials()); $data = json_decode($response->getBody()->getContents(), true); @@ -76,7 +76,7 @@ public function testAuthenticateUser(): void public function testInvalidRefreshToken(): void { - $response = $this->post('/security/refresh-token', $this->getInvalidFrontendRefreshTokenCredentials()); + $response = $this->post('/security/token', $this->getInvalidFrontendRefreshTokenCredentials()); $data = json_decode($response->getBody()->getContents(), true); @@ -99,7 +99,7 @@ public function testRefreshToken(): void $this->loginAs($user->getIdentity(), self::DEFAULT_PASSWORD); - $response = $this->post('/security/refresh-token', $this->getValidFrontendRefreshTokenCredentials()); + $response = $this->post('/security/token', $this->getValidFrontendRefreshTokenCredentials()); $this->assertResponseOk($response); $data = json_decode($response->getBody()->getContents(), true); @@ -122,7 +122,7 @@ public function testAdminCannotAuthenticateAsUser(): void $admin = $this->createAdmin(); $errorMessages = $this->getContainer()->get('config')['authentication']['invalid_credentials']; - $response = $this->post('/security/generate-token', $this->getValidFrontendAccessTokenCredentials([ + $response = $this->post('/security/token', $this->getValidFrontendAccessTokenCredentials([ 'username' => $admin->getIdentity(), ])); @@ -146,7 +146,7 @@ public function testUserCannotAuthenticateAsAdmin(): void $errorMessages = $this->getContainer()->get('config')['authentication']['invalid_credentials']; $user = $this->createUser(); - $response = $this->post('/security/generate-token', $this->getValidAdminAccessTokenCredentials([ + $response = $this->post('/security/token', $this->getValidAdminAccessTokenCredentials([ 'username' => $user->getIdentity(), ])); @@ -169,7 +169,7 @@ private function authenticateInvalidIdentity(array $credentials): void { $errorMessages = $this->getContainer()->get('config')['authentication']['invalid_credentials']; - $response = $this->post('/security/generate-token', $credentials); + $response = $this->post('/security/token', $credentials); $this->assertResponseBadRequest($response); $data = json_decode($response->getBody()->getContents(), true); diff --git a/test/Functional/UserTest.php b/test/Functional/UserTest.php index 08bc25ed..2f8ebd2f 100644 --- a/test/Functional/UserTest.php +++ b/test/Functional/UserTest.php @@ -47,7 +47,7 @@ public function testRegisterAccountDuplicateIdentity(): void $userAvatarService = $this->createMock(UserAvatarService::class); $this->replaceService(UserAvatarService::class, $userAvatarService); - $response = $this->post('/user', $this->getValidUserData(['status' => UserStatusEnum::Pending->value])); + $response = $this->post('/user/account', $this->getValidUserData(['status' => UserStatusEnum::Pending->value])); $this->assertResponseConflict($response); $data = json_decode($response->getBody()->getContents(), true); @@ -73,7 +73,7 @@ public function testRegisterAccountDuplicateEmail(): void $userAvatarService = $this->createMock(UserAvatarService::class); $this->replaceService(UserAvatarService::class, $userAvatarService); - $response = $this->post('/user', $this->getValidUserData(['status' => UserStatusEnum::Pending->value])); + $response = $this->post('/user/account', $this->getValidUserData(['status' => UserStatusEnum::Pending->value])); $this->assertResponseConflict($response); $data = json_decode($response->getBody()->getContents(), true); @@ -101,7 +101,7 @@ public function testRegisterAccount(): void 'status' => UserStatusEnum::Pending->value, ]); - $response = $this->post('/user', $user); + $response = $this->post('/user/account', $user); $this->assertResponseCreated($response); $data = json_decode($response->getBody()->getContents(), true); @@ -142,7 +142,7 @@ public function testCreateMyAvatar(): void $uploadedFile = $this->createUploadedFile(); - $response = $this->post('/user/my-avatar', [], [], ['avatar' => $uploadedFile]); + $response = $this->post('/user/account/avatar', [], [], ['avatar' => $uploadedFile]); $this->assertResponseCreated($response); $path = __DIR__ . DIRECTORY_SEPARATOR . $uploadedFile->getClientFilename(); @@ -158,7 +158,7 @@ public function testViewMyAvatarNotFound(): void $user = $this->createUser(); $this->loginAs($user->getIdentity(), self::DEFAULT_PASSWORD); - $response = $this->get('/user/my-avatar'); + $response = $this->get('/user/account/avatar'); $this->assertResponseNotFound($response); } @@ -179,7 +179,7 @@ public function testViewMyAvatar(): void $this->loginAs($user->getIdentity(), self::DEFAULT_PASSWORD); - $response = $this->get('/user/my-avatar'); + $response = $this->get('/user/account/avatar'); $this->assertResponseOk($response); } @@ -192,7 +192,7 @@ public function testDeleteMyAvatarNotFound(): void $user = $this->createUser(); $this->loginAs($user->getIdentity(), self::DEFAULT_PASSWORD); - $response = $this->delete('/user/my-avatar'); + $response = $this->delete('/user/account/avatar'); $this->assertResponseNotFound($response); } @@ -213,7 +213,7 @@ public function testDeleteMyAvatar(): void $this->loginAs($user->getIdentity(), self::DEFAULT_PASSWORD); - $response = $this->delete('/user/my-avatar'); + $response = $this->delete('/user/account/avatar'); $this->assertResponseNoContent($response); } @@ -231,7 +231,7 @@ public function testActivateMyAccountAlreadyActivated(): void { $user = $this->createUser(); - $response = $this->patch('/account/activate/' . $user->getHash()); + $response = $this->patch('/user/account/activate/' . $user->getHash()); $this->assertResponseConflict($response); } @@ -246,7 +246,7 @@ public function testActivateMyAccount(): void ]); $this->assertFalse($user->isActive()); - $response = $this->patch('/account/activate/' . $user->getHash()); + $response = $this->patch('/user/account/activate/' . $user->getHash()); $this->assertResponseOk($response); $userRepository = $this->getEntityManager()->getRepository(User::class); @@ -268,7 +268,7 @@ public function testActivateAccountByEmail(): void $mailService = $this->createMock(MailService::class); $this->replaceService(MailService::class, $mailService); - $response = $this->post('/account/activate', [ + $response = $this->post('/user/account/activate', [ 'email' => $user->getDetail()->getEmail(), ]); $this->assertResponseCreated($response); @@ -291,7 +291,7 @@ public function testDeleteMyAccount(): void $user = $this->createUser(); $this->loginAs($user->getIdentity(), self::DEFAULT_PASSWORD); - $response = $this->delete('/user/my-account'); + $response = $this->delete('/user/account'); $this->assertResponseNoContent($response); $userRepository = $this->getEntityManager()->getRepository(User::class); @@ -301,7 +301,7 @@ public function testDeleteMyAccount(): void public function testRequestResetPasswordInvalidHash(): void { - $response = $this->patch('/account/reset-password/invalid_hash'); + $response = $this->patch('/user/account/reset-password/invalid_hash'); $this->assertResponseNotFound($response); } @@ -328,7 +328,7 @@ public function testRequestResetPasswordExpired(): void $mailService = $this->createMock(MailService::class); $this->replaceService(MailService::class, $mailService); - $response = $this->patch('/account/reset-password/' . $resetPassword->getHash(), [ + $response = $this->patch('/user/account/reset-password/' . $resetPassword->getHash(), [ 'password' => '654321', 'passwordConfirm' => '654321', ]); @@ -367,7 +367,7 @@ public function testRequestResetPasswordAlreadyUsed(): void $mailService = $this->createMock(MailService::class); $this->replaceService(MailService::class, $mailService); - $response = $this->patch('/account/reset-password/' . $resetPassword->getHash(), [ + $response = $this->patch('/user/account/reset-password/' . $resetPassword->getHash(), [ 'password' => '654321', 'passwordConfirm' => '654321', ]); @@ -406,7 +406,7 @@ public function testResetPassword(): void $mailService = $this->createMock(MailService::class); $this->replaceService(MailService::class, $mailService); - $response = $this->patch('/account/reset-password/' . $resetPassword->getHash(), [ + $response = $this->patch('/user/account/reset-password/' . $resetPassword->getHash(), [ 'password' => '654321', 'passwordConfirm' => '654321', ]); @@ -431,7 +431,7 @@ public function testResetPasswordByEmail(): void $user = $this->createUser(); - $response = $this->post('/account/reset-password', [ + $response = $this->post('/user/account/reset-password', [ 'email' => $user->getDetail()->getEmail(), ]); $this->assertResponseCreated($response); @@ -448,7 +448,7 @@ public function testViewMyAccount(): void $this->loginAs($user->getIdentity(), self::DEFAULT_PASSWORD); - $response = $this->get('/user/my-account'); + $response = $this->get('/user/account'); $this->assertResponseOk($response); } @@ -469,7 +469,7 @@ public function testUpdateMyAccount(): void ], ]; - $response = $this->patch('/user/my-account', $updateData); + $response = $this->patch('/user/account', $updateData); $this->assertResponseOk($response); $data = json_decode($response->getBody()->getContents(), true); @@ -489,7 +489,7 @@ public function testRecoverAccountByIdentity(): void $mailService = $this->createMock(MailService::class); $this->replaceService(MailService::class, $mailService); - $response = $this->post('/account/recover-identity', [ + $response = $this->post('/user/account/recover', [ 'email' => $user->getDetail()->getEmail(), ]); $this->assertResponseOk($response); diff --git a/test/Unit/App/Middleware/AuthorizationMiddlewareTest.php b/test/Unit/App/Middleware/AuthorizationMiddlewareTest.php index 6c3903bf..9d866a38 100644 --- a/test/Unit/App/Middleware/AuthorizationMiddlewareTest.php +++ b/test/Unit/App/Middleware/AuthorizationMiddlewareTest.php @@ -31,7 +31,7 @@ class AuthorizationMiddlewareTest extends TestCase { - private Subject|MockObject $subject; + private Subject $subject; private UserRepository|MockObject $userRepository; private AdminRepository|MockObject $adminRepository; private AuthorizationInterface|MockObject $authorization; diff --git a/test/Unit/App/Middleware/DeprecationMiddlewareTest.php b/test/Unit/App/Middleware/DeprecationMiddlewareTest.php index f095e34c..37a80e64 100644 --- a/test/Unit/App/Middleware/DeprecationMiddlewareTest.php +++ b/test/Unit/App/Middleware/DeprecationMiddlewareTest.php @@ -7,7 +7,6 @@ use Api\App\Attribute\MethodDeprecation; use Api\App\Attribute\ResourceDeprecation; use Api\App\Exception\DeprecationConflictException; -use Api\App\Handler\AbstractHandler; use Api\App\Middleware\DeprecationMiddleware as Subject; use Core\App\Message; use Fig\Http\Message\RequestMethodInterface; @@ -32,7 +31,7 @@ class DeprecationMiddlewareTest extends TestCase { - private Subject|MockObject $subject; + private Subject $subject; private ServerRequestInterface|MockObject $request; private RequestHandlerInterface|MockObject $handler; private ResponseInterface $response; @@ -204,63 +203,6 @@ public function process( ]), $response->getHeader('link')[0]); } - /** - * @throws ReflectionException - * @throws Exception - */ - public function testDeprecationMethodUsesRequestMethod(): void - { - $handler = new class extends AbstractHandler { - #[MethodDeprecation( - sunset: '2038-01-01', - link: 'get-test-link', - deprecationReason: 'get-test-deprecation-reason', - rel: 'get-rel', - type: 'get-type', - )] - public function get(): ResponseInterface - { - return new EmptyResponse(); - } - - #[MethodDeprecation( - sunset: '2038-01-01', - link: 'post-test-link', - deprecationReason: 'post-test-deprecation-reason', - rel: 'post-rel', - type: 'post-type', - )] - public function post(): ResponseInterface - { - return new EmptyResponse(); - } - }; - - $routeResult = $this->createMock(RouteResult::class); - $route = $this->createMock(Route::class); - $lazyLoadingMiddleware = new LazyLoadingMiddleware( - $this->createMock(MiddlewareContainer::class), - $handler::class, - ); - - $route->method('getMiddleware')->willReturn($lazyLoadingMiddleware); - $routeResult->method('isFailure')->willReturn(false); - $routeResult->method('getMatchedRoute')->willReturn($route); - $this->request->method('getAttribute')->with(RouteResult::class)->willReturn($routeResult); - $this->request->method('getMethod')->willReturn(RequestMethodInterface::METHOD_POST); - $this->handler->method('handle')->with($this->request)->willReturn($this->response); - - $response = $this->subject->process($this->request, $this->handler); - - $this->assertTrue($response->hasHeader('sunset')); - $this->assertTrue($response->hasHeader('link')); - $this->assertSame('2038-01-01', $response->getHeader('sunset')[0]); - $this->assertSame($this->formatLink('post-test-link', [ - 'rel' => 'post-rel', - 'type' => 'post-type', - ]), $response->getHeader('link')[0]); - } - /** * @throws ReflectionException * @throws Exception diff --git a/test/Unit/User/Service/UserServiceTest.php b/test/Unit/User/Service/UserServiceTest.php index 59319c08..dadb138f 100644 --- a/test/Unit/User/Service/UserServiceTest.php +++ b/test/Unit/User/Service/UserServiceTest.php @@ -29,7 +29,7 @@ class UserServiceTest extends TestCase { - private Subject|MockObject $subject; + private Subject $subject; private UserRoleService|MockObject $userRoleService; private UserRepository|MockObject $userRepository; private UserDetailRepository|MockObject $userDetailRepository;