diff --git a/.idea/signer.iml b/.idea/signer.iml index dd914ac..21140ef 100644 --- a/.idea/signer.iml +++ b/.idea/signer.iml @@ -4,8 +4,6 @@ - - diff --git a/backend/.env b/backend/.env index 62f06bc..cdccb51 100644 --- a/backend/.env +++ b/backend/.env @@ -1,23 +1,5 @@ -# In all environments, the following files are loaded if they exist, -# the latter taking precedence over the former: -# -# * .env contains default values for the environment variables needed by the app -# * .env.local uncommitted file with local overrides -# * .env.$APP_ENV committed environment-specific defaults -# * .env.$APP_ENV.local uncommitted environment-specific overrides -# -# Real environment variables win over .env files. -# -# DO NOT DEFINE PRODUCTION SECRETS IN THIS FILE NOR IN ANY OTHER COMMITTED FILES. -# https://symfony.com/doc/current/configuration/secrets.html -# -# Run "composer dump-env prod" to compile .env files for production use (requires symfony/flex >=1.2). -# https://symfony.com/doc/current/best_practices.html#use-environment-variables-for-infrastructure-configuration - -###> symfony/framework-bundle ### APP_ENV=dev APP_SECRET=850da55654c68f779822ea80d2b66a94 -###< symfony/framework-bundle ### ###> doctrine/doctrine-bundle ### # Format described at https://www.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/configuration.html#connecting-using-a-url @@ -28,3 +10,4 @@ APP_SECRET=850da55654c68f779822ea80d2b66a94 # DATABASE_URL="mysql://app:!ChangeMe!@127.0.0.1:3306/app?serverVersion=10.11.2-MariaDB&charset=utf8mb4" DATABASE_URL="postgresql://app:!ChangeMe!@127.0.0.1:5432/app?serverVersion=16&charset=utf8" ###< doctrine/doctrine-bundle ### +DOT_DOT_URL='http://dot-dot.local' \ No newline at end of file diff --git a/backend/composer.json b/backend/composer.json index 5068288..b04d8e4 100755 --- a/backend/composer.json +++ b/backend/composer.json @@ -17,6 +17,7 @@ "symfony/flex": "^2", "symfony/framework-bundle": "6.2.*", "symfony/runtime": "6.2.*", + "symfony/serializer": "6.2.*", "symfony/ux-chartjs": "*", "symfony/yaml": "6.2.*" }, diff --git a/backend/composer.lock b/backend/composer.lock index 4aa2c94..83bd001 100644 --- a/backend/composer.lock +++ b/backend/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "bf87fa37fe523eb581063fa93c0fda64", + "content-hash": "0d9c45fd694083582028c80344c66518", "packages": [ { "name": "composer/package-versions-deprecated", @@ -5393,6 +5393,107 @@ ], "time": "2023-07-13T14:28:09+00:00" }, + { + "name": "symfony/serializer", + "version": "v6.2.13", + "source": { + "type": "git", + "url": "https://github.com/symfony/serializer.git", + "reference": "19083104e606ecf8a48baa8ed310c7a073887037" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/serializer/zipball/19083104e606ecf8a48baa8ed310c7a073887037", + "reference": "19083104e606ecf8a48baa8ed310c7a073887037", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/polyfill-ctype": "~1.8" + }, + "conflict": { + "doctrine/annotations": "<1.12", + "phpdocumentor/reflection-docblock": "<3.2.2", + "phpdocumentor/type-resolver": "<1.4.0", + "symfony/dependency-injection": "<5.4", + "symfony/property-access": "<5.4", + "symfony/property-info": "<5.4.24|>=6,<6.2.11", + "symfony/uid": "<5.4", + "symfony/yaml": "<5.4" + }, + "require-dev": { + "doctrine/annotations": "^1.12|^2", + "phpdocumentor/reflection-docblock": "^3.2|^4.0|^5.0", + "symfony/cache": "^5.4|^6.0", + "symfony/config": "^5.4|^6.0", + "symfony/dependency-injection": "^5.4|^6.0", + "symfony/error-handler": "^5.4|^6.0", + "symfony/filesystem": "^5.4|^6.0", + "symfony/form": "^5.4|^6.0", + "symfony/http-foundation": "^5.4|^6.0", + "symfony/http-kernel": "^5.4|^6.0", + "symfony/mime": "^5.4|^6.0", + "symfony/property-access": "^5.4|^6.0", + "symfony/property-info": "^5.4.24|^6.2.11", + "symfony/uid": "^5.4|^6.0", + "symfony/validator": "^5.4|^6.0", + "symfony/var-dumper": "^5.4|^6.0", + "symfony/var-exporter": "^5.4|^6.0", + "symfony/yaml": "^5.4|^6.0" + }, + "suggest": { + "psr/cache-implementation": "For using the metadata cache.", + "symfony/config": "For using the XML mapping loader.", + "symfony/mime": "For using a MIME type guesser within the DataUriNormalizer.", + "symfony/property-access": "For using the ObjectNormalizer.", + "symfony/property-info": "To deserialize relations.", + "symfony/var-exporter": "For using the metadata compiler.", + "symfony/yaml": "For using the default YAML mapping loader." + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Serializer\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Handles serializing and deserializing data structures, including object graphs, into array structures or other formats like XML and JSON.", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/serializer/tree/v6.2.13" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-07-27T16:18:16+00:00" + }, { "name": "symfony/service-contracts", "version": "v3.5.0", diff --git a/backend/config/services.yaml b/backend/config/services.yaml index 2d6a76f..aede265 100644 --- a/backend/config/services.yaml +++ b/backend/config/services.yaml @@ -14,11 +14,20 @@ services: # makes classes in src/ available to be used as services # this creates a service per class whose id is the fully-qualified class name App\: - resource: '../src/' + resource: '../src/*' exclude: - - '../src/DependencyInjection/' - - '../src/Entity/' - '../src/Kernel.php' + - '../src/*/Api/{Request,Response}' + - '../src/*/{Exception,Entity,Dto,Enum,Helper,Model}' # add more service definitions when explicit configuration is needed # please note that last definitions always *replace* previous ones + + guzzle.http_client: + class: GuzzleHttp\Client + + GuzzleHttp\Client: '@guzzle.http_client' + + App\Api\ApiParams: + arguments: + $endPointUrl: '%env(DOT_DOT_URL)%' \ No newline at end of file diff --git a/backend/src/Controller/SignController.php b/backend/src/Controller/SignController.php deleted file mode 100644 index 129a91d..0000000 --- a/backend/src/Controller/SignController.php +++ /dev/null @@ -1,18 +0,0 @@ -client = $client; + $this->responseHandler = $responseHandler; + } +} diff --git a/backend/src/Infrastructure/External/Api/ApiSleep.php b/backend/src/Infrastructure/External/Api/ApiSleep.php new file mode 100644 index 0000000..cccea40 --- /dev/null +++ b/backend/src/Infrastructure/External/Api/ApiSleep.php @@ -0,0 +1,13 @@ + 0) { + sleep($time); + } + } +} diff --git a/backend/src/Infrastructure/External/Api/BinaryStringFileResult.php b/backend/src/Infrastructure/External/Api/BinaryStringFileResult.php new file mode 100644 index 0000000..6331c2c --- /dev/null +++ b/backend/src/Infrastructure/External/Api/BinaryStringFileResult.php @@ -0,0 +1,26 @@ +saveToTempFile($content); + } + + private function saveToTempFile(string $content): void + { + $this->tempFileName = sprintf('%s/%s_%s', sys_get_temp_dir(), 'Document', time()); + file_put_contents($this->tempFileName, $content); + } +} diff --git a/backend/src/Infrastructure/External/Api/ResponseHandler.php b/backend/src/Infrastructure/External/Api/ResponseHandler.php new file mode 100644 index 0000000..3e60520 --- /dev/null +++ b/backend/src/Infrastructure/External/Api/ResponseHandler.php @@ -0,0 +1,110 @@ +response->getBody()->getSize()); + } + + /** + * Возвращает ответ в текстовом виде, применит фильтры при их наличие. + * + * @return string + */ + public function getContent(): string + { + $content = (string)$this->response->getBody(); + + return $this->filterContent($content); + } + + /** + * Добавляет фильтр контента иногда необходимо поправить ответ, что бы в итоге возвращался нужный тип данных. + * Бывает иногда сервер, возвращает массив данных и при преобразовании json_decode возвращается + * массив вместо stdClass. + */ + public function addContentFilter(callable $filter): self + { + $this->contentFilters[] = $filter; + + return $this; + } + + /** + * Преобразует json в объект + * + * @return stdClass + */ + public function getContentStdFromJson(): stdClass + { + return json_decode($this->getContent()); + } + + public function getContentAsBinaryStingResult(): BinaryStringFileResult + { + return new BinaryStringFileResult($this->getContent()); + } + + /** + * @return stdClass[] + */ + public function getContentArrayStdFromJson(): array + { + return json_decode($this->getContent()); + } + + /** + * Преобразует json в массив. + */ + public function getContentJsonToArray(): array + { + return $this->contentIsEmpty() ? [] : json_decode($this->getContent(), true); + } + + /** + * Устанавливает ответ в обработчик. + */ + public function setResponse(ResponseInterface $response): self + { + $this->response = $response; + + return $this; + } + + /** + * Фильтруем ответ + * + * @param string $content + * + * @return string + */ + private function filterContent(string $content): string + { + if (!$this->contentFilters) { + return $content; + } + + foreach ($this->contentFilters as $filter) { + $content = $filter($content); + } + + $this->contentFilters = []; + + return $content; + } +} diff --git a/backend/src/Infrastructure/Http/RequestDtoInterface.php b/backend/src/Infrastructure/Http/RequestDtoInterface.php new file mode 100644 index 0000000..f809ecb --- /dev/null +++ b/backend/src/Infrastructure/Http/RequestDtoInterface.php @@ -0,0 +1,7 @@ +getType(); + + if (empty($type) || !class_exists($type)) { + return false; + } + + try { + $reflection = new ReflectionClass($type); + } catch (Exception $e) { + return false; + } + + return $reflection->implementsInterface(RequestDtoInterface::class); + } + + public function resolve(Request $request, ArgumentMetadata $argument): iterable + { + if (($class = $argument->getType()) === null || !class_exists($class)) { + throw new RuntimeException('dto exception'); + } + + $object = $this->serializer->deserialize($request->getContent(), $class, 'json'); + + if (is_object($object)) { + $this->validateObject($object); + } + + yield $object; + } + + private function validateObject(object $object): void + { + $errors = $this->validator->validate($object); + + if (count($errors) > 0) { + /* todo получать из вне сообщение для ошибки */ + throw new RuntimeException($errors, 'Ошибка валидации'); + } + } +} diff --git a/backend/src/Repository/.gitignore b/backend/src/Repository/.gitignore deleted file mode 100644 index e69de29..0000000 diff --git a/backend/src/Sign/Api/Api.php b/backend/src/Sign/Api/Api.php new file mode 100644 index 0000000..55d2aeb --- /dev/null +++ b/backend/src/Sign/Api/Api.php @@ -0,0 +1,57 @@ + 'application/json', + 'Authorization' => $token, + ]; + $options = [ + 'multipart' => [ + [ + 'name' => 'type', + 'contents' => self::TYPE_OTHER + ], + [ + 'name' => 'file', + 'contents' => fopen($urlDocument, 'r'), + 'filename' => 'sign_attorney.pdf', + 'headers' => [ + 'Content-Type' => '' + ] + ] + ]]; + $request = new Request('POST', sprintf('%s%s%s', $url, self::API_DOCUMENT, $batch), $headers); + $res = $this->client->sendAsync($request, $options)->wait(); + + return $this->responseHandler->setResponse($res)->getContentJsonToArray(); + } + + public function download(string $url, string $token): BinaryStringFileResult + { + $params = [ + RequestOptions::HEADERS => [ + 'Authorization' => $token, + ], + ]; + + $response = $this->client->get($url, $params); + + return $this->responseHandler->setResponse($response)->getContentAsBinaryStingResult(); + } +} \ No newline at end of file diff --git a/backend/src/Sign/Api/ApiParams.php b/backend/src/Sign/Api/ApiParams.php new file mode 100644 index 0000000..cafcdc5 --- /dev/null +++ b/backend/src/Sign/Api/ApiParams.php @@ -0,0 +1,13 @@ +apiKey !== self::API_KEY) { + throw new AccessDeniedException('доступ запрещен'); + } + + $token = $request->server->get('HTTP_AUTHORIZATION'); + + return new JsonResponse($this->signService->signDocument($signRequest->url, $token, $signRequest->batch)); + } +} \ No newline at end of file diff --git a/backend/src/Sign/SignService.php b/backend/src/Sign/SignService.php new file mode 100644 index 0000000..a0a352f --- /dev/null +++ b/backend/src/Sign/SignService.php @@ -0,0 +1,64 @@ +api->apiParams = $this->apiParams; + + try { + $document = $this->api->download($url, $token); + + $this->sign($document->tempFileName); + + $response = $this->api->send($this->apiParams->endPointUrl, $token, $document->tempFileName . '_sign.pdf', $batch); + + $this->removeExistingDocument($document); + + return $response; + } catch (Exception $e) { + throw new RuntimeException($e->getMessage()); + } + } + + private function sign (string $documentUrl): void + { + match ($_ENV['APP_ENV']) { + 'dev' => $this->signDev($documentUrl), + 'prod' => $this->signProd($documentUrl), + }; + } + + private function signDev(string $documentUrl): void + { + exec(sprintf('cp %s %s_sign.pdf', $documentUrl, $documentUrl)); + } + + private function signProd(string $documentUrl): void + { + exec(sprintf('cp %s %s.pdf', $documentUrl, $documentUrl)); + exec(sprintf('pdfcpro sign /mnt/t/%s.pdf -out /mnt/t/%s_sign.pdf -cert ${SHA} -text "\n\t\tПодписано ЭП\n\t\t{subject/cn}\n\t\tСертификат {sha1}\n\t\tДействителен от {since} до {until}\n\t\tДата {date}\n\t\t{subject/t}\n\t\t{subject/fullname}\n\t\t" -fontfile /usr/local/share/fonts/Inter-Bold.ttf -fontsize 8 -x 2 -y 2 -w 96 -h 9', $documentUrl, $documentUrl)); + } + + private function removeExistingDocument(BinaryStringFileResult $document): void + { + if (file_exists($document->tempFileName)) { + unlink($document->tempFileName); + } + } +} \ No newline at end of file