DDB-1753 #5

Merged
dcherednik merged 21 commits from DDB-1753 into master 2025-03-21 13:37:34 +03:00
25 changed files with 594 additions and 288 deletions

View File

@ -12,4 +12,4 @@ DATABASE_URL="postgresql://app:!ChangeMe!@127.0.0.1:5432/app?serverVersion=16&ch
###< doctrine/doctrine-bundle ### ###< doctrine/doctrine-bundle ###
DOT_DOT_URL='http://dot-dot.local' DOT_DOT_URL='http://dot-dot.local'
API_TOKEN='secret' API_TOKEN='secret'

579
backend/composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -10,4 +10,5 @@ return [
Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true], Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true],
Symfony\UX\StimulusBundle\StimulusBundle::class => ['all' => true], Symfony\UX\StimulusBundle\StimulusBundle::class => ['all' => true],
Symfony\UX\Chartjs\ChartjsBundle::class => ['all' => true], Symfony\UX\Chartjs\ChartjsBundle::class => ['all' => true],
Symfony\UX\TwigComponent\TwigComponentBundle::class => ['all' => true],
]; ];

View File

@ -0,0 +1,5 @@
twig_component:
anonymous_template_directory: 'components/'
defaults:
# Namespace & directory for components
App\Twig\Components\: 'components/'

View File

@ -1,5 +1,5 @@
controllers: controllers_sign_document:
resource: resource:
path: ../src/Controller/ path: ../src/SignDocument/Controller/
namespace: App\Controller namespace: App\SignDocument\Controller
type: attribute type: attribute

View File

@ -28,6 +28,6 @@ services:
GuzzleHttp\Client: '@guzzle.http_client' GuzzleHttp\Client: '@guzzle.http_client'
App\Api\ApiParams: App\SignDocument\Api\ApiParams:
arguments: arguments:
$endPointUrl: '%env(DOT_DOT_URL)%' $endPointUrl: '%env(DOT_DOT_URL)%'

View File

View File

@ -0,0 +1,9 @@
<?php
namespace App\Infrastructure\Api\Response;
// Абстрактный респонс - от него наследуются нужные респонсы для стандартизации ответа в ApiHelperTrait -> createNewJsonResponse
abstract class AbstractResponse
{
}

View File

@ -4,6 +4,8 @@ declare(strict_types=1);
namespace App\Infrastructure\External\Api; namespace App\Infrastructure\External\Api;
use RuntimeException;
/** /**
* Враппер бинарных фалов, при создании класса содает новый временный файл, * Враппер бинарных фалов, при создании класса содает новый временный файл,
* при необходимости сохраняет файл под новым именем * при необходимости сохраняет файл под новым именем
@ -23,4 +25,14 @@ class BinaryStringFileResult
$this->tempFileName = sprintf('%s/%s_%s', sys_get_temp_dir(), 'Document', time()); $this->tempFileName = sprintf('%s/%s_%s', sys_get_temp_dir(), 'Document', time());
file_put_contents($this->tempFileName, $content); file_put_contents($this->tempFileName, $content);
} }
public function remove(): bool
{
if (file_exists($this->tempFileName)) {
unlink($this->tempFileName);
return true;
}
throw new RuntimeException('Temp file not found');
}
} }

View File

@ -0,0 +1,51 @@
<?php
namespace App\Infrastructure\Traits;
use App\Infrastructure\Api\Response\AbstractResponse;
use ReflectionClass;
use Symfony\Component\HttpFoundation\JsonResponse;
trait ApiHelperTrait
{
public function createJsonResponse(array $body, string $message = 'OK', int $code = 200): JsonResponse
{
return new JsonResponse([
'message' => $message,
'body' => $body,
], $code);
}
public function createPaginateJsonResponse(array $items, int $limit, int $totalCount): JsonResponse
{
return $this->createJsonResponse(
[
'items' => $items,
'pages' => ceil($totalCount / $limit),
'totalCount' => $totalCount,
]
);
}
public function createJsonResponseFromObject(AbstractResponse $body, string $message = 'OK', int $code = 200): JsonResponse
{
return new JsonResponse([
'message' => $message,
'body' => $this->toArray($body),
], $code);
}
private function toArray(AbstractResponse $body): array
{
$reflect = new ReflectionClass($body);
$props = $reflect->getProperties();
$array = [];
foreach ($props as $prop) {
$prop->setAccessible(true);
$array[$prop->getName()] = $prop->getValue($body);
$prop->setAccessible(false);
}
return $array;
}
}

View File

View File

@ -2,11 +2,10 @@
declare(strict_types=1); declare(strict_types=1);
namespace App\Api; namespace App\SignDocument\Api;
use App\Infrastructure\External\Api\AbstractApi; use App\Infrastructure\External\Api\AbstractApi;
use App\Infrastructure\External\Api\BinaryStringFileResult; use App\Infrastructure\External\Api\BinaryStringFileResult;
use GuzzleHttp\Psr7\Request;
use GuzzleHttp\RequestOptions; use GuzzleHttp\RequestOptions;
class Api extends AbstractApi class Api extends AbstractApi

View File

@ -2,7 +2,7 @@
declare(strict_types=1); declare(strict_types=1);
namespace App\Api; namespace App\SignDocument\Api;
class ApiParams class ApiParams
{ {

View File

@ -0,0 +1,13 @@
<?php
declare(strict_types=1);
namespace App\SignDocument\Api\Request;
use App\Infrastructure\Http\RequestDtoInterface;
class DigitalSignatureRequest implements RequestDtoInterface
{
public string $url;
public string $apiToken;
}

View File

@ -2,7 +2,7 @@
declare(strict_types=1); declare(strict_types=1);
namespace App\Api\Request; namespace App\SignDocument\Api\Request;
use App\Infrastructure\Http\RequestDtoInterface; use App\Infrastructure\Http\RequestDtoInterface;

View File

@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace App\SignDocument\Api\Response;
use App\Infrastructure\Api\Response\AbstractResponse;
class DigitalSignatureResponse extends AbstractResponse
{
public function __construct(
public string $hash,
public string $content,
)
{
}
}

View File

@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace App\SignDocument\Controller;
use App\Infrastructure\Traits\ApiHelperTrait;
use App\SignDocument\Api\Request\DigitalSignatureRequest;
use App\SignDocument\Services\DigitalSignatureService;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
class DigitalSignatureController extends AbstractController
{
use ApiHelperTrait;
public function __construct(
private readonly DigitalSignatureService $digitalSignatureService
){
}
#[Route(path: '/digital/sign', name: 'app.digital.sign', methods: ['POST'])]
public function __invoke(Request $request, DigitalSignatureRequest $digitalSignatureRequest): Response
{
$token = $request->server->get('HTTP_AUTHORIZATION');
return $this->createJsonResponseFromObject(
$this->digitalSignatureService->getSignature($digitalSignatureRequest, $token)
);
}
}

View File

@ -2,15 +2,15 @@
declare(strict_types=1); declare(strict_types=1);
namespace App\Controller; namespace App\SignDocument\Controller;
use App\Api\Request\SignRequest; use App\SignDocument\Api\Request\SignRequest;
use App\SignService; use App\SignDocument\Services\SignService;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
class SignController extends AbstractController class SignController extends AbstractController
{ {

View File

@ -2,7 +2,7 @@
declare(strict_types=1); declare(strict_types=1);
namespace App; namespace App\SignDocument\Services;
class DevSignService class DevSignService
{ {

View File

@ -0,0 +1,55 @@
<?php
declare(strict_types=1);
namespace App\SignDocument\Services;
use App\Infrastructure\External\Api\BinaryStringFileResult;
use App\SignDocument\Api\Api;
use App\SignDocument\Api\ApiParams;
use App\SignDocument\Api\Request\DigitalSignatureRequest;
use App\SignDocument\Api\Response\DigitalSignatureResponse;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Exception;
use RuntimeException;
class DigitalSignatureService
{
private BinaryStringFileResult $document;
public function __construct(
private readonly Api $api,
private readonly ApiParams $apiParams,
){
}
public function __destruct()
{
$this->document->remove();
}
public function getSignature(DigitalSignatureRequest $request, string $token): DigitalSignatureResponse
{
if ($_ENV['API_TOKEN'] !== $request->apiToken) {
throw new AccessDeniedHttpException('Доступ запрещен');
}
$this->api->apiParams = $this->apiParams;
try {
$this->document = $this->api->download($request->url, $token);
exec(sprintf('cp %s %s.pdf', $this->document->tempFileName, $this->document->tempFileName));
exec(sprintf('cryptcp -sign -detached -der %s.pdf', $this->document->tempFileName));
$response = base64_encode(file_get_contents($this->document->tempFileName . '.pdf.sgn'));
return new DigitalSignatureResponse(
hash: $response,
content: base64_encode(file_get_contents(sprintf('%s.pdf', $this->document->tempFileName)))
);
} catch (Exception $e) {
throw new RuntimeException($e->getMessage());
}
}
}

View File

@ -3,7 +3,7 @@
declare(strict_types=1); declare(strict_types=1);
namespace App; namespace App\SignDocument\Services;
class ProdSignService class ProdSignService

View File

@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace App\SignDocument\Services;
use App\Infrastructure\External\Api\BinaryStringFileResult;
class RemoveExistingDocumentService
{
public function removeExistingDocument(BinaryStringFileResult $document): void
{
if (file_exists($document->tempFileName)) {
unlink($document->tempFileName);
}
}
}

View File

@ -2,27 +2,24 @@
declare(strict_types=1); declare(strict_types=1);
namespace App; namespace App\SignDocument\Services;
use App\Api\Api; use App\SignDocument\Api\Api;
use App\Api\ApiParams; use App\SignDocument\Api\ApiParams;
use App\Api\Request\SignRequest; use App\SignDocument\Api\Request\SignRequest;
use App\Infrastructure\External\Api\BinaryStringFileResult;
use Exception; use Exception;
use RuntimeException; use RuntimeException;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
class SignService class SignService
{ {
private DevSignService $devSignService;
private ProdSignService $prodSignService;
public function __construct( public function __construct(
private Api $api, private readonly Api $api,
private ApiParams $apiParams private readonly ApiParams $apiParams,
private readonly DevSignService $devSignService,
private readonly ProdSignService $prodSignService,
private readonly RemoveExistingDocumentService $removeExistingDocumentService,
){ ){
$this->devSignService = new DevSignService();
$this->prodSignService = new ProdSignService();
} }
public function signDocument(SignRequest $request,string $token): array public function signDocument(SignRequest $request,string $token): array
{ {
@ -39,7 +36,7 @@ class SignService
$response = $this->api->send($token, $document->tempFileName . '_sign.pdf', $request->batch); $response = $this->api->send($token, $document->tempFileName . '_sign.pdf', $request->batch);
$this->removeExistingDocument($document); $this->removeExistingDocumentService->removeExistingDocument($document);
return $response; return $response;
} catch (Exception $e) { } catch (Exception $e) {
@ -54,11 +51,4 @@ class SignService
'prod' => $this->prodSignService->sign($documentUrl), 'prod' => $this->prodSignService->sign($documentUrl),
}; };
} }
private function removeExistingDocument(BinaryStringFileResult $document): void
{
if (file_exists($document->tempFileName)) {
unlink($document->tempFileName);
}
}
} }

View File

@ -176,6 +176,18 @@
"symfony/ux-chartjs": { "symfony/ux-chartjs": {
"version": "v2.19.3" "version": "v2.19.3"
}, },
"symfony/ux-twig-component": {
"version": "2.22",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "2.13",
"ref": "67814b5f9794798b885cec9d3f48631424449a01"
},
"files": [
"config/packages/twig_component.yaml"
]
},
"symfony/validator": { "symfony/validator": {
"version": "6.2", "version": "6.2",
"recipe": { "recipe": {

View File

@ -5,14 +5,16 @@ server {
root /usr/src/signer/public; root /usr/src/signer/public;
location ~* \.php$ { location / {
try_files $uri $uri/ /index.php last; try_files $uri $uri/ /index.php?$args;
fastcgi_split_path_info (.+?\.php)(/.*)$; }
fastcgi_pass localhost:9000;
fastcgi_index index.php; location ~ \.php$ {
include fastcgi_params; include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; fastcgi_param SCRIPT_FILENAME $document_root/$fastcgi_script_name;
fastcgi_param PATH_INFO $fastcgi_path_info; fastcgi_pass signer:9000;
try_files $uri =404;
fastcgi_index index.php;
} }
location ~* .php/ { location ~* .php/ {