Revived API POST /captcha/{captcha_uuid}.

Receiving captcha information for validation.
This commit is contained in:
Leonid Nikitin 2023-11-26 22:42:22 +06:00
parent 10425db5e0
commit 9221e089dd
Signed by: kor-elf
GPG Key ID: 604A019EB118CAC4
17 changed files with 290 additions and 5 deletions

View File

@ -0,0 +1,31 @@
<?php declare(strict_types=1);
namespace App\Dto\Request\Api\V1\Captcha;
use App\Dto\HttpUserData;
use App\Dto\Request\Dto;
use App\Models\CaptchaToken;
final readonly class VerificationInformationDto extends Dto
{
public function __construct(
private CaptchaToken $captchaToken,
private HttpUserData $httpUserData,
private ?string $userAgent = null,
) { }
public function getCaptchaToken(): CaptchaToken
{
return $this->captchaToken;
}
public function getUserAgent(): ?string
{
return $this->userAgent;
}
public function getHttpUserData(): HttpUserData
{
return $this->httpUserData;
}
}

View File

@ -7,4 +7,5 @@ enum CaptchaLogType: int
case Created = 1;
case Error = 2;
case Verified = 3;
case ReadVerified = 4;
}

View File

@ -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));
}
}

View File

@ -0,0 +1,58 @@
<?php declare(strict_types=1);
namespace App\Http\Requests\Api\V1\Captcha;
use App\Contracts\FormRequestDto;
use App\Dto\HttpUserData;
use App\Dto\Request\Api\V1\Captcha\VerificationInformationDto;
use App\Models\CaptchaToken;
use App\Repositories\CaptchaTokenRepository;
use Illuminate\Foundation\Http\FormRequest;
final class VerificationInformationRequest extends FormRequest implements FormRequestDto
{
private readonly CaptchaToken $captchaToken;
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(CaptchaTokenRepository $captchaTokenRepository): bool
{
if (!$this->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),
);
}
}

View File

@ -0,0 +1,30 @@
<?php declare(strict_types=1);
namespace App\Http\Resources\Api\V1;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
final class CaptchaVerificationInformation extends JsonResource
{
/**
* @var \App\ServiceResults\Api\V1\CaptchaService\CaptchaVerificationInformationResult
*/
public $resource;
/**
* Transform the resource into an array.
*
* @param Request $request
* @return array
*/
public function toArray(Request $request): array
{
$info = [];
return [
'status' => $this->resource->isStatus(),
'message' => $this->resource->getMessage(),
];
}
}

View File

@ -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');
}
}

View File

@ -52,7 +52,12 @@ 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')),
];
});
}

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -0,0 +1,23 @@
<?php declare(strict_types=1);
namespace App\ServiceResults\Api\V1\CaptchaService;
use App\ServiceResults\ServiceResult;
final class CaptchaVerificationInformationResult extends ServiceResult
{
public function __construct(
private readonly bool $status,
private readonly string $message,
) { }
public function isStatus(): bool
{
return $this->status;
}
public function getMessage(): string
{
return $this->message;
}
}

View File

@ -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'),
);
}
}

View File

@ -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' => [

View File

@ -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.",
}

View File

@ -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": "Капча не проходит проверку."
}

View File

@ -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"
}
}
}
}
}

View File

@ -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}']);