diff --git a/app/Events/Auth/FailedCaptcha.php b/app/Events/Auth/FailedCaptcha.php index 0605629bda..6f508bd417 100644 --- a/app/Events/Auth/FailedCaptcha.php +++ b/app/Events/Auth/FailedCaptcha.php @@ -12,7 +12,7 @@ class FailedCaptcha extends Event /** * Create a new event instance. */ - public function __construct(public string $ip, public string $domain) + public function __construct(public string $ip, public ?string $message) { } } diff --git a/app/Filament/Pages/Auth/Login.php b/app/Filament/Pages/Auth/Login.php new file mode 100644 index 0000000000..a4f157a374 --- /dev/null +++ b/app/Filament/Pages/Auth/Login.php @@ -0,0 +1,36 @@ + $this->form( + $this->makeForm() + ->schema([ + $this->getEmailFormComponent(), + $this->getPasswordFormComponent(), + $this->getRememberFormComponent(), + Turnstile::make('captcha') + ->hidden(!config('turnstile.turnstile_enabled')) + ->validationMessages([ + 'required' => config('turnstile.error_messages.turnstile_check_message'), + ]), + ]) + ->statePath('data'), + ), + ]; + } + + protected function throwFailureValidationException(): never + { + $this->dispatch('reset-captcha'); + + parent::throwFailureValidationException(); + } +} diff --git a/app/Filament/Pages/Settings.php b/app/Filament/Pages/Settings.php index 7043fe9d7d..a5c3572791 100644 --- a/app/Filament/Pages/Settings.php +++ b/app/Filament/Pages/Settings.php @@ -8,6 +8,7 @@ use Exception; use Filament\Actions\Action; use Filament\Forms\Components\Actions\Action as FormAction; +use Filament\Forms\Components\Placeholder; use Filament\Forms\Components\Section; use Filament\Forms\Components\Tabs; use Filament\Forms\Components\Tabs\Tab; @@ -26,6 +27,7 @@ use Filament\Pages\Page; use Illuminate\Support\Facades\Artisan; use Illuminate\Support\Facades\Notification as MailNotification; +use Illuminate\Support\HtmlString; /** * @property Form $form @@ -67,10 +69,11 @@ protected function getFormSchema(): array ->label('General') ->icon('tabler-home') ->schema($this->generalSettings()), - Tab::make('recaptcha') - ->label('reCAPTCHA') + Tab::make('captcha') + ->label('Captcha') ->icon('tabler-shield') - ->schema($this->recaptchaSettings()), + ->schema($this->captchaSettings()) + ->columns(3), Tab::make('mail') ->label('Mail') ->icon('tabler-mail') @@ -180,35 +183,47 @@ private function generalSettings(): array ]; } - private function recaptchaSettings(): array + private function captchaSettings(): array { return [ - Toggle::make('RECAPTCHA_ENABLED') - ->label('Enable reCAPTCHA?') + Toggle::make('TURNSTILE_ENABLED') + ->label('Enable Turnstile Captcha?') ->inline(false) + ->columnSpan(1) ->onIcon('tabler-check') ->offIcon('tabler-x') ->onColor('success') ->offColor('danger') ->live() ->formatStateUsing(fn ($state): bool => (bool) $state) - ->afterStateUpdated(fn ($state, Set $set) => $set('RECAPTCHA_ENABLED', (bool) $state)) - ->default(env('RECAPTCHA_ENABLED', config('recaptcha.enabled'))), - TextInput::make('RECAPTCHA_DOMAIN') - ->label('Domain') + ->afterStateUpdated(fn ($state, Set $set) => $set('TURNSTILE_ENABLED', (bool) $state)) + ->default(env('TURNSTILE_ENABLED', config('turnstile.turnstile_enabled'))), + Placeholder::make('info') + ->columnSpan(2) + ->content(new HtmlString('

You can generate the keys on your Cloudflare Dashboard. A Cloudflare account is required.

')), + TextInput::make('TURNSTILE_SITE_KEY') + ->label('Site Key') ->required() - ->visible(fn (Get $get) => $get('RECAPTCHA_ENABLED')) - ->default(env('RECAPTCHA_DOMAIN', config('recaptcha.domain'))), - TextInput::make('RECAPTCHA_WEBSITE_KEY') - ->label('Website Key') - ->required() - ->visible(fn (Get $get) => $get('RECAPTCHA_ENABLED')) - ->default(env('RECAPTCHA_WEBSITE_KEY', config('recaptcha.website_key'))), - TextInput::make('RECAPTCHA_SECRET_KEY') + ->visible(fn (Get $get) => $get('TURNSTILE_ENABLED')) + ->default(env('TURNSTILE_SITE_KEY', config('turnstile.turnstile_site_key'))) + ->placeholder('1x00000000000000000000AA'), + TextInput::make('TURNSTILE_SECRET_KEY') ->label('Secret Key') ->required() - ->visible(fn (Get $get) => $get('RECAPTCHA_ENABLED')) - ->default(env('RECAPTCHA_SECRET_KEY', config('recaptcha.secret_key'))), + ->visible(fn (Get $get) => $get('TURNSTILE_ENABLED')) + ->default(env('TURNSTILE_SECRET_KEY', config('turnstile.secret_key'))) + ->placeholder('1x0000000000000000000000000000000AA'), + Toggle::make('TURNSTILE_VERIFY_DOMAIN') + ->label('Verify domain?') + ->inline(false) + ->onIcon('tabler-check') + ->offIcon('tabler-x') + ->onColor('success') + ->offColor('danger') + ->visible(fn (Get $get) => $get('TURNSTILE_ENABLED')) + ->formatStateUsing(fn ($state): bool => (bool) $state) + ->afterStateUpdated(fn ($state, Set $set) => $set('TURNSTILE_VERIFY_DOMAIN', (bool) $state)) + ->default(env('TURNSTILE_VERIFY_DOMAIN', config('turnstile.turnstile_verify_domain'))), ]; } diff --git a/app/Http/Middleware/VerifyReCaptcha.php b/app/Http/Middleware/VerifyReCaptcha.php index 4ba454ce4c..b58906e90e 100644 --- a/app/Http/Middleware/VerifyReCaptcha.php +++ b/app/Http/Middleware/VerifyReCaptcha.php @@ -2,11 +2,11 @@ namespace App\Http\Middleware; -use GuzzleHttp\Client; use Illuminate\Foundation\Application; use Illuminate\Http\Request; use Illuminate\Http\Response; use App\Events\Auth\FailedCaptcha; +use Coderflex\LaravelTurnstile\Facades\LaravelTurnstile; use Symfony\Component\HttpKernel\Exception\HttpException; readonly class VerifyReCaptcha @@ -18,7 +18,7 @@ public function __construct(private Application $app) public function handle(Request $request, \Closure $next): mixed { - if (!config('recaptcha.enabled')) { + if (!config('turnstile.turnstile_enabled')) { return $next($request); } @@ -26,40 +26,30 @@ public function handle(Request $request, \Closure $next): mixed return $next($request); } - if ($request->filled('g-recaptcha-response')) { - $client = new Client(); - $res = $client->post(config('recaptcha.domain'), [ - 'form_params' => [ - 'secret' => config('recaptcha.secret_key'), - 'response' => $request->input('g-recaptcha-response'), - ], - ]); + if ($request->filled('cf-turnstile-response')) { + $response = LaravelTurnstile::validate($request->get('cf-turnstile-response')); - if ($res->getStatusCode() === 200) { - $result = json_decode($res->getBody()); - - if ($result->success && (!config('recaptcha.verify_domain') || $this->isResponseVerified($result, $request))) { - return $next($request); - } + if ($response['success'] && $this->isResponseVerified($response['hostname'] ?? '', $request)) { + return $next($request); } } - event(new FailedCaptcha($request->ip(), $result->hostname ?? null)); + event(new FailedCaptcha($request->ip(), $response['message'] ?? null)); - throw new HttpException(Response::HTTP_BAD_REQUEST, 'Failed to validate reCAPTCHA data.'); + throw new HttpException(Response::HTTP_BAD_REQUEST, 'Failed to validate turnstile captcha data.'); } /** * Determine if the response from the recaptcha servers was valid. */ - private function isResponseVerified(\stdClass $result, Request $request): bool + private function isResponseVerified(string $hostname, Request $request): bool { - if (!config('recaptcha.verify_domain')) { - return false; + if (!config('turnstile.turnstile_verify_domain')) { + return true; } $url = parse_url($request->url()); - return $result->hostname === array_get($url, 'host'); + return $hostname === array_get($url, 'host'); } } diff --git a/app/Http/ViewComposers/AssetComposer.php b/app/Http/ViewComposers/AssetComposer.php index 6908f41395..58cc53218b 100644 --- a/app/Http/ViewComposers/AssetComposer.php +++ b/app/Http/ViewComposers/AssetComposer.php @@ -24,8 +24,8 @@ public function compose(View $view): void 'name' => config('app.name', 'Panel'), 'locale' => config('app.locale') ?? 'en', 'recaptcha' => [ - 'enabled' => config('recaptcha.enabled', false), - 'siteKey' => config('recaptcha.website_key') ?? '', + 'enabled' => config('turnstile.turnstile_enabled', false), + 'siteKey' => config('turnstile.turnstile_site_key') ?? '', ], 'usesSyncDriver' => config('queue.default') === 'sync', 'serverDescriptionsEditable' => config('panel.editable_server_descriptions'), diff --git a/app/Providers/Filament/AdminPanelProvider.php b/app/Providers/Filament/AdminPanelProvider.php index 3df1b101cb..829f893fbf 100644 --- a/app/Providers/Filament/AdminPanelProvider.php +++ b/app/Providers/Filament/AdminPanelProvider.php @@ -2,6 +2,7 @@ namespace App\Providers\Filament; +use App\Filament\Pages\Auth\Login; use App\Filament\Resources\UserResource\Pages\EditProfile; use App\Http\Middleware\LanguageMiddleware; use Filament\Http\Middleware\Authenticate; @@ -36,7 +37,7 @@ public function panel(Panel $panel): Panel ->id('admin') ->path('admin') ->topNavigation(config('panel.filament.top-navigation', true)) - ->login() + ->login(Login::class) ->breadcrumbs(false) ->homeUrl('/') ->favicon(config('app.favicon', '/pelican.ico')) diff --git a/composer.json b/composer.json index 6b44652ffc..ab67ddd236 100644 --- a/composer.json +++ b/composer.json @@ -11,6 +11,7 @@ "abdelhamiderrahmouni/filament-monaco-editor": "0.2.1", "aws/aws-sdk-php": "~3.288.1", "chillerlan/php-qrcode": "^5.0.2", + "coderflex/filament-turnstile": "^2.2", "dedoc/scramble": "^0.10.0", "doctrine/dbal": "~3.6.0", "filament/filament": "^3.2", diff --git a/composer.lock b/composer.lock index ca8c6f54ab..b629a15af8 100644 --- a/composer.lock +++ b/composer.lock @@ -743,6 +743,159 @@ ], "time": "2024-03-02T20:07:15+00:00" }, + { + "name": "coderflex/filament-turnstile", + "version": "v2.2.0", + "source": { + "type": "git", + "url": "https://github.com/coderflexx/filament-turnstile.git", + "reference": "85735c61d414f67f8e3edca40af5d986c6eba496" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/coderflexx/filament-turnstile/zipball/85735c61d414f67f8e3edca40af5d986c6eba496", + "reference": "85735c61d414f67f8e3edca40af5d986c6eba496", + "shasum": "" + }, + "require": { + "coderflex/laravel-turnstile": "^1.0|^2.0", + "illuminate/contracts": "^10.0|^11.0", + "php": "^8.1", + "spatie/laravel-package-tools": "^1.14.0" + }, + "require-dev": { + "filament/filament": "^3.0", + "larastan/larastan": "^2.0.1", + "laravel/pint": "^1.0", + "nunomaduro/collision": "^7.9|^8.1", + "orchestra/testbench": "^8.0|^9.0", + "pestphp/pest": "^2.0", + "pestphp/pest-plugin-arch": "^2.0", + "pestphp/pest-plugin-laravel": "^2.0", + "pestphp/pest-plugin-livewire": "^2.0", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan-deprecation-rules": "^1.0", + "phpstan/phpstan-phpunit": "^1.0" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Coderflex\\FilamentTurnstile\\FilamentTurnstileServiceProvider" + ], + "aliases": { + "FilamentTurnstile": "Coderflex\\FilamentTurnstile\\Facades\\FilamentTurnstile" + } + } + }, + "autoload": { + "psr-4": { + "Coderflex\\FilamentTurnstile\\": "src/", + "Coderflex\\FilamentTurnstile\\Database\\Factories\\": "database/factories/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Oussama", + "email": "oussama@coderflex.com", + "role": "Developer" + } + ], + "description": "Filament Plugin to help you implement Cloudflare Turnstile", + "homepage": "https://github.com/coderflex/filament-turnstile", + "keywords": [ + "cloudflare", + "coderflex", + "filament", + "filament-turnstile", + "laravel", + "laravel-turnstile", + "turnstile" + ], + "support": { + "issues": "https://github.com/coderflexx/filament-turnstile/issues", + "source": "https://github.com/coderflexx/filament-turnstile/tree/v2.2.0" + }, + "time": "2024-05-04T13:23:47+00:00" + }, + { + "name": "coderflex/laravel-turnstile", + "version": "v2.0.1", + "source": { + "type": "git", + "url": "https://github.com/coderflexx/laravel-turnstile.git", + "reference": "02d5604e32f9ea578b5a40bc92b97c8b726ca34b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/coderflexx/laravel-turnstile/zipball/02d5604e32f9ea578b5a40bc92b97c8b726ca34b", + "reference": "02d5604e32f9ea578b5a40bc92b97c8b726ca34b", + "shasum": "" + }, + "require": { + "guzzlehttp/guzzle": "^7.7", + "illuminate/contracts": "^10.0|^11.0", + "php": "^8.1|^8.2", + "spatie/laravel-package-tools": "^1.14.0" + }, + "require-dev": { + "laravel/pint": "^1.0", + "nunomaduro/collision": "^7.0|^8.0", + "nunomaduro/larastan": "^2.0.1", + "orchestra/testbench": "^8.0|^9.0", + "pestphp/pest": "^2.0", + "pestphp/pest-plugin-arch": "^2.0", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan-deprecation-rules": "^1.0", + "phpstan/phpstan-phpunit": "^1.0" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Coderflex\\LaravelTurnstile\\LaravelTurnstileServiceProvider" + ], + "aliases": { + "LaravelTurnstile": "Coderflex\\LaravelTurnstile\\Facades\\LaravelTurnstile" + } + } + }, + "autoload": { + "psr-4": { + "Coderflex\\LaravelTurnstile\\": "src/", + "Coderflex\\LaravelTurnstile\\Database\\Factories\\": "database/factories/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "ousid", + "email": "oussama@coderflex.com", + "role": "Developer" + } + ], + "description": "A package to help you implement the Cloudflare turnstile \"CAPTCHA Alternative\"", + "homepage": "https://github.com/coderflexx/laravel-turnstile", + "keywords": [ + "cloudflare", + "coderflex", + "laravel", + "laravel-turnstile", + "turnstile" + ], + "support": { + "issues": "https://github.com/coderflexx/laravel-turnstile/issues", + "source": "https://github.com/coderflexx/laravel-turnstile/tree/v2.0.1" + }, + "time": "2024-04-08T16:05:46+00:00" + }, { "name": "danharrin/date-format-converter", "version": "v0.3.1", @@ -13483,4 +13636,4 @@ }, "platform-dev": [], "plugin-api-version": "2.6.0" -} +} \ No newline at end of file diff --git a/config/recaptcha.php b/config/recaptcha.php deleted file mode 100644 index 757e184a5b..0000000000 --- a/config/recaptcha.php +++ /dev/null @@ -1,31 +0,0 @@ - env('RECAPTCHA_ENABLED', true), - - /* - * API endpoint for recaptcha checks. You should not edit this. - */ - 'domain' => env('RECAPTCHA_DOMAIN', 'https://www.google.com/recaptcha/api/siteverify'), - - /* - * Use a custom secret key, we use our public one by default - */ - 'secret_key' => env('RECAPTCHA_SECRET_KEY', '6LcJcjwUAAAAALOcDJqAEYKTDhwELCkzUkNDQ0J5'), - '_shipped_secret_key' => '6LcJcjwUAAAAALOcDJqAEYKTDhwELCkzUkNDQ0J5', - - /* - * Use a custom website key, we use our public one by default - */ - 'website_key' => env('RECAPTCHA_WEBSITE_KEY', '6LcJcjwUAAAAAO_Xqjrtj9wWufUpYRnK6BW8lnfn'), - '_shipped_website_key' => '6LcJcjwUAAAAAO_Xqjrtj9wWufUpYRnK6BW8lnfn', - - /* - * Domain verification is enabled by default and compares the domain used when solving the captcha - * as public keys can't have domain verification on google's side enabled (obviously). - */ - 'verify_domain' => true, -]; diff --git a/config/turnstile.php b/config/turnstile.php new file mode 100644 index 0000000000..2a2e6c8cbd --- /dev/null +++ b/config/turnstile.php @@ -0,0 +1,15 @@ + env('TURNSTILE_ENABLED', false), + + 'turnstile_site_key' => env('TURNSTILE_SITE_KEY', null), + 'turnstile_secret_key' => env('TURNSTILE_SECRET_KEY', null), + + 'turnstile_verify_domain' => env('TURNSTILE_VERIFY_DOMAIN', true), + + 'error_messages' => [ + 'turnstile_check_message' => 'Captcha failed! Please refresh and try again.', + ], +]; diff --git a/package.json b/package.json index 713b5cbafb..c08b6d02ed 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,7 @@ "react-i18next": "^11.2.1", "react-router-dom": "^5.1.2", "react-transition-group": "^4.4.1", - "reaptcha": "^1.7.2", + "react-turnstile": "^1.1.4", "rimraf": "^4", "sockette": "^2.0.6", "styled-components": "^5.2.1", diff --git a/resources/scripts/api/auth/login.ts b/resources/scripts/api/auth/login.ts index ff0de7ac62..d42f9b2764 100644 --- a/resources/scripts/api/auth/login.ts +++ b/resources/scripts/api/auth/login.ts @@ -19,7 +19,7 @@ export default ({ username, password, recaptchaData }: LoginData): Promise { diff --git a/resources/scripts/api/auth/requestPasswordResetEmail.ts b/resources/scripts/api/auth/requestPasswordResetEmail.ts index d68fa44472..cf3c3c49fd 100644 --- a/resources/scripts/api/auth/requestPasswordResetEmail.ts +++ b/resources/scripts/api/auth/requestPasswordResetEmail.ts @@ -2,7 +2,7 @@ import http from '@/api/http'; export default (email: string, recaptchaData?: string): Promise => { return new Promise((resolve, reject) => { - http.post('/auth/password', { email, 'g-recaptcha-response': recaptchaData }) + http.post('/auth/password', { email, 'cf-turnstile-response': recaptchaData }) .then((response) => resolve(response.data.status || '')) .catch(reject); }); diff --git a/resources/scripts/components/auth/ForgotPasswordContainer.tsx b/resources/scripts/components/auth/ForgotPasswordContainer.tsx index 3821332f82..ce3149d499 100644 --- a/resources/scripts/components/auth/ForgotPasswordContainer.tsx +++ b/resources/scripts/components/auth/ForgotPasswordContainer.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { useEffect, useRef, useState } from 'react'; +import { useEffect, useState } from 'react'; import { Link } from 'react-router-dom'; import requestPasswordResetEmail from '@/api/auth/requestPasswordResetEmail'; import { httpErrorToHuman } from '@/api/http'; @@ -11,7 +11,7 @@ import { object, string } from 'yup'; import { useTranslation } from 'react-i18next'; import tw from 'twin.macro'; import Button from '@/components/elements/Button'; -import Reaptcha from 'reaptcha'; +import Turnstile, { useTurnstile } from 'react-turnstile'; import useFlash from '@/plugins/useFlash'; interface Values { @@ -21,10 +21,10 @@ interface Values { export default () => { const { t } = useTranslation('auth'); - const ref = useRef(null); + const turnstile = useTurnstile(); const [token, setToken] = useState(''); - const { clearFlashes, addFlash } = useFlash(); + const { clearFlashes, addFlash, addError } = useFlash(); const { enabled: recaptchaEnabled, siteKey } = useStoreState((state) => state.settings.data!.recaptcha); useEffect(() => { @@ -34,16 +34,10 @@ export default () => { const handleSubmission = ({ email }: Values, { setSubmitting, resetForm }: FormikHelpers) => { clearFlashes(); - // If there is no token in the state yet, request the token and then abort this submit request - // since it will be re-submitted when the recaptcha data is returned by the component. if (recaptchaEnabled && !token) { - ref.current!.execute().catch((error) => { - console.error(error); - - setSubmitting(false); - addFlash({ type: 'error', title: 'Error', message: httpErrorToHuman(error) }); - }); + addError({ message: 'No captcha token found.' }); + setSubmitting(false); return; } @@ -58,7 +52,7 @@ export default () => { }) .then(() => { setToken(''); - if (ref.current) ref.current.reset(); + turnstile.reset(); setSubmitting(false); }); @@ -74,7 +68,7 @@ export default () => { .required(t('forgot_password.required.email')), })} > - {({ isSubmitting, setSubmitting, submitForm }) => ( + {({ isSubmitting, setSubmitting }) => ( { name={'email'} type={'email'} /> -
- -
{recaptchaEnabled && ( - { - setToken(response); - submitForm(); + className='mt-6 flex justify-center' + retry='never' + onVerify={(token) => { + setToken(token); + }} + onError={(error) => { + console.error('Error verifying captcha: ' + error); + addError({ message: 'Error verifying captcha: ' + error }); + + setSubmitting(false); + setToken(''); }} onExpire={() => { setSubmitting(false); @@ -103,6 +98,11 @@ export default () => { }} /> )} +
+ +
{ const { t } = useTranslation(['auth', 'strings']); - const ref = useRef(null); + const turnstile = useTurnstile(); const [token, setToken] = useState(''); - const { clearFlashes, clearAndAddHttpError } = useFlash(); + const { clearFlashes, clearAndAddHttpError, addError } = useFlash(); const { enabled: recaptchaEnabled, siteKey } = useStoreState((state) => state.settings.data!.recaptcha); useEffect(() => { @@ -33,16 +33,10 @@ const LoginContainer = ({ history }: RouteComponentProps) => { const onSubmit = (values: Values, { setSubmitting }: FormikHelpers) => { clearFlashes(); - // If there is no token in the state yet, request the token and then abort this submit request - // since it will be re-submitted when the recaptcha data is returned by the component. if (recaptchaEnabled && !token) { - ref.current!.execute().catch((error) => { - console.error(error); - - setSubmitting(false); - clearAndAddHttpError({ error }); - }); + addError({ message: 'No captcha token found.' }); + setSubmitting(false); return; } @@ -60,7 +54,7 @@ const LoginContainer = ({ history }: RouteComponentProps) => { console.error(error); setToken(''); - if (ref.current) ref.current.reset(); + turnstile.reset(); setSubmitting(false); clearAndAddHttpError({ error }); @@ -76,7 +70,7 @@ const LoginContainer = ({ history }: RouteComponentProps) => { password: string().required(t('login.required.password')), })} > - {({ isSubmitting, setSubmitting, submitForm }) => ( + {({ isSubmitting, setSubmitting }) => ( { disabled={isSubmitting} />
-
- -
{recaptchaEnabled && ( - { - setToken(response); - submitForm(); + className='mt-6 flex justify-center' + retry='never' + onVerify={(token) => { + setToken(token); + }} + onError={(error) => { + console.error('Error verifying captcha: ' + error); + addError({ message: 'Error verifying captcha: ' + error }); + + setSubmitting(false); + setToken(''); }} onExpire={() => { setSubmitting(false); @@ -114,6 +109,11 @@ const LoginContainer = ({ history }: RouteComponentProps) => { }} /> )} +
+ +
+ +
+
+
+
+ + + + @push('scripts') + + @endpush + \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index fca7a74157..8ab4e9065a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7730,6 +7730,11 @@ react-transition-group@^4.4.1: loose-envify "^1.4.0" prop-types "^15.6.2" +react-turnstile@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/react-turnstile/-/react-turnstile-1.1.4.tgz#0c23b2f4b55f83b929407ae9bfbd211fbe5df362" + integrity sha512-oluyRWADdsufCt5eMqacW4gfw8/csr6Tk+fmuaMx0PWMKP1SX1iCviLvD2D5w92eAzIYDHi/krUWGHhlfzxTpQ== + react@^16.14.0: version "16.14.0" resolved "https://registry.yarnpkg.com/react/-/react-16.14.0.tgz#94d776ddd0aaa37da3eda8fc5b6b18a4c9a3114d" @@ -7794,11 +7799,6 @@ readdirp@~3.6.0: dependencies: picomatch "^2.2.1" -reaptcha@^1.7.2: - version "1.7.2" - resolved "https://registry.yarnpkg.com/reaptcha/-/reaptcha-1.7.2.tgz#d829f54270c241f46501e92a5a7badeb1fcf372d" - integrity sha512-/RXiPeMd+fPUGByv+kAaQlCXCsSflZ9bKX5Fcwv9IYGS1oyT2nntL/8zn9IaiUFHL66T1jBtOABcb92g2+3w8w== - redent@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/redent/-/redent-3.0.0.tgz#e557b7998316bb53c9f1f56fa626352c6963059f"