From 27046e66742d33658e0fa3d929cc55af0378c8eb Mon Sep 17 00:00:00 2001 From: Leonid Nikitin Date: Tue, 19 Sep 2023 14:27:33 +0600 Subject: [PATCH] Revived API /api/v1/captcha. Now a new captcha is created to check for a bot. --- app/Dto/HttpUserData.php | 27 ++++++ .../Api/V1/Captcha/CaptchaPublicToken.php | 25 +++++ app/Enums/CaptchaLogType.php | 8 ++ .../DataCaptchaRepositoryException.php | 8 ++ .../Controllers/Api/V1/CaptchaController.php | 7 +- .../Api/V1/Captcha/CaptchaRequest.php | 57 ++++++++++++ app/Http/Resources/Api/V1/Captcha.php | 14 ++- app/Models/Captcha.php | 37 ++++++++ app/Models/CaptchaLog.php | 37 ++++++++ app/Models/CaptchaToken.php | 6 ++ app/Providers/AppServiceProvider.php | 6 +- app/Repositories/CaptchaTokenRepository.php | 5 + app/Repositories/DataCaptchaRepository.php | 31 +++++++ .../Api/V1/CaptchaGenerateService/Captcha.php | 25 +++++ .../Api/V1/CaptchaService/Captcha.php | 22 ++--- .../Api/V1/CaptchaGenerateService.php | 89 ++++++++++++++++++ app/Services/Api/V1/CaptchaService.php | 91 ++++--------------- app/Services/Captcha/CaptchaHandler.php | 28 ++++++ app/Services/CaptchaLog/CaptchaLogHandler.php | 22 +++++ config/captcha.php | 1 + .../2023_08_27_163058_create_captcha.php | 44 +++++++++ public/swagger.json | 36 ++++++-- 22 files changed, 525 insertions(+), 101 deletions(-) create mode 100644 app/Dto/HttpUserData.php create mode 100644 app/Dto/Request/Api/V1/Captcha/CaptchaPublicToken.php create mode 100644 app/Enums/CaptchaLogType.php create mode 100644 app/Exceptions/Repositories/DataCaptchaRepositoryException.php create mode 100644 app/Http/Requests/Api/V1/Captcha/CaptchaRequest.php create mode 100644 app/Models/Captcha.php create mode 100644 app/Models/CaptchaLog.php create mode 100644 app/Repositories/DataCaptchaRepository.php create mode 100644 app/ServiceResults/Api/V1/CaptchaGenerateService/Captcha.php create mode 100644 app/Services/Api/V1/CaptchaGenerateService.php create mode 100644 app/Services/Captcha/CaptchaHandler.php create mode 100644 app/Services/CaptchaLog/CaptchaLogHandler.php create mode 100644 database/migrations/2023_08_27_163058_create_captcha.php diff --git a/app/Dto/HttpUserData.php b/app/Dto/HttpUserData.php new file mode 100644 index 0000000..0e2b977 --- /dev/null +++ b/app/Dto/HttpUserData.php @@ -0,0 +1,27 @@ +clientIp; + } + + public function getUserAgent(): ?string + { + return $this->userAgent; + } + + public function getReferer(): ?string + { + return $this->referer; + } +} diff --git a/app/Dto/Request/Api/V1/Captcha/CaptchaPublicToken.php b/app/Dto/Request/Api/V1/Captcha/CaptchaPublicToken.php new file mode 100644 index 0000000..26175f0 --- /dev/null +++ b/app/Dto/Request/Api/V1/Captcha/CaptchaPublicToken.php @@ -0,0 +1,25 @@ +captchaToken; + } + + public function getHttpUserData(): HttpUserData + { + return $this->httpUserData; + } +} diff --git a/app/Enums/CaptchaLogType.php b/app/Enums/CaptchaLogType.php new file mode 100644 index 0000000..34f2dc2 --- /dev/null +++ b/app/Enums/CaptchaLogType.php @@ -0,0 +1,8 @@ +captchaService->generate(); + $captchaPublicToken = $request->getDto(); + $expires = now()->addSeconds(config('captcha.waiting_for_captcha_verification_in_seconds')); + $result = $this->captchaService->createKeyWithCaptcha($captchaPublicToken, $expires); if (!$result->isSuccess()) { return response()->json($result->getData())->setStatusCode($result->getCode()); } diff --git a/app/Http/Requests/Api/V1/Captcha/CaptchaRequest.php b/app/Http/Requests/Api/V1/Captcha/CaptchaRequest.php new file mode 100644 index 0000000..fa8c7ca --- /dev/null +++ b/app/Http/Requests/Api/V1/Captcha/CaptchaRequest.php @@ -0,0 +1,57 @@ +hasHeader('public-token')) { + return false; + } + $captchaToken = $captchaTokenRepository->getCaptchaTokenByPublicToken($this->header('public-token')); + if (is_null($captchaToken)) { + return false; + } + + $this->captchaToken = $captchaToken; + + return true; + } + + /** + * Get the validation rules that apply to the request. + */ + public function rules(): array + { + return [ + + ]; + } + + public function getDto(): CaptchaPublicToken + { + $httpUserData = new HttpUserData( + $this->getClientIp(), + $this->userAgent(), + $this->header('referer') + ); + + return new CaptchaPublicToken( + $this->captchaToken, + $httpUserData + ); + } +} diff --git a/app/Http/Resources/Api/V1/Captcha.php b/app/Http/Resources/Api/V1/Captcha.php index ef9cd4a..6bdf182 100644 --- a/app/Http/Resources/Api/V1/Captcha.php +++ b/app/Http/Resources/Api/V1/Captcha.php @@ -23,9 +23,17 @@ final class Captcha extends JsonResource public function toArray(Request $request): array { return [ - 'image_base64' => $this->resource->getImageBase64(), - 'image_text_base64' => $this->resource->getImageTextBase64(), - 'captcha_key' => $this->resource->getKey() + 'image_head' => [ + 'base64' => $this->resource->getImageHead()->getImageBase64(), + 'width' => $this->resource->getImageHead()->getWidth(), + 'height' => $this->resource->getImageHead()->getHeight(), + ], + 'image_body' => [ + 'base64' => $this->resource->getImageBody()->getImageBase64(), + 'width' => $this->resource->getImageBody()->getWidth(), + 'height' => $this->resource->getImageBody()->getHeight(), + ], + 'captcha_key' => $this->resource->getKey() ]; } } diff --git a/app/Models/Captcha.php b/app/Models/Captcha.php new file mode 100644 index 0000000..37ed7a9 --- /dev/null +++ b/app/Models/Captcha.php @@ -0,0 +1,37 @@ + + */ + protected $hidden = [ + 'uuid', + ]; + + public function captchaLogs(): HasMany + { + return $this->hasMany(CaptchaLog::class); + } +} diff --git a/app/Models/CaptchaLog.php b/app/Models/CaptchaLog.php new file mode 100644 index 0000000..1689454 --- /dev/null +++ b/app/Models/CaptchaLog.php @@ -0,0 +1,37 @@ + CaptchaLogType::class, + ]; +} diff --git a/app/Models/CaptchaToken.php b/app/Models/CaptchaToken.php index 71544be..b837307 100644 --- a/app/Models/CaptchaToken.php +++ b/app/Models/CaptchaToken.php @@ -4,6 +4,7 @@ namespace App\Models; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\SoftDeletes; final class CaptchaToken extends Model @@ -30,4 +31,9 @@ final class CaptchaToken extends Model 'public_token', 'private_token', ]; + + public function captchas(): HasMany + { + return $this->hasMany(Captcha::class); + } } diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index f9a56fd..7900e94 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -10,7 +10,7 @@ use App\Captcha\Images\Body; use App\Captcha\Images\Head; use App\Captcha\Images\ImageManager; use App\Captcha\Images\Lines; -use App\Services\Api\V1\CaptchaService; +use App\Services\Api\V1\CaptchaGenerateService; use App\Services\CaptchaToken\CaptchaTokenHandler; use App\Services\GenerateTokenCommand\GenerateTokenUlidCommand; use App\Services\GenerateTokenCommand\GenerateTokenUuidCommand; @@ -39,8 +39,8 @@ final class AppServiceProvider extends ServiceProvider return new CreateSearchInstanceCommand(Search::class); }); - $this->app->bind(CaptchaService::class, function (Application $app) { - return new CaptchaService( + $this->app->bind(CaptchaGenerateService::class, function (Application $app) { + return new CaptchaGenerateService( config: config('captcha', []), imageHead: $app->make(ImageHead::class), imageBody: $app->make(ImageBody::class) diff --git a/app/Repositories/CaptchaTokenRepository.php b/app/Repositories/CaptchaTokenRepository.php index d09f7ce..9328ee1 100644 --- a/app/Repositories/CaptchaTokenRepository.php +++ b/app/Repositories/CaptchaTokenRepository.php @@ -36,4 +36,9 @@ final readonly class CaptchaTokenRepository return $this->createSearchInstanceCommand->execute($query); } + + public function getCaptchaTokenByPublicToken(string $token): ?CaptchaToken + { + return CaptchaToken::query()->where('public_token', '=', $token)->first(); + } } diff --git a/app/Repositories/DataCaptchaRepository.php b/app/Repositories/DataCaptchaRepository.php new file mode 100644 index 0000000..464eaf8 --- /dev/null +++ b/app/Repositories/DataCaptchaRepository.php @@ -0,0 +1,31 @@ +generateTokenUlidCommand->execute() . '_' . time(); + $data = [ + 'id' => $captcha->id, + 'coordinators' => $coordinators + ]; + + if (!Cache::driver('redis')->add($key, $data, $expires) ) { + throw new DataCaptchaRepositoryException('Could not create cache for captcha: ' . $captcha->id . ' key: ' . $key); + } + + return $key; + } +} diff --git a/app/ServiceResults/Api/V1/CaptchaGenerateService/Captcha.php b/app/ServiceResults/Api/V1/CaptchaGenerateService/Captcha.php new file mode 100644 index 0000000..53ccc8b --- /dev/null +++ b/app/ServiceResults/Api/V1/CaptchaGenerateService/Captcha.php @@ -0,0 +1,25 @@ +imageHead; + } + + public function getImageBody(): ImageBody + { + return $this->imageBody; + } +} diff --git a/app/ServiceResults/Api/V1/CaptchaService/Captcha.php b/app/ServiceResults/Api/V1/CaptchaService/Captcha.php index 79edf7c..fba355d 100644 --- a/app/ServiceResults/Api/V1/CaptchaService/Captcha.php +++ b/app/ServiceResults/Api/V1/CaptchaService/Captcha.php @@ -2,35 +2,27 @@ namespace App\ServiceResults\Api\V1\CaptchaService; +use App\Captcha\Dto\Image; use App\ServiceResults\ServiceResult; final class Captcha extends ServiceResult { public function __construct( - private readonly string $imageBase64, - private readonly string $imageTextBase64, + private readonly Image $imageHead, + private readonly Image $imageBody, private readonly string $key ) { } - /** - * @return string - */ - public function getImageBase64(): string + public function getImageHead(): Image { - return $this->imageBase64; + return $this->imageHead; } - /** - * @return string - */ - public function getImageTextBase64(): string + public function getImageBody(): Image { - return $this->imageTextBase64; + return $this->imageBody; } - /** - * @return string - */ public function getKey(): string { return $this->key; diff --git a/app/Services/Api/V1/CaptchaGenerateService.php b/app/Services/Api/V1/CaptchaGenerateService.php new file mode 100644 index 0000000..e2fa96f --- /dev/null +++ b/app/Services/Api/V1/CaptchaGenerateService.php @@ -0,0 +1,89 @@ +config['types'] ?? []; + if (empty($types)) { + $error = __('No captcha type settings!'); + report($error); + return $this->errService($error); + } + + try { + $type = Arr::random($types); + + /** @var Type $captcha */ + $typeCaptcha = new $type['class']( + $type['params'] ?? [] + ); + $symbols = $typeCaptcha->getSymbols(); + + $imageHeadConfig = $this->makeImageHeadConfig($this->config['image_head'] ?? []); + $imageHead = $this->imageHead->processing($symbols, $imageHeadConfig); + unset($imageHeadConfig); + + $imageBodyConfig = $this->makeImageBodyConfig($this->config['image_body'] ?? []); + $imageBody = $this->imageBody->processing($symbols, $imageBodyConfig); + unset($imageBodyConfig); + } catch (\Throwable $e) { + report($e); + return $this->errService('Captcha service error!'); + } + + return new Captcha( + imageHead: $imageHead, + imageBody: $imageBody + ); + } + + private function makeImageHeadConfig(array $params): ImageHeadConfig + { + return new ImageHeadConfig( + backgrounds: $params['backgrounds'] ?? [], + fonts: $params['fonts'] ?? [], + fontColors: $params['font_colors'] ?? [], + width: $params['width'] ?? 150, + height: $params['height'] ?? 40, + textPaddingTop: $params['text_padding_top'] ?? 5, + textPaddingLeft: $params['text_padding_left'] ?? 10, + angle: $params['angle'] ?? 20, + numberLines: $params['number_lines'] ?? 3, + lineColors: $params['line_colors'] ?? [] + ); + } + + private function makeImageBodyConfig(array $params): ImageBodyConfig + { + return new ImageBodyConfig( + backgrounds: $params['backgrounds'] ?? [], + fonts: $params['fonts'] ?? [], + fontColors: $params['font_colors'] ?? [], + width: $params['width'] ?? 300, + height: $params['height'] ?? 240, + angle: $params['angle'] ?? 20, + fontSize: $params['font_size'] ?? [20, 50], + numberLines: $params['number_lines'] ?? 3, + lineColors: $params['line_colors'] ?? [] + ); + } +} diff --git a/app/Services/Api/V1/CaptchaService.php b/app/Services/Api/V1/CaptchaService.php index 33553d2..008e41a 100644 --- a/app/Services/Api/V1/CaptchaService.php +++ b/app/Services/Api/V1/CaptchaService.php @@ -2,96 +2,45 @@ namespace App\Services\Api\V1; -use App\Captcha\Contracts\ImageBody; -use App\Captcha\Contracts\ImageHead; -use App\Captcha\Contracts\Type; +use App\Dto\Request\Api\V1\Captcha\CaptchaPublicToken; +use App\Repositories\DataCaptchaRepository; use App\ServiceResults\Api\V1\CaptchaService\Captcha; use App\ServiceResults\ServiceResultError; +use App\Services\Captcha\CaptchaHandler; use App\Services\Service; -use Illuminate\Support\Arr; -use App\Captcha\Config\ImageHead as ImageHeadConfig; -use App\Captcha\Config\ImageBody as ImageBodyConfig; +use Carbon\Carbon; +use Illuminate\Support\Facades\DB; final class CaptchaService extends Service { public function __construct( - private readonly array $config, - private readonly ImageHead $imageHead, - private readonly ImageBody $imageBody + private readonly CaptchaGenerateService $captchaGenerateService, + private readonly CaptchaHandler $captchaHandler, + private readonly DataCaptchaRepository $dataCaptchaRepository, ) { } - public function generate(): ServiceResultError | Captcha + public function createKeyWithCaptcha(CaptchaPublicToken $captchaPublicToken, Carbon $expires): ServiceResultError | Captcha { - $types = $this->config['types'] ?? []; - if (empty($types)) { - $error = __('No captcha type settings!'); - report($error); - return $this->errService($error); - } - try { - $type = Arr::random($types); + $captcha = $this->captchaGenerateService->generate(); + if ($captcha->isError()) { + return $captcha; + } - /** @var Type $captcha */ - $typeCaptcha = new $type['class']( - $type['params'] ?? [] - ); - $symbols = $typeCaptcha->getSymbols(); + $modelCaptcha = DB::transaction(function () use ($captchaPublicToken) { + return $this->captchaHandler->handleStore($captchaPublicToken->getCaptchaToken(), $captchaPublicToken->getHttpUserData()); + }); - $imageHeadConfig = $this->makeImageHeadConfig($this->config['image_head'] ?? []); - $imageHead = $this->imageHead->processing($symbols, $imageHeadConfig); - unset($imageHeadConfig); - - $imageBodyConfig = $this->makeImageBodyConfig($this->config['image_body'] ?? []); - $imageBody = $this->imageBody->processing($symbols, $imageBodyConfig); - unset($imageBodyConfig); - - - dd($imageHead, $imageBody); + $captchaKey = $this->dataCaptchaRepository->store($modelCaptcha, $captcha->getImageBody()->getCoordinators(), $expires); } catch (\Throwable $e) { report($e); return $this->errService('Captcha service error!'); } - - - - return new Captcha( - imageBase64: $imageHead->getImage()->getImageBase64(), - imageTextBase64: $imageBody->getImage()->getImageBase64(), - key: 'dddd' - ); - } - - private function makeImageHeadConfig(array $params): ImageHeadConfig - { - return new ImageHeadConfig( - backgrounds: $params['backgrounds'] ?? [], - fonts: $params['fonts'] ?? [], - fontColors: $params['font_colors'] ?? [], - width: $params['width'] ?? 150, - height: $params['height'] ?? 40, - textPaddingTop: $params['text_padding_top'] ?? 5, - textPaddingLeft: $params['text_padding_left'] ?? 10, - angle: $params['angle'] ?? 20, - numberLines: $params['number_lines'] ?? 3, - lineColors: $params['line_colors'] ?? [] - ); - } - - private function makeImageBodyConfig(array $params): ImageBodyConfig - { - return new ImageBodyConfig( - backgrounds: $params['backgrounds'] ?? [], - fonts: $params['fonts'] ?? [], - fontColors: $params['font_colors'] ?? [], - width: $params['width'] ?? 300, - height: $params['height'] ?? 240, - angle: $params['angle'] ?? 20, - fontSize: $params['font_size'] ?? [20, 50], - numberLines: $params['number_lines'] ?? 3, - lineColors: $params['line_colors'] ?? [] + imageHead: $captcha->getImageHead()->getImage(), + imageBody: $captcha->getImageBody()->getImage(), + key: $captchaKey ); } } diff --git a/app/Services/Captcha/CaptchaHandler.php b/app/Services/Captcha/CaptchaHandler.php new file mode 100644 index 0000000..12fd273 --- /dev/null +++ b/app/Services/Captcha/CaptchaHandler.php @@ -0,0 +1,28 @@ +captchas()->create([ + 'uuid' => $this->uuidCommand->unique($captchaToken->captchas()->getQuery(), 'uuid') + ]); + $this->captchaLogHandler->handleStore($captcha, CaptchaLogType::Created, $httpUserData); + + return $captcha; + } +} diff --git a/app/Services/CaptchaLog/CaptchaLogHandler.php b/app/Services/CaptchaLog/CaptchaLogHandler.php new file mode 100644 index 0000000..c0cc2cf --- /dev/null +++ b/app/Services/CaptchaLog/CaptchaLogHandler.php @@ -0,0 +1,22 @@ +captchaLogs()->create([ + 'type' => $captchaLogType, + 'ip' => $httpUserData->getClientIp(), + 'user_agent' => Str::limit($httpUserData->getUserAgent(), 255, ''), + 'referer' => Str::limit($httpUserData->getReferer(), 10000, ''), + ]); + } +} diff --git a/config/captcha.php b/config/captcha.php index 8c45fa3..162db3d 100644 --- a/config/captcha.php +++ b/config/captcha.php @@ -1,6 +1,7 @@ env('WAITING_FOR_CAPTCHA_VERIFICATION_IN_SECONDS', 900), 'imageClass' => \App\Captcha\Images\Image::class, 'types' => [ 'string' => [ diff --git a/database/migrations/2023_08_27_163058_create_captcha.php b/database/migrations/2023_08_27_163058_create_captcha.php new file mode 100644 index 0000000..ed62bc4 --- /dev/null +++ b/database/migrations/2023_08_27_163058_create_captcha.php @@ -0,0 +1,44 @@ +id(); + $table->uuid('uuid'); + $table->unsignedBigInteger('captcha_token_id')->index(); + $table->foreign('captcha_token_id')->references('id')->on('captcha_tokens'); + $table->timestamps(); + $table->index('created_at'); + $table->unique(['uuid', 'captcha_token_id']); + }); + Schema::create('captcha_logs', function (Blueprint $table) { + $table->id(); + $table->unsignedBigInteger('captcha_id')->index(); + $table->foreign('captcha_id')->references('id')->on('captchas'); + $table->unsignedInteger('type')->index(); + $table->ipAddress('ip')->nullable(); + $table->string('user_agent')->nullable(); + $table->text('referer')->nullable(); + $table->timestamp('created_at')->index(); + $table->index(['captcha_id', 'type']); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('captcha_logs'); + Schema::dropIfExists('captchas'); + } +}; diff --git a/public/swagger.json b/public/swagger.json index 760f09c..6d41a88 100644 --- a/public/swagger.json +++ b/public/swagger.json @@ -157,20 +157,42 @@ "Captcha": { "type": "object", "properties": { - "image_base64": { - "type": "string", - "format": "byte" + "image_head": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Image", + } }, - "image_text_base64": { - "type": "string", - "format": "byte" + "image_body": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Image" + } }, "captcha_key": { "type": "string" } }, "required": [ - "image_base64", "image_text_base64", "captcha_key" + "image_head", "image_body", "captcha_key" + ] + }, + "Image": { + "type": "object", + "properties": { + "base64": { + "type": "string", + "format": "byte" + }, + "width": { + "type": "integer" + }, + "height": { + "type": "integer" + } + }, + "required": [ + "base64", "width", "height" ] }, "CaptchaVerification": {