From a727faa5c1af52e9da527c50f0fcefc34486fe5a Mon Sep 17 00:00:00 2001 From: Hossein Azizabadi Farahani Date: Mon, 28 Feb 2022 10:58:03 +0330 Subject: [PATCH] Import module to github --- .gitignore | 4 + README.md | 2 +- composer.json | 63 +++++++ config/custom.config.php | 9 + config/module.config.php | 123 ++++++++++++++ data/schema.sql | 18 ++ .../Handler/Api/LoginHandlerFactory.php | 34 ++++ .../Handler/Api/LogoutHandlerFactory.php | 34 ++++ .../Handler/Api/ProfileHandlerFactory.php | 34 ++++ .../Handler/Api/RefreshHandlerFactory.php | 34 ++++ .../Handler/Api/RegisterHandlerFactory.php | 34 ++++ src/Factory/Handler/ErrorHandlerFactory.php | 30 ++++ .../AuthenticationMiddlewareFactory.php | 34 ++++ .../Middleware/SecurityMiddlewareFactory.php | 30 ++++ .../ValidationMiddlewareFactory.php | 32 ++++ .../Repository/AccountRepositoryFactory.php | 32 ++++ src/Factory/Service/AccountServiceFactory.php | 30 ++++ src/Factory/Service/CacheServiceFactory.php | 24 +++ src/Factory/Service/TokenServiceFactory.php | 24 +++ .../Validator/EmailValidatorFactory.php | 28 ++++ .../Validator/IdentityValidatorFactory.php | 28 ++++ .../Validator/NameValidatorFactory.php | 28 ++++ src/Handler/Api/LoginHandler.php | 56 +++++++ src/Handler/Api/LogoutHandler.php | 52 ++++++ src/Handler/Api/ProfileHandler.php | 53 ++++++ src/Handler/Api/RefreshHandler.php | 66 ++++++++ src/Handler/Api/RegisterHandler.php | 66 ++++++++ src/Handler/ErrorHandler.php | 44 +++++ src/Middleware/AuthenticationMiddleware.php | 111 +++++++++++++ src/Middleware/SecurityMiddleware.php | 39 +++++ src/Middleware/ValidationMiddleware.php | 146 +++++++++++++++++ src/Model/Account.php | 66 ++++++++ src/Module.php | 22 +++ src/Repository/AccountRepository.php | 155 ++++++++++++++++++ src/Repository/AccountRepositoryInterface.php | 19 +++ src/Service/AccountService.php | 128 +++++++++++++++ src/Service/CacheService.php | 66 ++++++++ src/Service/ServiceInterface.php | 7 + src/Service/TokenService.php | 96 +++++++++++ src/Validator/EmailValidator.php | 86 ++++++++++ src/Validator/IdentityValidator.php | 117 +++++++++++++ src/Validator/NameValidator.php | 135 +++++++++++++++ src/Validator/PasswordValidator.php | 72 ++++++++ test/readme.md | 0 44 files changed, 2310 insertions(+), 1 deletion(-) create mode 100644 .gitignore create mode 100644 composer.json create mode 100644 config/custom.config.php create mode 100644 config/module.config.php create mode 100644 data/schema.sql create mode 100644 src/Factory/Handler/Api/LoginHandlerFactory.php create mode 100644 src/Factory/Handler/Api/LogoutHandlerFactory.php create mode 100644 src/Factory/Handler/Api/ProfileHandlerFactory.php create mode 100644 src/Factory/Handler/Api/RefreshHandlerFactory.php create mode 100644 src/Factory/Handler/Api/RegisterHandlerFactory.php create mode 100644 src/Factory/Handler/ErrorHandlerFactory.php create mode 100644 src/Factory/Middleware/AuthenticationMiddlewareFactory.php create mode 100644 src/Factory/Middleware/SecurityMiddlewareFactory.php create mode 100644 src/Factory/Middleware/ValidationMiddlewareFactory.php create mode 100644 src/Factory/Repository/AccountRepositoryFactory.php create mode 100644 src/Factory/Service/AccountServiceFactory.php create mode 100644 src/Factory/Service/CacheServiceFactory.php create mode 100644 src/Factory/Service/TokenServiceFactory.php create mode 100644 src/Factory/Validator/EmailValidatorFactory.php create mode 100644 src/Factory/Validator/IdentityValidatorFactory.php create mode 100644 src/Factory/Validator/NameValidatorFactory.php create mode 100644 src/Handler/Api/LoginHandler.php create mode 100644 src/Handler/Api/LogoutHandler.php create mode 100644 src/Handler/Api/ProfileHandler.php create mode 100644 src/Handler/Api/RefreshHandler.php create mode 100644 src/Handler/Api/RegisterHandler.php create mode 100644 src/Handler/ErrorHandler.php create mode 100644 src/Middleware/AuthenticationMiddleware.php create mode 100644 src/Middleware/SecurityMiddleware.php create mode 100644 src/Middleware/ValidationMiddleware.php create mode 100644 src/Model/Account.php create mode 100644 src/Module.php create mode 100644 src/Repository/AccountRepository.php create mode 100644 src/Repository/AccountRepositoryInterface.php create mode 100644 src/Service/AccountService.php create mode 100644 src/Service/CacheService.php create mode 100644 src/Service/ServiceInterface.php create mode 100644 src/Service/TokenService.php create mode 100644 src/Validator/EmailValidator.php create mode 100644 src/Validator/IdentityValidator.php create mode 100644 src/Validator/NameValidator.php create mode 100644 src/Validator/PasswordValidator.php create mode 100644 test/readme.md diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4a584c4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/.idea/ +/vendor/ +composer.lock +_build diff --git a/README.md b/README.md index cd19c5c..bd31d9a 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,2 @@ # user -Api base authentication and user managment via laminas and pi +Api base authentication and user management via laminas and pi engine diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..3c6876d --- /dev/null +++ b/composer.json @@ -0,0 +1,63 @@ +{ + "name": "pi/user", + "description": "Api base authentication and user management via laminas and pi engine", + "license": "BSD-3-Clause", + "keywords": [ + "Pi", + "Pi Engine", + "Laminas", + "Laminas MVC", + "User management", + "User management", + "Psr", + "MultiTenant", + "SaaS" + ], + "homepage": "https://piengine.org", + "authors": [ + { + "name": "Hossein Azizabadi Farahani", + "email": "hossein@azizabadi.com" + } + ], + "require": { + "php": "^7.4 || ~8.0.0", + "ext-ctype": "*", + "ext-fileinfo": "*", + "ext-gd": "*", + "ext-json": "*", + "ext-mbstring": "*", + "ext-pdo": "*", + "ext-spl": "*", + "lib-curl": "*", + + "laminas/laminas-mvc": "^3.1.1", + "laminas/laminas-mvc-i18n": "^1.2.0", + "laminas/laminas-mvc-plugins": "^1.1.0", + "laminas/laminas-mvc-middleware": "^2.2", + "laminas/laminas-db": "^2.12.0", + "laminas/laminas-json": "^3.2", + "laminas/laminas-log": "^2.13.1", + "laminas/laminas-authentication": "^2.9", + "laminas/laminas-crypt": "^3.6", + "laminas/laminas-http": "^2.15", + "laminas/laminas-eventmanager": "^3.4", + "laminas/laminas-cache": "^3.1", + "laminas/laminas-cache-storage-adapter-redis": "^2.1", + "laminas/laminas-serializer": "^2.12", + "laminas/laminas-inputfilter": "^2.13", + "firebase/php-jwt": "^5.5" + }, + + "autoload": { + "psr-4": { + "Pi\\User\\": "src/" + } + }, + + "suggest": { + "ext-apc": "for opcode cache and system persistent data", + "ext-discount": "for Markdown text parsing", + "ext-intl": "for i18n features" + } +} diff --git a/config/custom.config.php b/config/custom.config.php new file mode 100644 index 0000000..d34e044 --- /dev/null +++ b/config/custom.config.php @@ -0,0 +1,9 @@ + [ + 'secret' => 'xt2468xc9mh5hvnal80rng36bbk16co4', + 'exp_access' => 900, // 15 min + 'exp_refresh' => 7776000, // 90 days + ], +]; \ No newline at end of file diff --git a/config/module.config.php b/config/module.config.php new file mode 100644 index 0000000..0c77726 --- /dev/null +++ b/config/module.config.php @@ -0,0 +1,123 @@ + [ + 'aliases' => [ + Repository\AccountRepositoryInterface::class => Repository\AccountRepository::class, + Service\ServiceInterface::class => Service\AccountService::class, + ], + 'factories' => [ + Repository\AccountRepository::class => Factory\Repository\AccountRepositoryFactory::class, + Service\AccountService::class => Factory\Service\AccountServiceFactory::class, + Service\TokenService::class => Factory\Service\TokenServiceFactory::class, + Service\CacheService::class => Factory\Service\CacheServiceFactory::class, + Middleware\AuthenticationMiddleware::class => Factory\Middleware\AuthenticationMiddlewareFactory::class, + Middleware\SecurityMiddleware::class => Factory\Middleware\SecurityMiddlewareFactory::class, + Middleware\ValidationMiddleware::class => Factory\Middleware\ValidationMiddlewareFactory::class, + Validator\EmailValidator::class => Factory\Validator\EmailValidatorFactory::class, + Validator\IdentityValidator::class => Factory\Validator\IdentityValidatorFactory::class, + Validator\NameValidator::class => Factory\Validator\NameValidatorFactory::class, + Handler\Api\ProfileHandler::class => Factory\Handler\Api\ProfileHandlerFactory::class, + Handler\Api\LoginHandler::class => Factory\Handler\Api\LoginHandlerFactory::class, + Handler\Api\LogoutHandler::class => Factory\Handler\Api\LogoutHandlerFactory::class, + Handler\Api\RegisterHandler::class => Factory\Handler\Api\RegisterHandlerFactory::class, + Handler\Api\RefreshHandler::class => Factory\Handler\Api\RefreshHandlerFactory::class, + Handler\ErrorHandler::class => Factory\Handler\ErrorHandlerFactory::class, + ], + ], + + 'router' => [ + 'routes' => [ + 'user' => [ + 'type' => Literal::class, + 'options' => [ + 'route' => '/user', + 'defaults' => [], + ], + 'child_routes' => [ + 'login' => [ + 'type' => Literal::class, + 'options' => [ + 'route' => '/login', + 'defaults' => [ + 'controller' => PipeSpec::class, + 'middleware' => new PipeSpec( + Middleware\ValidationMiddleware::class, + Middleware\SecurityMiddleware::class, + Handler\Api\LoginHandler::class + ), + ], + ], + ], + 'logout' => [ + 'type' => Literal::class, + 'options' => [ + 'route' => '/logout', + 'defaults' => [ + 'controller' => PipeSpec::class, + 'middleware' => new PipeSpec( + Middleware\AuthenticationMiddleware::class, + Middleware\SecurityMiddleware::class, + Handler\Api\LogoutHandler::class + ), + ], + ], + ], + 'register' => [ + 'type' => Literal::class, + 'options' => [ + 'route' => '/register', + 'defaults' => [ + 'controller' => PipeSpec::class, + 'middleware' => new PipeSpec( + Middleware\ValidationMiddleware::class, + Middleware\SecurityMiddleware::class, + Handler\Api\RegisterHandler::class + ), + ], + ], + ], + 'profile' => [ + 'type' => Literal::class, + 'options' => [ + 'route' => '/profile', + 'defaults' => [ + 'controller' => PipeSpec::class, + 'middleware' => new PipeSpec( + Middleware\AuthenticationMiddleware::class, + Middleware\SecurityMiddleware::class, + Handler\Api\ProfileHandler::class + ), + ], + ], + ], + 'refresh' => [ + 'type' => Literal::class, + 'options' => [ + 'route' => '/refresh', + 'defaults' => [ + 'controller' => PipeSpec::class, + 'middleware' => new PipeSpec( + Middleware\AuthenticationMiddleware::class, + Middleware\SecurityMiddleware::class, + Handler\Api\RefreshHandler::class + ), + ], + ], + ], + ], + ], + ], + ], + + 'view_manager' => [ + 'strategies' => [ + 'ViewJsonStrategy', + ], + ], +]; \ No newline at end of file diff --git a/data/schema.sql b/data/schema.sql new file mode 100644 index 0000000..30aab11 --- /dev/null +++ b/data/schema.sql @@ -0,0 +1,18 @@ +CREATE TABLE `account` +( + `id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT, + `name` VARCHAR(255) DEFAULT NULL, + `identity` VARCHAR(128) DEFAULT NULL, + `email` VARCHAR(128) DEFAULT NULL, + `credential` VARCHAR(255) NOT NULL DEFAULT '', + `status` TINYINT(1) UNSIGNED NOT NULL DEFAULT '0', + `time_created` INT(10) UNSIGNED NOT NULL DEFAULT '0', + `time_activated` INT(10) UNSIGNED NOT NULL DEFAULT '0', + `time_disabled` INT(10) UNSIGNED NOT NULL DEFAULT '0', + `time_deleted` INT(10) UNSIGNED NOT NULL DEFAULT '0', + PRIMARY KEY (`id`), + UNIQUE KEY `identity` (`identity`), + UNIQUE KEY `email` (`email`), + KEY `name` (`name`), + KEY `status` (`status`) +); \ No newline at end of file diff --git a/src/Factory/Handler/Api/LoginHandlerFactory.php b/src/Factory/Handler/Api/LoginHandlerFactory.php new file mode 100644 index 0000000..f537779 --- /dev/null +++ b/src/Factory/Handler/Api/LoginHandlerFactory.php @@ -0,0 +1,34 @@ +get(ResponseFactoryInterface::class), + $container->get(StreamFactoryInterface::class), + $container->get(AccountService::class), + $container->get(TokenService::class) + ); + } +} \ No newline at end of file diff --git a/src/Factory/Handler/Api/LogoutHandlerFactory.php b/src/Factory/Handler/Api/LogoutHandlerFactory.php new file mode 100644 index 0000000..a6daef4 --- /dev/null +++ b/src/Factory/Handler/Api/LogoutHandlerFactory.php @@ -0,0 +1,34 @@ +get(ResponseFactoryInterface::class), + $container->get(StreamFactoryInterface::class), + $container->get(AccountService::class), + $container->get(TokenService::class) + ); + } +} \ No newline at end of file diff --git a/src/Factory/Handler/Api/ProfileHandlerFactory.php b/src/Factory/Handler/Api/ProfileHandlerFactory.php new file mode 100644 index 0000000..24e6b7b --- /dev/null +++ b/src/Factory/Handler/Api/ProfileHandlerFactory.php @@ -0,0 +1,34 @@ +get(ResponseFactoryInterface::class), + $container->get(StreamFactoryInterface::class), + $container->get(AccountService::class), + $container->get(TokenService::class) + ); + } +} \ No newline at end of file diff --git a/src/Factory/Handler/Api/RefreshHandlerFactory.php b/src/Factory/Handler/Api/RefreshHandlerFactory.php new file mode 100644 index 0000000..c35cb18 --- /dev/null +++ b/src/Factory/Handler/Api/RefreshHandlerFactory.php @@ -0,0 +1,34 @@ +get(ResponseFactoryInterface::class), + $container->get(StreamFactoryInterface::class), + $container->get(AccountService::class), + $container->get(TokenService::class) + ); + } +} \ No newline at end of file diff --git a/src/Factory/Handler/Api/RegisterHandlerFactory.php b/src/Factory/Handler/Api/RegisterHandlerFactory.php new file mode 100644 index 0000000..7e12419 --- /dev/null +++ b/src/Factory/Handler/Api/RegisterHandlerFactory.php @@ -0,0 +1,34 @@ +get(ResponseFactoryInterface::class), + $container->get(StreamFactoryInterface::class), + $container->get(AccountService::class), + $container->get(TokenService::class) + ); + } +} \ No newline at end of file diff --git a/src/Factory/Handler/ErrorHandlerFactory.php b/src/Factory/Handler/ErrorHandlerFactory.php new file mode 100644 index 0000000..b4f39c7 --- /dev/null +++ b/src/Factory/Handler/ErrorHandlerFactory.php @@ -0,0 +1,30 @@ +get(ResponseFactoryInterface::class), + $container->get(StreamFactoryInterface::class) + ); + } +} \ No newline at end of file diff --git a/src/Factory/Middleware/AuthenticationMiddlewareFactory.php b/src/Factory/Middleware/AuthenticationMiddlewareFactory.php new file mode 100644 index 0000000..a0cbf45 --- /dev/null +++ b/src/Factory/Middleware/AuthenticationMiddlewareFactory.php @@ -0,0 +1,34 @@ +get(ResponseFactoryInterface::class), + $container->get(StreamFactoryInterface::class), + $container->get(AccountService::class), + $container->get(TokenService::class) + ); + } +} \ No newline at end of file diff --git a/src/Factory/Middleware/SecurityMiddlewareFactory.php b/src/Factory/Middleware/SecurityMiddlewareFactory.php new file mode 100644 index 0000000..357537f --- /dev/null +++ b/src/Factory/Middleware/SecurityMiddlewareFactory.php @@ -0,0 +1,30 @@ +get(ResponseFactoryInterface::class), + $container->get(StreamFactoryInterface::class) + ); + } +} \ No newline at end of file diff --git a/src/Factory/Middleware/ValidationMiddlewareFactory.php b/src/Factory/Middleware/ValidationMiddlewareFactory.php new file mode 100644 index 0000000..8000ad5 --- /dev/null +++ b/src/Factory/Middleware/ValidationMiddlewareFactory.php @@ -0,0 +1,32 @@ +get(ResponseFactoryInterface::class), + $container->get(StreamFactoryInterface::class), + $container->get(AccountService::class) + ); + } +} \ No newline at end of file diff --git a/src/Factory/Repository/AccountRepositoryFactory.php b/src/Factory/Repository/AccountRepositoryFactory.php new file mode 100644 index 0000000..55f95bf --- /dev/null +++ b/src/Factory/Repository/AccountRepositoryFactory.php @@ -0,0 +1,32 @@ +get(AdapterInterface::class), + new ReflectionHydrator(), + new Account('', '', '') + ); + } +} \ No newline at end of file diff --git a/src/Factory/Service/AccountServiceFactory.php b/src/Factory/Service/AccountServiceFactory.php new file mode 100644 index 0000000..f527814 --- /dev/null +++ b/src/Factory/Service/AccountServiceFactory.php @@ -0,0 +1,30 @@ +get(AccountRepositoryInterface::class), + $container->get(TokenService::class) + ); + } +} \ No newline at end of file diff --git a/src/Factory/Service/CacheServiceFactory.php b/src/Factory/Service/CacheServiceFactory.php new file mode 100644 index 0000000..0ed92fe --- /dev/null +++ b/src/Factory/Service/CacheServiceFactory.php @@ -0,0 +1,24 @@ +get(StorageAdapterFactoryInterface::class) + ); + } +} \ No newline at end of file diff --git a/src/Factory/Service/TokenServiceFactory.php b/src/Factory/Service/TokenServiceFactory.php new file mode 100644 index 0000000..3d17b9f --- /dev/null +++ b/src/Factory/Service/TokenServiceFactory.php @@ -0,0 +1,24 @@ +get(CacheService::class) + ); + } +} \ No newline at end of file diff --git a/src/Factory/Validator/EmailValidatorFactory.php b/src/Factory/Validator/EmailValidatorFactory.php new file mode 100644 index 0000000..3822081 --- /dev/null +++ b/src/Factory/Validator/EmailValidatorFactory.php @@ -0,0 +1,28 @@ +get(AccountService::class) + ); + } +} \ No newline at end of file diff --git a/src/Factory/Validator/IdentityValidatorFactory.php b/src/Factory/Validator/IdentityValidatorFactory.php new file mode 100644 index 0000000..261d3be --- /dev/null +++ b/src/Factory/Validator/IdentityValidatorFactory.php @@ -0,0 +1,28 @@ +get(AccountService::class) + ); + } +} \ No newline at end of file diff --git a/src/Factory/Validator/NameValidatorFactory.php b/src/Factory/Validator/NameValidatorFactory.php new file mode 100644 index 0000000..64c7a86 --- /dev/null +++ b/src/Factory/Validator/NameValidatorFactory.php @@ -0,0 +1,28 @@ +get(AccountService::class) + ); + } +} \ No newline at end of file diff --git a/src/Handler/Api/LoginHandler.php b/src/Handler/Api/LoginHandler.php new file mode 100644 index 0000000..dff7a43 --- /dev/null +++ b/src/Handler/Api/LoginHandler.php @@ -0,0 +1,56 @@ +responseFactory = $responseFactory; + $this->streamFactory = $streamFactory; + $this->accountService = $accountService; + $this->tokenService = $tokenService; + } + + public function handle(ServerRequestInterface $request): ResponseInterface + { + $requestBody = $request->getParsedBody(); + + // Set login params + $params = [ + 'identity' => $requestBody['identity'], + 'credential' => $requestBody['credential'], + ]; + + // Do log in + $result = $this->accountService->authentication($params); + + + return new JsonResponse($result); + } +} diff --git a/src/Handler/Api/LogoutHandler.php b/src/Handler/Api/LogoutHandler.php new file mode 100644 index 0000000..dfa3de0 --- /dev/null +++ b/src/Handler/Api/LogoutHandler.php @@ -0,0 +1,52 @@ +responseFactory = $responseFactory; + $this->streamFactory = $streamFactory; + $this->accountService = $accountService; + $this->tokenService = $tokenService; + } + + public function handle(ServerRequestInterface $request): ResponseInterface + { + + + $storage = $this->storageFactory->create('redis'); + $cache = new SimpleCacheDecorator($storage); + + echo '
';
+        var_dump($storage);
+        echo '
'; + die; + } +} \ No newline at end of file diff --git a/src/Handler/Api/ProfileHandler.php b/src/Handler/Api/ProfileHandler.php new file mode 100644 index 0000000..e8d4603 --- /dev/null +++ b/src/Handler/Api/ProfileHandler.php @@ -0,0 +1,53 @@ +responseFactory = $responseFactory; + $this->streamFactory = $streamFactory; + $this->accountService = $accountService; + $this->tokenService = $tokenService; + } + + public function handle(ServerRequestInterface $request): ResponseInterface + { + $account = $request->getAttribute('account'); + + // Set result array + $result = [ + 'result' => 'true', + 'data' => $account, + 'error' => '', + ]; + + return new JsonResponse($result); + } +} diff --git a/src/Handler/Api/RefreshHandler.php b/src/Handler/Api/RefreshHandler.php new file mode 100644 index 0000000..c96a4e3 --- /dev/null +++ b/src/Handler/Api/RefreshHandler.php @@ -0,0 +1,66 @@ +responseFactory = $responseFactory; + $this->streamFactory = $streamFactory; + $this->accountService = $accountService; + $this->tokenService = $tokenService; + } + + public function handle(ServerRequestInterface $request): ResponseInterface + { + $account = $request->getAttribute('account'); + + $accessToken = $this->tokenService->generate( + [ + 'user_id' => $account['id'], + 'type' => 'access', + 'roles' => [ + 'member', + ], + ] + ); + + // Set result array + $result = [ + 'result' => 'true', + 'data' => [ + 'access_token' => $accessToken, + ], + 'error' => '', + ]; + + // Set result + return new JsonResponse($result); + } +} diff --git a/src/Handler/Api/RegisterHandler.php b/src/Handler/Api/RegisterHandler.php new file mode 100644 index 0000000..0224a69 --- /dev/null +++ b/src/Handler/Api/RegisterHandler.php @@ -0,0 +1,66 @@ +responseFactory = $responseFactory; + $this->streamFactory = $streamFactory; + $this->accountService = $accountService; + $this->tokenService = $tokenService; + } + + public function handle(ServerRequestInterface $request): ResponseInterface + { + + var_dump([1,2,3,4,5,6]); + die; + + + $account = $this->accountService->addAccount($params); + + $params = [ + 'field' => 'email', + 'value' => '24745@me.com', + + ]; + + $isDuplicated = $this->accountService->isDuplicated($params); + + // Set result array + $result = [ + 'result' => 'true', + 'data' => $account, + 'error' => '', + ]; + + return new JsonResponse($result); + } +} \ No newline at end of file diff --git a/src/Handler/ErrorHandler.php b/src/Handler/ErrorHandler.php new file mode 100644 index 0000000..5aa0171 --- /dev/null +++ b/src/Handler/ErrorHandler.php @@ -0,0 +1,44 @@ +responseFactory = $responseFactory; + $this->streamFactory = $streamFactory; + } + + public function handle(ServerRequestInterface $request): ResponseInterface + { + $error = $request->getAttribute('error'); + $status = $request->getAttribute('status'); + + // Set result + return new JsonResponse( + [ + 'result' => 'false', + 'data' => [], + 'error' => $error, + ], + $status + ); + } +} diff --git a/src/Middleware/AuthenticationMiddleware.php b/src/Middleware/AuthenticationMiddleware.php new file mode 100644 index 0000000..8321811 --- /dev/null +++ b/src/Middleware/AuthenticationMiddleware.php @@ -0,0 +1,111 @@ +responseFactory = $responseFactory; + $this->streamFactory = $streamFactory; + $this->accountService = $accountService; + $this->tokenService = $tokenService; + $this->errorHandler = new ErrorHandler($this->responseFactory, $this->streamFactory); + } + + public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface + { + // Get request body + $requestBody = $request->getParsedBody(); + $routeMatch = $request->getAttribute('Laminas\Router\RouteMatch'); + + // Check token set + if (!isset($requestBody['token']) || empty($requestBody['token'])) { + $request = $request->withAttribute('status', StatusCodeInterface::STATUS_FORBIDDEN); + $request = $request->withAttribute('error', + [ + 'message' => 'Token is not set !', + 'code' => StatusCodeInterface::STATUS_FORBIDDEN, + ] + ); + return $this->errorHandler->handle($request); + } + + // parse token + $token = $this->tokenService->parse($requestBody['token']); + + // Check parsed token + if (!$token['status']) { + $request = $request->withAttribute('status', StatusCodeInterface::STATUS_FORBIDDEN); + $request = $request->withAttribute('error', + [ + 'message' => $token['message'], + 'code' => StatusCodeInterface::STATUS_FORBIDDEN, + ] + ); + return $this->errorHandler->handle($request); + } + + // Check token type + $type = ($routeMatch->getMatchedRouteName() == 'user/refresh') ? 'refresh' : 'access'; + if ($token['type'] != $type) { + $request = $request->withAttribute('status', StatusCodeInterface::STATUS_FORBIDDEN); + $request = $request->withAttribute('error', + [ + 'message' => 'This token not allowed for authentication', + 'code' => StatusCodeInterface::STATUS_FORBIDDEN, + ] + ); + return $this->errorHandler->handle($request); + } + + // Get account + $account = $this->accountService->getAccount(['id' => $token['user_id']]); + + // Check user is found + if (empty($account)) { + $request = $request->withAttribute('status', StatusCodeInterface::STATUS_UNAUTHORIZED); + $request = $request->withAttribute('error', + [ + 'message' => 'No user information found by this token !', + 'code' => StatusCodeInterface::STATUS_UNAUTHORIZED, + ] + ); + return $this->errorHandler->handle($request); + } + + // Set attribute + $request = $request->withAttribute('account', $account); + return $handler->handle($request); + } +} \ No newline at end of file diff --git a/src/Middleware/SecurityMiddleware.php b/src/Middleware/SecurityMiddleware.php new file mode 100644 index 0000000..5177af6 --- /dev/null +++ b/src/Middleware/SecurityMiddleware.php @@ -0,0 +1,39 @@ +responseFactory = $responseFactory; + $this->streamFactory = $streamFactory; + $this->errorHandler = new ErrorHandler($this->responseFactory, $this->streamFactory); + } + + public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface + { + // ToDo : check security + //var_dump($_POST); + + + return $handler->handle($request); + } +} \ No newline at end of file diff --git a/src/Middleware/ValidationMiddleware.php b/src/Middleware/ValidationMiddleware.php new file mode 100644 index 0000000..917b3c8 --- /dev/null +++ b/src/Middleware/ValidationMiddleware.php @@ -0,0 +1,146 @@ + true, + 'code' => StatusCodeInterface::STATUS_OK, + 'message' => '', + ]; + + public function __construct( + ResponseFactoryInterface $responseFactory, + StreamFactoryInterface $streamFactory, + AccountService $accountService + ) { + $this->responseFactory = $responseFactory; + $this->streamFactory = $streamFactory; + $this->accountService = $accountService; + $this->errorHandler = new ErrorHandler($this->responseFactory, $this->streamFactory); + } + + public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface + { + // Get information from request + $routeMatch = $request->getAttribute('Laminas\Router\RouteMatch'); + $parsedBody = $request->getParsedBody(); + + // Check parsedBody + switch ($routeMatch->getMatchedRouteName()) { + case 'user/login': + $this->loginIsValid($parsedBody); + break; + + case 'user/register': + $this->registerIsValid($parsedBody); + break; + } + + // Check if validation result is not true + if (!$this->validationResult['status']) { + $request = $request->withAttribute('status', $this->validationResult['code']); + $request = $request->withAttribute('error', + [ + 'message' => $this->validationResult['message'], + 'code' => $this->validationResult['code'], + ] + ); + return $this->errorHandler->handle($request); + } + + return $handler->handle($request); + } + + public function loginIsValid($params) + { + $identity = new Input('identity'); + $identity->getValidatorChain()->attach(new IdentityValidator($this->accountService, ['check_duplication' => false])); + + $credential = new Input('credential'); + $credential->getValidatorChain()->attach(new PasswordValidator()); + + $inputFilter = new InputFilter(); + $inputFilter->add($identity); + $inputFilter->add($credential); + $inputFilter->setData($params); + + if (!$inputFilter->isValid()) { + $message = []; + foreach ($inputFilter->getInvalidInput() as $error) { + $message[] = implode(', ', $error->getMessages()); + } + + return $this->validationResult = [ + 'status' => false, + 'code' => StatusCodeInterface::STATUS_FORBIDDEN, + 'message' => implode(', ', $message), + ]; + } + } + + protected function registerIsValid($params) + { + $email = new Input('email'); + $email->getValidatorChain()->attach(new EmailValidator($this->accountService)); + + $name = new Input('name'); + $name->getValidatorChain()->attach(new NameValidator($this->accountService)); + + $identity = new Input('identity'); + $identity->getValidatorChain()->attach(new IdentityValidator($this->accountService)); + + $credential = new Input('credential'); + $credential->getValidatorChain()->attach(new PasswordValidator()); + + $inputFilter = new InputFilter(); + $inputFilter->add($email); + $inputFilter->add($name); + $inputFilter->add($identity); + $inputFilter->add($credential); + $inputFilter->setData($params); + + if (!$inputFilter->isValid()) { + $message = []; + foreach ($inputFilter->getInvalidInput() as $error) { + $message[] = implode(', ', $error->getMessages()); + } + + return $this->validationResult = [ + 'status' => false, + 'code' => StatusCodeInterface::STATUS_FORBIDDEN, + 'message' => implode(', ', $message), + ]; + } + } +} \ No newline at end of file diff --git a/src/Model/Account.php b/src/Model/Account.php new file mode 100644 index 0000000..f6459c4 --- /dev/null +++ b/src/Model/Account.php @@ -0,0 +1,66 @@ +name = $name; + $this->identity = $identity; + $this->email = $email; + $this->status = $status; + $this->id = $id; + } + + /** + * @return int|null + */ + public function getId(): ?int + { + return $this->id; + } + + /** + * @return string + */ + public function getName(): string + { + return $this->name; + } + + /** + * @return string + */ + public function getIdentity(): string + { + return $this->identity; + } + + /** + * @return string + */ + public function getEmail(): string + { + return $this->email; + } + + /** + * @return int + */ + public function getStatus(): ?int + { + return $this->status; + } +} \ No newline at end of file diff --git a/src/Module.php b/src/Module.php new file mode 100644 index 0000000..f381b4c --- /dev/null +++ b/src/Module.php @@ -0,0 +1,22 @@ +getApplication(); + $eventManager = $application->getEventManager(); + $serviceManager = $application->getServiceManager(); + */ + } +} \ No newline at end of file diff --git a/src/Repository/AccountRepository.php b/src/Repository/AccountRepository.php new file mode 100644 index 0000000..2fc7be6 --- /dev/null +++ b/src/Repository/AccountRepository.php @@ -0,0 +1,155 @@ +db = $db; + $this->hydrator = $hydrator; + $this->accountPrototype = $accountPrototype; + } + + public function getAccounts($params = []) + { + // TODO: Implement getAccounts() method. + + $sql = new Sql($this->db); + $select = $sql->select('account'); + $statement = $sql->prepareStatementForSqlObject($select); + $result = $statement->execute(); + + if (!$result instanceof ResultInterface || !$result->isQueryResult()) { + return []; + } + + $resultSet = new HydratingResultSet($this->hydrator, $this->accountPrototype); + $resultSet->initialize($result); + return $resultSet; + } + + public function getAccount(array $params = []): Account + { + $sql = new Sql($this->db); + $select = $sql->select('account')->where(['id' => $params['id']]); + $statement = $sql->prepareStatementForSqlObject($select); + $result = $statement->execute(); + + + if (!$result instanceof ResultInterface || !$result->isQueryResult()) { + throw new RuntimeException( + sprintf( + 'Failed retrieving blog post with identifier "%s"; unknown database error.', + $params + ) + ); + } + + $resultSet = new HydratingResultSet($this->hydrator, $this->accountPrototype); + $resultSet->initialize($result); + $account = $resultSet->current(); + + if (!$account) { + throw new InvalidArgumentException( + sprintf( + 'Account with identifier "%s" not found.', + $params + ) + ); + } + + return $account; + } + + public function addAccount(array $params = []): Account + { + $insert = new Insert('account'); + $insert->values($params); + + $sql = new Sql($this->db); + $statement = $sql->prepareStatementForSqlObject($insert); + $result = $statement->execute(); + + if (!$result instanceof ResultInterface) { + throw new RuntimeException( + 'Database error occurred during blog post insert operation' + ); + } + + $id = $result->getGeneratedValue(); + + return $this->getAccount(['id' => $id]); + } + + public function count(array $params = []): int + { + // Set where + $columns = ['count' => new Expression('count(*)')]; + $where = [$params['field'] => $params['value']]; + if (isset($params['id']) && (int)$params['id'] > 0) { + $where['id <> ?'] = $params['id']; + } + + $sql = new Sql($this->db); + $select = $sql->select('account')->columns($columns)->where($where); + $statement = $sql->prepareStatementForSqlObject($select); + $row = $statement->execute()->current(); + + return (int)$row['count']; + } + + public function authentication(): AuthenticationService + { + // Call authAdapter + $authAdapter = new CallbackCheckAdapter( + $this->db, + 'account', + 'identity', + 'credential', + function ($hash, $password) { + $bcrypt = new Bcrypt(); + return $bcrypt->verify($password, $hash); + } + ); + + // Set condition + $select = $authAdapter->getDbSelect(); + $select->where(['status' => 1]); + + return new AuthenticationService(null, $authAdapter); + } +} \ No newline at end of file diff --git a/src/Repository/AccountRepositoryInterface.php b/src/Repository/AccountRepositoryInterface.php new file mode 100644 index 0000000..cd663c7 --- /dev/null +++ b/src/Repository/AccountRepositoryInterface.php @@ -0,0 +1,19 @@ +accountRepository = $accountRepository; + $this->tokenService = $tokenService; + } + + public function authentication($params): array + { + // Do login + $authentication = $this->accountRepository->authentication(); + $adapter = $authentication->getAdapter(); + $adapter->setIdentity($params['identity'])->setCredential($params['credential']); + + // Check login + if ($authentication->authenticate()->isValid()) { + // Get user account + $account = (array)$adapter->getResultRowObject( + [ + 'id', + 'name', + 'email', + 'identity', + ] + ); + + // Generate access token + $account['access_token'] = $this->tokenService->generate( + [ + 'user_id' => $account['id'], + 'type' => 'access', + 'roles' => [ + 'member', + ], + ] + ); + + // Generate refresh token + $account['refresh_token'] = $this->tokenService->generate( + [ + 'user_id' => $account['id'], + 'type' => 'refresh', + 'roles' => [ + 'member', + ], + ] + ); + + $result = [ + 'result' => 'true', + 'data' => $account, + 'error' => '', + ]; + } else { + $result = [ + 'result' => 'false', + 'data' => [], + 'error' => 'error in login', + ]; + } + + return $result; + } + + public function getAccount($params): array + { + $account = $this->accountRepository->getAccount($params); + + return [ + 'id' => $account->getId(), + 'name' => $account->getName(), + 'identity' => $account->getIdentity(), + 'email' => $account->getEmail(), + ]; + } + + public function addAccount($params): array + { + $params['credential'] = $this->generateCredential($params['credential']); + $params['status'] = 0; + $params['time_created'] = time(); + + $account = $this->accountRepository->addAccount($params); + + return [ + 'id' => $account->getId(), + 'name' => $account->getName(), + 'identity' => $account->getIdentity(), + 'email' => $account->getEmail(), + ]; + } + + public function generateCredential($credential): string + { + $bcrypt = new Bcrypt(); + return $bcrypt->create($credential); + } + + public function isDuplicated($type, $value): bool + { + return (bool) $this->accountRepository->count( + [ + 'field' => $type, + 'value' => $value, + ] + ); + } +} \ No newline at end of file diff --git a/src/Service/CacheService.php b/src/Service/CacheService.php new file mode 100644 index 0000000..4004e02 --- /dev/null +++ b/src/Service/CacheService.php @@ -0,0 +1,66 @@ +storageFactory = $storageFactory; + } + + public function setCache($key, $payload, $ttl) + { + // Set cache + $cache = $this->storageFactory->create( + 'redis', + [ + 'ttl' => $ttl, + 'server' => [ + '127.0.0.1', + 6379 + ], + ], + [ + [ + 'name' => 'serializer', + ], + ] + ); + $cache->addPlugin(new Serializer()); + $cache = new SimpleCacheDecorator($cache); + $cache->set($key, $payload); + } + + public function getCache($key) + { + $cache = $this->storageFactory->create( + 'redis', + [ + 'server' => [ + '127.0.0.1', + 6379 + ], + ], + [ + [ + 'name' => 'serializer', + ], + ] + ); + + $cache->addPlugin(new Serializer()); + $cache = new SimpleCacheDecorator($cache); + return $cache->get($key); + } +} \ No newline at end of file diff --git a/src/Service/ServiceInterface.php b/src/Service/ServiceInterface.php new file mode 100644 index 0000000..2e65edb --- /dev/null +++ b/src/Service/ServiceInterface.php @@ -0,0 +1,7 @@ +config = new Config(include __DIR__ . '/../../config/custom.config.php'); + $this->cacheService = $cacheService; + } + + public function generate($params): string + { + // Set cache key + $key = Rand::getString('16', 'abcdefghijklmnopqrstuvwxyz0123456789'); + + // Set payload + switch ($params['type']) { + default: + case 'access': + $ttl = $this->config->jwt->exp_access; + $payload = [ + 'id' => $key, + 'uid' => $params['user_id'], + 'iat' => time(), + 'exp' => time() + $ttl, + 'type' => $params['type'], + 'roles' => $params['roles'], + ]; + break; + + case 'refresh': + $ttl = $this->config->jwt->exp_refresh; + $payload = [ + 'id' => $key, + 'uid' => $params['user_id'], + 'iat' => time(), + 'exp' => time() + $ttl, + 'type' => $params['type'], + ]; + break; + } + + // Set to cache + $this->cacheService->setCache($key, $payload, $ttl); + + return JWT::encode($payload, $this->config->jwt->secret, 'HS256'); + } + + public function parse($token): array + { + try { + $decoded = JWT::decode($token, new Key($this->config->jwt->secret, 'HS256')); + + // Get data from cache + $cacheCheck = $this->cacheService->getCache($decoded->id); + + if (isset($cacheCheck['id']) && $cacheCheck['exp'] > time()) { + return [ + 'status' => true, + 'id' => $decoded->id, + 'user_id' => $decoded->uid, + 'type' => $decoded->type, + ]; + } else { + return [ + 'status' => false, + 'message' => 'Token not valid !', + ]; + } + } catch (Exception $e) { + return [ + 'status' => false, + 'message' => $e->getMessage(), + ]; + } + } +} \ No newline at end of file diff --git a/src/Validator/EmailValidator.php b/src/Validator/EmailValidator.php new file mode 100644 index 0000000..60e0305 --- /dev/null +++ b/src/Validator/EmailValidator.php @@ -0,0 +1,86 @@ + [], + 'check_duplication' => true, + 'useMxCheck' => false, + 'useDeepMxCheck' => false, + 'useDomainCheck' => true, + 'allow' => Hostname::ALLOW_DNS, + 'strict' => true, + 'hostnameValidator' => null, + ]; + + /** @var AccountService */ + protected AccountService $accountService; + + /** + * {@inheritDoc} + */ + public function __construct( + AccountService $accountService, + $options = [] + ) { + $this->accountService = $accountService; + $this->options = array_merge($this->options, $options); + + $this->messageTemplates = [ + self::RESERVED => 'User email is reserved', + self::USED => 'User email is already used', + ]; + + parent::__construct($this->options); + } + + /** + * User name validate + * + * @param mixed $value + * @param array|null $context + * + * @return bool + */ + public function isValid($value, array $context = null): bool + { + $this->setValue($value); + + $result = parent::isValid($value); + if (!$result) { + return false; + } + + if (isset($this->options['blacklist']) && !empty($this->options['blacklist'])) { + $pattern = is_array($this->options['blacklist']) ? implode('|', $this->options['blacklist']) : $this->options['blacklist']; + if (preg_match('/(' . $pattern . ')/', $value)) { + $this->error(static::RESERVED); + return false; + } + } + + if ($this->options['check_duplication']) { + $isDuplicated = $this->accountService->isDuplicated('email', $value); + if ($isDuplicated) { + $this->error(static::USED); + return false; + } + } + + return true; + } +} diff --git a/src/Validator/IdentityValidator.php b/src/Validator/IdentityValidator.php new file mode 100644 index 0000000..8309be3 --- /dev/null +++ b/src/Validator/IdentityValidator.php @@ -0,0 +1,117 @@ + '/[^a-zA-Z0-9\_\-]/', + 'strict-space' => '/[^a-zA-Z0-9\_\-\s]/', + 'medium' => '/[^a-zA-Z0-9\_\-\<\>\,\.\$\%\#\@\!\\\'\"]/', + 'medium-space' => '/[^a-zA-Z0-9\_\-\<\>\,\.\$\%\#\@\!\\\'\"\s]/', + 'loose' => '/[\000-\040]/', + 'loose-space' => '/[\000-\040][\s]/', + ]; + + /** @var array */ + protected $options + = [ + 'format' => 'strict', + 'blacklist' => [], + 'check_duplication' => true, + ]; + + /** @var AccountService */ + protected AccountService $accountService; + + /** + * {@inheritDoc} + */ + public function __construct( + AccountService $accountService, + $options = [] + ) { + $this->accountService = $accountService; + $this->options = array_merge($this->options, $options); + + $this->messageTemplates = [ + self::INVALID => 'Invalid identity: %formatHint%', + self::RESERVED => 'Identity is reserved', + self::TAKEN => 'Identity is already taken', + ]; + + $this->formatMessage = [ + 'strict' => 'Only alphabetic and digits are allowed', + 'strict-space' => 'Only alphabetic, digits and spaces are allowed', + 'medium' => 'Only ASCII characters are allowed', + 'medium-space' => 'Only ASCII characters and spaces are allowed', + 'loose' => 'Only multi-byte characters are allowed', + 'loose-space' => 'Only multi-byte characters and spaces are allowed', + ]; + + parent::__construct($options); + } + + /** + * identity validate + * + * @param mixed $value + * @param array|null $context + * + * @return bool + */ + public function isValid($value, array $context = null): bool + { + $this->setValue($value); + $format = empty($this->options['format']) + ? 'strict' : $this->options['format']; + if (preg_match($this->formatPattern[$format], $value)) { + $this->formatHint = $this->formatMessage[$format]; + $this->error(static::INVALID); + return false; + } + + if (!empty($this->options['blacklist'])) { + $pattern = is_array($this->options['blacklist']) + ? implode('|', $this->options['blacklist']) + : $this->options['blacklist']; + if (preg_match('/(' . $pattern . ')/', $value)) { + $this->error(static::RESERVED); + return false; + } + } + + if ($this->options['check_duplication']) { + $isDuplicated = $this->accountService->isDuplicated('identity', $value); + if ($isDuplicated) { + $this->error(static::TAKEN); + return false; + } + } + + return true; + } +} diff --git a/src/Validator/NameValidator.php b/src/Validator/NameValidator.php new file mode 100644 index 0000000..13e2dcc --- /dev/null +++ b/src/Validator/NameValidator.php @@ -0,0 +1,135 @@ + 'formatHint', + ]; + + /** + * Format hint + * @var string + */ + protected string $formatHint; + + /** @var array */ + protected $messageTemplates = []; + + /** @var array */ + protected array $formatMessage = []; + + /** + * Format pattern + * @var array + */ + protected array $formatPattern + = [ + 'strict' => '/[^a-zA-Z0-9\_\-]/', + 'strict-space' => '/[^a-zA-Z0-9\_\-\s]/', + 'medium' => '/[^a-zA-Z0-9\_\-\<\>\,\.\$\%\#\@\!\\\'\"]/', + 'medium-space' => '/[^a-zA-Z0-9\_\-\<\>\,\.\$\%\#\@\!\\\'\"\s]/', + 'loose' => '/[\000-\040]/', + 'loose-space' => '/[\000-\040][\s]/', + ]; + + /** + * Options + * @var array + */ + protected $options + = [ + 'format' => 'medium-space', + 'blacklist' => [], + 'check_duplication' => true, + ]; + + /** @var AccountService */ + protected AccountService $accountService; + + /** + * {@inheritDoc} + */ + public function __construct( + AccountService $accountService, + $options = [] + ) { + $this->accountService = $accountService; + $this->options = array_merge($this->options, $options); + + $this->messageTemplates = $this->messageTemplates + [ + self::INVALID => 'Invalid name: %formatHint%', + self::RESERVED => 'Name is reserved', + self::TAKEN => 'Name is already taken', + ]; + + $this->formatMessage = [ + 'strict' => 'Only alphabetic and digits are allowed', + 'strict-space' => 'Only alphabetic, digits and spaces are allowed', + 'medium' => 'Only ASCII characters are allowed', + 'medium-space' => 'Only ASCII characters and spaces are allowed', + 'loose' => 'Only multi-byte characters are allowed', + 'loose-space' => 'Only multi-byte characters and spaces are allowed', + ]; + + parent::__construct($options); + } + + /** + * User name validate + * + * @param mixed $value + * @param array|null $context + * + * @return bool + */ + public function isValid($value, array $context = null): bool + { + $this->setValue($value); + $format = empty($this->options['format']) + ? 'strict' : $this->options['format']; + if (preg_match($this->formatPattern[$format], $value)) { + $this->formatHint = $this->formatMessage[$format]; + $this->error(static::INVALID); + return false; + } + + if (!empty($this->options['blacklist'])) { + $pattern = is_array($this->options['blacklist']) + ? implode('|', $this->options['blacklist']) + : $this->options['blacklist']; + if (preg_match('/(' . $pattern . ')/', $value)) { + $this->error(static::RESERVED); + return false; + } + } + + if ($this->options['check_duplication']) { + $isDuplicated = $this->accountService->isDuplicated('name', $value); + if ($isDuplicated) { + $this->error(static::TAKEN); + return false; + } + } + + return true; + } +} diff --git a/src/Validator/PasswordValidator.php b/src/Validator/PasswordValidator.php new file mode 100644 index 0000000..a9fd8fe --- /dev/null +++ b/src/Validator/PasswordValidator.php @@ -0,0 +1,72 @@ + 'max', + 'min' => 'min', + ]; + + protected $max; + protected $min; + + private array $messageTemplates; + + private $options; + + public function __construct() + { + $this->messageTemplates = [ + self::TOO_SHORT => 'Password is less than %min% characters long', + self::TOO_LONG => 'Password is more than %max% characters long', + ]; + + parent::__construct(); + } + + public function isValid($value): bool + { + $this->setValue($value); + $this->setConfigOption(); + + if (!empty($this->options['max']) + && $this->options['max'] < strlen($value) + ) { + $this->max = $this->options['max']; + $this->error(static::TOO_LONG); + return false; + } + if (!empty($this->options['min']) + && $this->options['min'] > strlen($value) + ) { + $this->min = $this->options['min']; + $this->error(static::TOO_SHORT); + return false; + } + + return true; + } + + /** + * Set username validator according to config + * + * @return $this + */ + public function setConfigOption() + { + $this->options = [ + 'min' => 8, + 'max' => 32, + ]; + + return $this; + } +} diff --git a/test/readme.md b/test/readme.md new file mode 100644 index 0000000..e69de29