From 9221e089dd86afc75a8622f27917c0a4b509c012 Mon Sep 17 00:00:00 2001 From: Leonid Nikitin Date: Sun, 26 Nov 2023 22:42:22 +0600 Subject: [PATCH] Revived API POST /captcha/{captcha_uuid}. Receiving captcha information for validation. --- .../V1/Captcha/VerificationInformationDto.php | 31 ++++++++++ app/Enums/CaptchaLogType.php | 1 + .../Controllers/Api/V1/CaptchaController.php | 16 +++++ .../VerificationInformationRequest.php | 58 +++++++++++++++++++ .../Api/V1/CaptchaVerificationInformation.php | 30 ++++++++++ app/Models/CaptchaLog.php | 6 ++ app/Providers/RouteServiceProvider.php | 7 ++- app/Repositories/CaptchaLogRepository.php | 17 +++++- app/Repositories/CaptchaRepository.php | 6 ++ app/Repositories/CaptchaTokenRepository.php | 5 ++ .../CaptchaVerificationInformationResult.php | 23 ++++++++ app/Services/Api/V1/CaptchaService.php | 53 +++++++++++++++++ config/captcha.php | 2 + lang/en.json | 8 ++- lang/ru.json | 8 ++- public/swagger.json | 22 ++++++- routes/api-v1.php | 2 + 17 files changed, 290 insertions(+), 5 deletions(-) create mode 100644 app/Dto/Request/Api/V1/Captcha/VerificationInformationDto.php create mode 100644 app/Http/Requests/Api/V1/Captcha/VerificationInformationRequest.php create mode 100644 app/Http/Resources/Api/V1/CaptchaVerificationInformation.php create mode 100644 app/ServiceResults/Api/V1/CaptchaService/CaptchaVerificationInformationResult.php diff --git a/app/Dto/Request/Api/V1/Captcha/VerificationInformationDto.php b/app/Dto/Request/Api/V1/Captcha/VerificationInformationDto.php new file mode 100644 index 0000000..d39a1fc --- /dev/null +++ b/app/Dto/Request/Api/V1/Captcha/VerificationInformationDto.php @@ -0,0 +1,31 @@ +captchaToken; + } + + public function getUserAgent(): ?string + { + return $this->userAgent; + } + + public function getHttpUserData(): HttpUserData + { + return $this->httpUserData; + } +} diff --git a/app/Enums/CaptchaLogType.php b/app/Enums/CaptchaLogType.php index 6ccfefc..8d9147d 100644 --- a/app/Enums/CaptchaLogType.php +++ b/app/Enums/CaptchaLogType.php @@ -7,4 +7,5 @@ enum CaptchaLogType: int case Created = 1; case Error = 2; case Verified = 3; + case ReadVerified = 4; } diff --git a/app/Http/Controllers/Api/V1/CaptchaController.php b/app/Http/Controllers/Api/V1/CaptchaController.php index 070a06c..80f6bb1 100644 --- a/app/Http/Controllers/Api/V1/CaptchaController.php +++ b/app/Http/Controllers/Api/V1/CaptchaController.php @@ -4,7 +4,9 @@ namespace App\Http\Controllers\Api\V1; use App\Http\Requests\Api\V1\Captcha\CaptchaRequest; use App\Http\Requests\Api\V1\Captcha\CheckingRequest; +use App\Http\Requests\Api\V1\Captcha\VerificationInformationRequest; use App\Http\Resources\Api\V1\Captcha; +use App\Http\Resources\Api\V1\CaptchaVerificationInformation; use App\Http\Resources\Api\V1\CaptchaVerified; use App\Services\Api\V1\CaptchaService; use Illuminate\Http\JsonResponse; @@ -38,4 +40,18 @@ final class CaptchaController extends Controller return response()->json(new CaptchaVerified($result)); } + + public function verificationInformation(string $captchaUuid, VerificationInformationRequest $request): JsonResponse + { + $params = $request->getDto(); + $expiresMinutes = config('captcha.verification_data_view_limit_in_minutes'); + $maxInfoDisplayCount = config('captcha.max_info_display_count'); + $result = $this->captchaService->verificationInformation($captchaUuid, $params, $expiresMinutes, $maxInfoDisplayCount); + + if (!$result->isSuccess()) { + return response()->json($result->getData())->setStatusCode($result->getCode()); + } + + return response()->json(new CaptchaVerificationInformation($result)); + } } diff --git a/app/Http/Requests/Api/V1/Captcha/VerificationInformationRequest.php b/app/Http/Requests/Api/V1/Captcha/VerificationInformationRequest.php new file mode 100644 index 0000000..7733812 --- /dev/null +++ b/app/Http/Requests/Api/V1/Captcha/VerificationInformationRequest.php @@ -0,0 +1,58 @@ +hasHeader('private-token')) { + return false; + } + $captchaToken = $captchaTokenRepository->getCaptchaTokenByPrivateToken($this->header('private-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 [ + 'user_agent' => ['nullable', 'string'], + ]; + } + + public function getDto(): VerificationInformationDto + { + $httpUserData = new HttpUserData( + $this->getClientIp(), + $this->userAgent(), + $this->header('referer') + ); + + return new VerificationInformationDto( + captchaToken: $this->captchaToken, + httpUserData: $httpUserData, + userAgent: $this->input('user_agent', null), + ); + } +} diff --git a/app/Http/Resources/Api/V1/CaptchaVerificationInformation.php b/app/Http/Resources/Api/V1/CaptchaVerificationInformation.php new file mode 100644 index 0000000..a19d68a --- /dev/null +++ b/app/Http/Resources/Api/V1/CaptchaVerificationInformation.php @@ -0,0 +1,30 @@ + $this->resource->isStatus(), + 'message' => $this->resource->getMessage(), + ]; + } +} diff --git a/app/Models/CaptchaLog.php b/app/Models/CaptchaLog.php index a2f564b..80c0bdf 100644 --- a/app/Models/CaptchaLog.php +++ b/app/Models/CaptchaLog.php @@ -3,6 +3,7 @@ namespace App\Models; use App\Enums\CaptchaLogType; +use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; @@ -35,4 +36,9 @@ final class CaptchaLog extends Model protected $casts = [ 'type' => CaptchaLogType::class, ]; + + public function scopeLatest(Builder $query): Builder + { + return $query->orderBy('created_at', 'desc'); + } } diff --git a/app/Providers/RouteServiceProvider.php b/app/Providers/RouteServiceProvider.php index 284cff4..751b319 100644 --- a/app/Providers/RouteServiceProvider.php +++ b/app/Providers/RouteServiceProvider.php @@ -52,8 +52,13 @@ class RouteServiceProvider extends ServiceProvider }); RateLimiter::for('captcha-checking', function (Request $request) { return [ - Limit::perMinute(config('captcha.validate_max_count_errors', 5))->by($request->input('captcha_key', null)), + Limit::perMinute(config('captcha.validate_max_count_errors', 5))->by($request->getClientIp() . '-' . $request->input('captcha_key', 'key')), ]; }); + RateLimiter::for('captcha-verification-information', function (Request $request) { + return [ + Limit::perMinute(config('captcha.max_info_display_count', 5))->by($request->getClientIp() . '-' . $request->route('captcha_uuid', 'uuid')), + ]; + }); } } diff --git a/app/Repositories/CaptchaLogRepository.php b/app/Repositories/CaptchaLogRepository.php index 6be864d..a778315 100644 --- a/app/Repositories/CaptchaLogRepository.php +++ b/app/Repositories/CaptchaLogRepository.php @@ -5,6 +5,7 @@ namespace App\Repositories; use App\Enums\CaptchaLogType; use App\Models\CaptchaLog; use Illuminate\Database\Eloquent\Builder; +use Illuminate\Database\Eloquent\Collection; final class CaptchaLogRepository { @@ -12,9 +13,23 @@ final class CaptchaLogRepository { return CaptchaLog::query() ->when($captchaId, function (Builder $query, int $captchaId) { - $query->where('id', $captchaId); + $query->where('captcha_id', $captchaId); }) ->where('type', '=', $type) ->count(); } + + public function getCaptchaLogsByTypes(array $types, ?int $captchaId = null, ?int $limit = null): Collection + { + return CaptchaLog::query() + ->when($captchaId, function (Builder $query, int $captchaId) { + $query->where('captcha_id', $captchaId); + }) + ->when($limit, function (Builder $query, int $limit) { + $query->limit($limit); + }) + ->whereIn('type', $types) + ->latest() + ->get(); + } } diff --git a/app/Repositories/CaptchaRepository.php b/app/Repositories/CaptchaRepository.php index 18f6184..9a3d9a4 100644 --- a/app/Repositories/CaptchaRepository.php +++ b/app/Repositories/CaptchaRepository.php @@ -3,6 +3,7 @@ namespace App\Repositories; use App\Models\Captcha; +use App\Models\CaptchaToken; final class CaptchaRepository { @@ -10,4 +11,9 @@ final class CaptchaRepository { return Captcha::query()->where('id', $id)->first(); } + + public function getCaptchaByUuid(CaptchaToken $captchaToken, string $uuid): ?Captcha + { + return $captchaToken->captchas()->where('uuid', $uuid)->first(); + } } diff --git a/app/Repositories/CaptchaTokenRepository.php b/app/Repositories/CaptchaTokenRepository.php index 9328ee1..b86491a 100644 --- a/app/Repositories/CaptchaTokenRepository.php +++ b/app/Repositories/CaptchaTokenRepository.php @@ -41,4 +41,9 @@ final readonly class CaptchaTokenRepository { return CaptchaToken::query()->where('public_token', '=', $token)->first(); } + + public function getCaptchaTokenByPrivateToken(string $token): ?CaptchaToken + { + return CaptchaToken::query()->where('private_token', '=', $token)->first(); + } } diff --git a/app/ServiceResults/Api/V1/CaptchaService/CaptchaVerificationInformationResult.php b/app/ServiceResults/Api/V1/CaptchaService/CaptchaVerificationInformationResult.php new file mode 100644 index 0000000..a41312d --- /dev/null +++ b/app/ServiceResults/Api/V1/CaptchaService/CaptchaVerificationInformationResult.php @@ -0,0 +1,23 @@ +status; + } + + public function getMessage(): string + { + return $this->message; + } +} diff --git a/app/Services/Api/V1/CaptchaService.php b/app/Services/Api/V1/CaptchaService.php index a64172a..7331365 100644 --- a/app/Services/Api/V1/CaptchaService.php +++ b/app/Services/Api/V1/CaptchaService.php @@ -4,11 +4,13 @@ namespace App\Services\Api\V1; use App\Dto\Request\Api\V1\Captcha\CaptchaPublicToken; use App\Dto\Request\Api\V1\Captcha\CheckingDto; +use App\Dto\Request\Api\V1\Captcha\VerificationInformationDto; use App\Enums\CaptchaLogType; use App\Repositories\CaptchaLogRepository; use App\Repositories\CaptchaRepository; use App\Repositories\DataCaptchaRepository; use App\ServiceResults\Api\V1\CaptchaService\Captcha; +use App\ServiceResults\Api\V1\CaptchaService\CaptchaVerificationInformationResult; use App\ServiceResults\Api\V1\CaptchaService\CaptchaVerifiedResult; use App\ServiceResults\ServiceResultError; use App\Services\Captcha\CaptchaHandler; @@ -17,6 +19,7 @@ use App\Services\CaptchaLog\CaptchaLogHandler; use App\Services\Service; use Carbon\Carbon; use Illuminate\Support\Facades\DB; +use Illuminate\Support\Str; final class CaptchaService extends Service { @@ -90,4 +93,54 @@ final class CaptchaService extends Service return $result; } + + public function verificationInformation(string $captchaUuid, VerificationInformationDto $verificationInformationDto, int $expiresMinutes, int $maxInfoDisplayCount = 1): ServiceResultError | CaptchaVerificationInformationResult + { + try { + $captcha = $this->captchaRepository->getCaptchaByUuid($verificationInformationDto->getCaptchaToken(), $captchaUuid); + if (is_null($captcha)) { + return $this->errNotFound(__('Captcha not found')); + } + + $captchaLogs = $this->captchaLogRepository->getCaptchaLogsByTypes([CaptchaLogType::Verified, CaptchaLogType::ReadVerified], $captcha->id); + $this->captchaLogHandler->handleStore($captcha->id, CaptchaLogType::ReadVerified, $verificationInformationDto->getHttpUserData()); + + $captchaVerificationLog = $captchaLogs->firstWhere('type', CaptchaLogType::Verified); + if (is_null($captchaVerificationLog)) { + return new CaptchaVerificationInformationResult( + status: false, + message: __('Captcha does not pass verification'), + ); + } + + if (!is_null($verificationInformationDto->getUserAgent()) && Str::limit($verificationInformationDto->getUserAgent(), 255, '') !== $captchaVerificationLog->user_agent) { + return new CaptchaVerificationInformationResult( + status: false, + message: __('Captcha User Agent does not match'), + ); + } + + if (now()->diffInMinutes($captchaVerificationLog->created_at) > $expiresMinutes) { + return new CaptchaVerificationInformationResult( + status: false, + message: __('The time for captcha verification has passed'), + ); + } + + if ($captchaLogs->where('type', CaptchaLogType::ReadVerified)->count() >= $maxInfoDisplayCount) { + return new CaptchaVerificationInformationResult( + status: false, + message: __('Captcha verification status view count exceeded'), + ); + } + } catch (\Throwable $e) { + report($e); + return $this->errService('Captcha service error!'); + } + + return new CaptchaVerificationInformationResult( + status: true, + message: __('Captcha Verified'), + ); + } } diff --git a/config/captcha.php b/config/captcha.php index 4943a2f..c06af38 100644 --- a/config/captcha.php +++ b/config/captcha.php @@ -3,6 +3,8 @@ return [ 'waiting_for_captcha_verification_in_seconds' => env('WAITING_FOR_CAPTCHA_VERIFICATION_IN_SECONDS', 900), 'validate_max_count_errors' => env('CAPTCHA_VALIDATE_MAX_COUNT_ERRORS', 3), + 'max_info_display_count' => env('CAPTCHA_MAX_INFO_DISPLAY_COUNT', 1), + 'verification_data_view_limit_in_minutes' => env('CAPTCHA_VERIFICATION_DATA_VIEW_LIMIT_IN_MINUTES', 60), 'imageClass' => \App\Captcha\Images\Image::class, 'types' => [ 'string' => [ diff --git a/lang/en.json b/lang/en.json index 25d656a..cbc886a 100644 --- a/lang/en.json +++ b/lang/en.json @@ -65,5 +65,11 @@ "Failed to generate token.": "Failed to generate token.", "Captcha not found or verification period has expired. Please try to refresh the captcha.": "Captcha not found or verification period has expired. Please try to refresh the captcha.", "CAPTCHA validation failed. Please try again.": "CAPTCHA validation failed. Please try again.", - "You have exceeded the number of attempts. Please refresh the captcha.": "You have exceeded the number of attempts. Please refresh the captcha." + "You have exceeded the number of attempts. Please refresh the captcha.": "You have exceeded the number of attempts. Please refresh the captcha.", + "Captcha not found": "Captcha not found.", + "Captcha Verified": "Captcha Verified.", + "Captcha verification status view count exceeded": "Captcha verification status view count exceeded!", + "Captcha User Agent does not match": "The CAPTCHA verification state has been rejected because the User Agent does not match. Please check and ensure that you are using the same User Agent.", + "The time for captcha verification has passed": "The time for captcha verification has passed.", + "Captcha does not pass verification": "Captcha does not pass verification.", } diff --git a/lang/ru.json b/lang/ru.json index 6a9f7ad..9f4001c 100644 --- a/lang/ru.json +++ b/lang/ru.json @@ -65,5 +65,11 @@ "Failed to generate token.": "Не удалось сгенерировать токен.", "Captcha not found or verification period has expired. Please try to refresh the captcha.": "Капча не найдена или истёк срок проверки. По пробуйтн обновить капчу.", "CAPTCHA validation failed. Please try again.": "Не удалось пройти проверку CAPTCHA. Пожалуйста, попробуйте еще раз.", - "You have exceeded the number of attempts. Please refresh the captcha.": "Вы превысили количество попыток. Обновите каптчу." + "You have exceeded the number of attempts. Please refresh the captcha.": "Вы превысили количество попыток. Обновите каптчу.", + "Captcha not found": "Капча не найдена.", + "Captcha Verified": "Капча проверена.", + "Captcha verification status view count exceeded": "Количество просмотра статуса проверки капчи превышено!", + "Captcha User Agent does not match": "Отклонено состояние проверки CAPTCHA, так как User Agent не совпадает. Пожалуйста, проверьте и убедитесь, что вы используете тот же User Agent.", + "The time for captcha verification has passed": "Время верификации капчи прошло.", + "Captcha does not pass verification": "Капча не проходит проверку." } diff --git a/public/swagger.json b/public/swagger.json index 0b2aad4..0c6f504 100644 --- a/public/swagger.json +++ b/public/swagger.json @@ -101,7 +101,7 @@ } }, "/api/v1/captcha/{captcha_key}": { - "get": { + "post": { "tags": [ "captcha" ], @@ -128,6 +128,18 @@ } } ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/GetCaptchaKeyInfo" + } + } + } + } + }, "responses": { "200": { "description": "successful operation", @@ -234,6 +246,14 @@ "required": [ "status", "message" ] + }, + "GetCaptchaKeyInfo": { + "type": "object", + "properties": { + "user_ageng": { + "type": "string" + } + } } } } diff --git a/routes/api-v1.php b/routes/api-v1.php index fcbfb6b..1044c10 100644 --- a/routes/api-v1.php +++ b/routes/api-v1.php @@ -16,3 +16,5 @@ use Illuminate\Support\Facades\Route; Route::get('/captcha', [\App\Http\Controllers\Api\V1\CaptchaController::class, 'getCaptcha']); Route::middleware(['throttle:captcha-checking'])->post('/captcha', [\App\Http\Controllers\Api\V1\CaptchaController::class, 'checking']); +Route::middleware(['throttle:captcha-verification-information'])->post('/captcha/{captcha_uuid}', [\App\Http\Controllers\Api\V1\CaptchaController::class, 'verificationInformation']) + ->where(['captcha_uuid' => '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}']);