mirror of
https://github.com/pelican-dev/panel.git
synced 2025-05-20 12:14:45 +02:00
Replace reCAPTCHA with Turnstile (#589)
* add laravel turnstile * add config & settings for turnstile * publish view to center captcha * completely replace reCAPTCHA * update FailedCaptcha event * add back config for domain verification * don't set language so browser lang is used
This commit is contained in:
parent
cf57c28c40
commit
9d02aeb130
@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
36
app/Filament/Pages/Auth/Login.php
Normal file
36
app/Filament/Pages/Auth/Login.php
Normal file
@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Pages\Auth;
|
||||
|
||||
use Coderflex\FilamentTurnstile\Forms\Components\Turnstile;
|
||||
use Filament\Pages\Auth\Login as BaseLogin;
|
||||
|
||||
class Login extends BaseLogin
|
||||
{
|
||||
protected function getForms(): array
|
||||
{
|
||||
return [
|
||||
'form' => $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();
|
||||
}
|
||||
}
|
@ -8,6 +8,7 @@ use App\Traits\EnvironmentWriterTrait;
|
||||
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\Concerns\InteractsWithHeaderActions;
|
||||
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 @@ class Settings extends Page implements HasForms
|
||||
->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 @@ class Settings extends Page implements HasForms
|
||||
];
|
||||
}
|
||||
|
||||
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('<p>You can generate the keys on your <u><a href="https://developers.cloudflare.com/turnstile/get-started/#get-a-sitekey-and-secret-key" target="_blank">Cloudflare Dashboard</a></u>. A Cloudflare account is required.</p>')),
|
||||
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'))),
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -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 @@ readonly class VerifyReCaptcha
|
||||
|
||||
public function handle(Request $request, \Closure $next): mixed
|
||||
{
|
||||
if (!config('recaptcha.enabled')) {
|
||||
if (!config('turnstile.turnstile_enabled')) {
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
@ -26,40 +26,30 @@ readonly class VerifyReCaptcha
|
||||
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');
|
||||
}
|
||||
}
|
||||
|
@ -24,8 +24,8 @@ class AssetComposer
|
||||
'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'),
|
||||
|
@ -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 @@ class AdminPanelProvider extends PanelProvider
|
||||
->id('admin')
|
||||
->path('admin')
|
||||
->topNavigation(config('panel.filament.top-navigation', true))
|
||||
->login()
|
||||
->login(Login::class)
|
||||
->breadcrumbs(false)
|
||||
->homeUrl('/')
|
||||
->favicon(config('app.favicon', '/pelican.ico'))
|
||||
|
@ -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",
|
||||
|
155
composer.lock
generated
155
composer.lock
generated
@ -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"
|
||||
}
|
||||
}
|
@ -1,31 +0,0 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
/*
|
||||
* Enable or disable captchas
|
||||
*/
|
||||
'enabled' => 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,
|
||||
];
|
15
config/turnstile.php
Normal file
15
config/turnstile.php
Normal file
@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
'turnstile_enabled' => 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.',
|
||||
],
|
||||
];
|
@ -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",
|
||||
|
@ -19,7 +19,7 @@ export default ({ username, password, recaptchaData }: LoginData): Promise<Login
|
||||
http.post('/auth/login', {
|
||||
user: username,
|
||||
password,
|
||||
'g-recaptcha-response': recaptchaData,
|
||||
'cf-turnstile-response': recaptchaData,
|
||||
})
|
||||
)
|
||||
.then((response) => {
|
||||
|
@ -2,7 +2,7 @@ import http from '@/api/http';
|
||||
|
||||
export default (email: string, recaptchaData?: string): Promise<string> => {
|
||||
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);
|
||||
});
|
||||
|
@ -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<Reaptcha>(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<Values>) => {
|
||||
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 }) => (
|
||||
<LoginFormContainer title={t('forgot_password.title')} css={tw`w-full flex`}>
|
||||
<Field
|
||||
light
|
||||
@ -83,19 +77,20 @@ export default () => {
|
||||
name={'email'}
|
||||
type={'email'}
|
||||
/>
|
||||
<div css={tw`mt-6`}>
|
||||
<Button type={'submit'} size={'xlarge'} disabled={isSubmitting} isLoading={isSubmitting}>
|
||||
{t('forgot_password.button')}
|
||||
</Button>
|
||||
</div>
|
||||
{recaptchaEnabled && (
|
||||
<Reaptcha
|
||||
ref={ref}
|
||||
size={'invisible'}
|
||||
<Turnstile
|
||||
sitekey={siteKey || '_invalid_key'}
|
||||
onVerify={(response) => {
|
||||
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 () => {
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<div css={tw`mt-6`}>
|
||||
<Button type={'submit'} size={'xlarge'} disabled={isSubmitting} isLoading={isSubmitting}>
|
||||
{t('forgot_password.button')}
|
||||
</Button>
|
||||
</div>
|
||||
<div css={tw`mt-6 text-center`}>
|
||||
<Link
|
||||
to={'/auth/login'}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Link, RouteComponentProps } from 'react-router-dom';
|
||||
import login from '@/api/auth/login';
|
||||
import LoginFormContainer from '@/components/auth/LoginFormContainer';
|
||||
@ -9,7 +9,7 @@ import { useTranslation } from 'react-i18next';
|
||||
import Field from '@/components/elements/Field';
|
||||
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 {
|
||||
@ -20,10 +20,10 @@ interface Values {
|
||||
const LoginContainer = ({ history }: RouteComponentProps) => {
|
||||
const { t } = useTranslation(['auth', 'strings']);
|
||||
|
||||
const ref = useRef<Reaptcha>(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<Values>) => {
|
||||
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 }) => (
|
||||
<LoginFormContainer title={t('login.title')} css={tw`w-full flex`}>
|
||||
<Field
|
||||
light
|
||||
@ -94,19 +88,20 @@ const LoginContainer = ({ history }: RouteComponentProps) => {
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
</div>
|
||||
<div css={tw`mt-6`}>
|
||||
<Button type={'submit'} size={'xlarge'} isLoading={isSubmitting} disabled={isSubmitting}>
|
||||
{t('login.button')}
|
||||
</Button>
|
||||
</div>
|
||||
{recaptchaEnabled && (
|
||||
<Reaptcha
|
||||
ref={ref}
|
||||
size={'invisible'}
|
||||
<Turnstile
|
||||
sitekey={siteKey || '_invalid_key'}
|
||||
onVerify={(response) => {
|
||||
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) => {
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<div css={tw`mt-6`}>
|
||||
<Button type={'submit'} size={'xlarge'} isLoading={isSubmitting} disabled={isSubmitting}>
|
||||
{t('login.button')}
|
||||
</Button>
|
||||
</div>
|
||||
<div css={tw`mt-6 text-center`}>
|
||||
<Link
|
||||
to={'/auth/password'}
|
||||
|
56
resources/views/vendor/turnstile/components/turnstile.blade.php
vendored
Normal file
56
resources/views/vendor/turnstile/components/turnstile.blade.php
vendored
Normal file
@ -0,0 +1,56 @@
|
||||
@php
|
||||
$statePath = $getStatePath();
|
||||
$fieldWrapperView = $getFieldWrapperView();
|
||||
|
||||
$theme = $getTheme();
|
||||
$size = $getSize();
|
||||
$language = $getLanguage();
|
||||
@endphp
|
||||
|
||||
<x-dynamic-component class="flex justify-center" :component="$fieldWrapperView" :field="$turnstile">
|
||||
|
||||
<div x-data="{
|
||||
state: $wire.entangle('{{ $statePath }}').defer
|
||||
}"
|
||||
wire:ignore
|
||||
x-init="(() => {
|
||||
let options= {
|
||||
callback: function (token) {
|
||||
$wire.set('{{ $statePath }}', token)
|
||||
},
|
||||
|
||||
errorCallback: function () {
|
||||
$wire.set('{{ $statePath }}', null)
|
||||
},
|
||||
}
|
||||
|
||||
window.onloadTurnstileCallback = () => {
|
||||
turnstile.render($refs.turnstile, options)
|
||||
}
|
||||
|
||||
resetCaptcha = () => {
|
||||
turnstile.reset($refs.turnstile)
|
||||
}
|
||||
})()"
|
||||
>
|
||||
<div data-sitekey="{{config('turnstile.turnstile_site_key')}}"
|
||||
data-theme="{{ $theme }}"
|
||||
data-language="{{ $language }}"
|
||||
data-size="{{ $size }}"
|
||||
x-ref="turnstile"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js?onload=onloadTurnstileCallback" defer></script>
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
document.addEventListener('livewire:init', () => {
|
||||
Livewire.on('reset-captcha', (event) => {
|
||||
resetCaptcha()
|
||||
})
|
||||
})
|
||||
</script>
|
||||
@endpush
|
||||
</x-dynamic-component>
|
10
yarn.lock
10
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"
|
||||
|
Loading…
x
Reference in New Issue
Block a user