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.
|
* 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 Exception;
|
||||||
use Filament\Actions\Action;
|
use Filament\Actions\Action;
|
||||||
use Filament\Forms\Components\Actions\Action as FormAction;
|
use Filament\Forms\Components\Actions\Action as FormAction;
|
||||||
|
use Filament\Forms\Components\Placeholder;
|
||||||
use Filament\Forms\Components\Section;
|
use Filament\Forms\Components\Section;
|
||||||
use Filament\Forms\Components\Tabs;
|
use Filament\Forms\Components\Tabs;
|
||||||
use Filament\Forms\Components\Tabs\Tab;
|
use Filament\Forms\Components\Tabs\Tab;
|
||||||
@ -26,6 +27,7 @@ use Filament\Pages\Concerns\InteractsWithHeaderActions;
|
|||||||
use Filament\Pages\Page;
|
use Filament\Pages\Page;
|
||||||
use Illuminate\Support\Facades\Artisan;
|
use Illuminate\Support\Facades\Artisan;
|
||||||
use Illuminate\Support\Facades\Notification as MailNotification;
|
use Illuminate\Support\Facades\Notification as MailNotification;
|
||||||
|
use Illuminate\Support\HtmlString;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @property Form $form
|
* @property Form $form
|
||||||
@ -67,10 +69,11 @@ class Settings extends Page implements HasForms
|
|||||||
->label('General')
|
->label('General')
|
||||||
->icon('tabler-home')
|
->icon('tabler-home')
|
||||||
->schema($this->generalSettings()),
|
->schema($this->generalSettings()),
|
||||||
Tab::make('recaptcha')
|
Tab::make('captcha')
|
||||||
->label('reCAPTCHA')
|
->label('Captcha')
|
||||||
->icon('tabler-shield')
|
->icon('tabler-shield')
|
||||||
->schema($this->recaptchaSettings()),
|
->schema($this->captchaSettings())
|
||||||
|
->columns(3),
|
||||||
Tab::make('mail')
|
Tab::make('mail')
|
||||||
->label('Mail')
|
->label('Mail')
|
||||||
->icon('tabler-mail')
|
->icon('tabler-mail')
|
||||||
@ -180,35 +183,47 @@ class Settings extends Page implements HasForms
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
private function recaptchaSettings(): array
|
private function captchaSettings(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
Toggle::make('RECAPTCHA_ENABLED')
|
Toggle::make('TURNSTILE_ENABLED')
|
||||||
->label('Enable reCAPTCHA?')
|
->label('Enable Turnstile Captcha?')
|
||||||
->inline(false)
|
->inline(false)
|
||||||
|
->columnSpan(1)
|
||||||
->onIcon('tabler-check')
|
->onIcon('tabler-check')
|
||||||
->offIcon('tabler-x')
|
->offIcon('tabler-x')
|
||||||
->onColor('success')
|
->onColor('success')
|
||||||
->offColor('danger')
|
->offColor('danger')
|
||||||
->live()
|
->live()
|
||||||
->formatStateUsing(fn ($state): bool => (bool) $state)
|
->formatStateUsing(fn ($state): bool => (bool) $state)
|
||||||
->afterStateUpdated(fn ($state, Set $set) => $set('RECAPTCHA_ENABLED', (bool) $state))
|
->afterStateUpdated(fn ($state, Set $set) => $set('TURNSTILE_ENABLED', (bool) $state))
|
||||||
->default(env('RECAPTCHA_ENABLED', config('recaptcha.enabled'))),
|
->default(env('TURNSTILE_ENABLED', config('turnstile.turnstile_enabled'))),
|
||||||
TextInput::make('RECAPTCHA_DOMAIN')
|
Placeholder::make('info')
|
||||||
->label('Domain')
|
->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()
|
->required()
|
||||||
->visible(fn (Get $get) => $get('RECAPTCHA_ENABLED'))
|
->visible(fn (Get $get) => $get('TURNSTILE_ENABLED'))
|
||||||
->default(env('RECAPTCHA_DOMAIN', config('recaptcha.domain'))),
|
->default(env('TURNSTILE_SITE_KEY', config('turnstile.turnstile_site_key')))
|
||||||
TextInput::make('RECAPTCHA_WEBSITE_KEY')
|
->placeholder('1x00000000000000000000AA'),
|
||||||
->label('Website Key')
|
TextInput::make('TURNSTILE_SECRET_KEY')
|
||||||
->required()
|
|
||||||
->visible(fn (Get $get) => $get('RECAPTCHA_ENABLED'))
|
|
||||||
->default(env('RECAPTCHA_WEBSITE_KEY', config('recaptcha.website_key'))),
|
|
||||||
TextInput::make('RECAPTCHA_SECRET_KEY')
|
|
||||||
->label('Secret Key')
|
->label('Secret Key')
|
||||||
->required()
|
->required()
|
||||||
->visible(fn (Get $get) => $get('RECAPTCHA_ENABLED'))
|
->visible(fn (Get $get) => $get('TURNSTILE_ENABLED'))
|
||||||
->default(env('RECAPTCHA_SECRET_KEY', config('recaptcha.secret_key'))),
|
->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;
|
namespace App\Http\Middleware;
|
||||||
|
|
||||||
use GuzzleHttp\Client;
|
|
||||||
use Illuminate\Foundation\Application;
|
use Illuminate\Foundation\Application;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Http\Response;
|
use Illuminate\Http\Response;
|
||||||
use App\Events\Auth\FailedCaptcha;
|
use App\Events\Auth\FailedCaptcha;
|
||||||
|
use Coderflex\LaravelTurnstile\Facades\LaravelTurnstile;
|
||||||
use Symfony\Component\HttpKernel\Exception\HttpException;
|
use Symfony\Component\HttpKernel\Exception\HttpException;
|
||||||
|
|
||||||
readonly class VerifyReCaptcha
|
readonly class VerifyReCaptcha
|
||||||
@ -18,7 +18,7 @@ readonly class VerifyReCaptcha
|
|||||||
|
|
||||||
public function handle(Request $request, \Closure $next): mixed
|
public function handle(Request $request, \Closure $next): mixed
|
||||||
{
|
{
|
||||||
if (!config('recaptcha.enabled')) {
|
if (!config('turnstile.turnstile_enabled')) {
|
||||||
return $next($request);
|
return $next($request);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -26,40 +26,30 @@ readonly class VerifyReCaptcha
|
|||||||
return $next($request);
|
return $next($request);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($request->filled('g-recaptcha-response')) {
|
if ($request->filled('cf-turnstile-response')) {
|
||||||
$client = new Client();
|
$response = LaravelTurnstile::validate($request->get('cf-turnstile-response'));
|
||||||
$res = $client->post(config('recaptcha.domain'), [
|
|
||||||
'form_params' => [
|
|
||||||
'secret' => config('recaptcha.secret_key'),
|
|
||||||
'response' => $request->input('g-recaptcha-response'),
|
|
||||||
],
|
|
||||||
]);
|
|
||||||
|
|
||||||
if ($res->getStatusCode() === 200) {
|
if ($response['success'] && $this->isResponseVerified($response['hostname'] ?? '', $request)) {
|
||||||
$result = json_decode($res->getBody());
|
return $next($request);
|
||||||
|
|
||||||
if ($result->success && (!config('recaptcha.verify_domain') || $this->isResponseVerified($result, $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.
|
* 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')) {
|
if (!config('turnstile.turnstile_verify_domain')) {
|
||||||
return false;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
$url = parse_url($request->url());
|
$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'),
|
'name' => config('app.name', 'Panel'),
|
||||||
'locale' => config('app.locale') ?? 'en',
|
'locale' => config('app.locale') ?? 'en',
|
||||||
'recaptcha' => [
|
'recaptcha' => [
|
||||||
'enabled' => config('recaptcha.enabled', false),
|
'enabled' => config('turnstile.turnstile_enabled', false),
|
||||||
'siteKey' => config('recaptcha.website_key') ?? '',
|
'siteKey' => config('turnstile.turnstile_site_key') ?? '',
|
||||||
],
|
],
|
||||||
'usesSyncDriver' => config('queue.default') === 'sync',
|
'usesSyncDriver' => config('queue.default') === 'sync',
|
||||||
'serverDescriptionsEditable' => config('panel.editable_server_descriptions'),
|
'serverDescriptionsEditable' => config('panel.editable_server_descriptions'),
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Providers\Filament;
|
namespace App\Providers\Filament;
|
||||||
|
|
||||||
|
use App\Filament\Pages\Auth\Login;
|
||||||
use App\Filament\Resources\UserResource\Pages\EditProfile;
|
use App\Filament\Resources\UserResource\Pages\EditProfile;
|
||||||
use App\Http\Middleware\LanguageMiddleware;
|
use App\Http\Middleware\LanguageMiddleware;
|
||||||
use Filament\Http\Middleware\Authenticate;
|
use Filament\Http\Middleware\Authenticate;
|
||||||
@ -36,7 +37,7 @@ class AdminPanelProvider extends PanelProvider
|
|||||||
->id('admin')
|
->id('admin')
|
||||||
->path('admin')
|
->path('admin')
|
||||||
->topNavigation(config('panel.filament.top-navigation', true))
|
->topNavigation(config('panel.filament.top-navigation', true))
|
||||||
->login()
|
->login(Login::class)
|
||||||
->breadcrumbs(false)
|
->breadcrumbs(false)
|
||||||
->homeUrl('/')
|
->homeUrl('/')
|
||||||
->favicon(config('app.favicon', '/pelican.ico'))
|
->favicon(config('app.favicon', '/pelican.ico'))
|
||||||
|
@ -11,6 +11,7 @@
|
|||||||
"abdelhamiderrahmouni/filament-monaco-editor": "0.2.1",
|
"abdelhamiderrahmouni/filament-monaco-editor": "0.2.1",
|
||||||
"aws/aws-sdk-php": "~3.288.1",
|
"aws/aws-sdk-php": "~3.288.1",
|
||||||
"chillerlan/php-qrcode": "^5.0.2",
|
"chillerlan/php-qrcode": "^5.0.2",
|
||||||
|
"coderflex/filament-turnstile": "^2.2",
|
||||||
"dedoc/scramble": "^0.10.0",
|
"dedoc/scramble": "^0.10.0",
|
||||||
"doctrine/dbal": "~3.6.0",
|
"doctrine/dbal": "~3.6.0",
|
||||||
"filament/filament": "^3.2",
|
"filament/filament": "^3.2",
|
||||||
|
155
composer.lock
generated
155
composer.lock
generated
@ -743,6 +743,159 @@
|
|||||||
],
|
],
|
||||||
"time": "2024-03-02T20:07:15+00:00"
|
"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",
|
"name": "danharrin/date-format-converter",
|
||||||
"version": "v0.3.1",
|
"version": "v0.3.1",
|
||||||
@ -13483,4 +13636,4 @@
|
|||||||
},
|
},
|
||||||
"platform-dev": [],
|
"platform-dev": [],
|
||||||
"plugin-api-version": "2.6.0"
|
"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-i18next": "^11.2.1",
|
||||||
"react-router-dom": "^5.1.2",
|
"react-router-dom": "^5.1.2",
|
||||||
"react-transition-group": "^4.4.1",
|
"react-transition-group": "^4.4.1",
|
||||||
"reaptcha": "^1.7.2",
|
"react-turnstile": "^1.1.4",
|
||||||
"rimraf": "^4",
|
"rimraf": "^4",
|
||||||
"sockette": "^2.0.6",
|
"sockette": "^2.0.6",
|
||||||
"styled-components": "^5.2.1",
|
"styled-components": "^5.2.1",
|
||||||
|
@ -19,7 +19,7 @@ export default ({ username, password, recaptchaData }: LoginData): Promise<Login
|
|||||||
http.post('/auth/login', {
|
http.post('/auth/login', {
|
||||||
user: username,
|
user: username,
|
||||||
password,
|
password,
|
||||||
'g-recaptcha-response': recaptchaData,
|
'cf-turnstile-response': recaptchaData,
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
|
@ -2,7 +2,7 @@ import http from '@/api/http';
|
|||||||
|
|
||||||
export default (email: string, recaptchaData?: string): Promise<string> => {
|
export default (email: string, recaptchaData?: string): Promise<string> => {
|
||||||
return new Promise((resolve, reject) => {
|
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 || ''))
|
.then((response) => resolve(response.data.status || ''))
|
||||||
.catch(reject);
|
.catch(reject);
|
||||||
});
|
});
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import requestPasswordResetEmail from '@/api/auth/requestPasswordResetEmail';
|
import requestPasswordResetEmail from '@/api/auth/requestPasswordResetEmail';
|
||||||
import { httpErrorToHuman } from '@/api/http';
|
import { httpErrorToHuman } from '@/api/http';
|
||||||
@ -11,7 +11,7 @@ import { object, string } from 'yup';
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import tw from 'twin.macro';
|
import tw from 'twin.macro';
|
||||||
import Button from '@/components/elements/Button';
|
import Button from '@/components/elements/Button';
|
||||||
import Reaptcha from 'reaptcha';
|
import Turnstile, { useTurnstile } from 'react-turnstile';
|
||||||
import useFlash from '@/plugins/useFlash';
|
import useFlash from '@/plugins/useFlash';
|
||||||
|
|
||||||
interface Values {
|
interface Values {
|
||||||
@ -21,10 +21,10 @@ interface Values {
|
|||||||
export default () => {
|
export default () => {
|
||||||
const { t } = useTranslation('auth');
|
const { t } = useTranslation('auth');
|
||||||
|
|
||||||
const ref = useRef<Reaptcha>(null);
|
const turnstile = useTurnstile();
|
||||||
const [token, setToken] = useState('');
|
const [token, setToken] = useState('');
|
||||||
|
|
||||||
const { clearFlashes, addFlash } = useFlash();
|
const { clearFlashes, addFlash, addError } = useFlash();
|
||||||
const { enabled: recaptchaEnabled, siteKey } = useStoreState((state) => state.settings.data!.recaptcha);
|
const { enabled: recaptchaEnabled, siteKey } = useStoreState((state) => state.settings.data!.recaptcha);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -34,16 +34,10 @@ export default () => {
|
|||||||
const handleSubmission = ({ email }: Values, { setSubmitting, resetForm }: FormikHelpers<Values>) => {
|
const handleSubmission = ({ email }: Values, { setSubmitting, resetForm }: FormikHelpers<Values>) => {
|
||||||
clearFlashes();
|
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) {
|
if (recaptchaEnabled && !token) {
|
||||||
ref.current!.execute().catch((error) => {
|
addError({ message: 'No captcha token found.' });
|
||||||
console.error(error);
|
|
||||||
|
|
||||||
setSubmitting(false);
|
|
||||||
addFlash({ type: 'error', title: 'Error', message: httpErrorToHuman(error) });
|
|
||||||
});
|
|
||||||
|
|
||||||
|
setSubmitting(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -58,7 +52,7 @@ export default () => {
|
|||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
setToken('');
|
setToken('');
|
||||||
if (ref.current) ref.current.reset();
|
turnstile.reset();
|
||||||
|
|
||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
});
|
});
|
||||||
@ -74,7 +68,7 @@ export default () => {
|
|||||||
.required(t('forgot_password.required.email')),
|
.required(t('forgot_password.required.email')),
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{({ isSubmitting, setSubmitting, submitForm }) => (
|
{({ isSubmitting, setSubmitting }) => (
|
||||||
<LoginFormContainer title={t('forgot_password.title')} css={tw`w-full flex`}>
|
<LoginFormContainer title={t('forgot_password.title')} css={tw`w-full flex`}>
|
||||||
<Field
|
<Field
|
||||||
light
|
light
|
||||||
@ -83,19 +77,20 @@ export default () => {
|
|||||||
name={'email'}
|
name={'email'}
|
||||||
type={'email'}
|
type={'email'}
|
||||||
/>
|
/>
|
||||||
<div css={tw`mt-6`}>
|
|
||||||
<Button type={'submit'} size={'xlarge'} disabled={isSubmitting} isLoading={isSubmitting}>
|
|
||||||
{t('forgot_password.button')}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
{recaptchaEnabled && (
|
{recaptchaEnabled && (
|
||||||
<Reaptcha
|
<Turnstile
|
||||||
ref={ref}
|
|
||||||
size={'invisible'}
|
|
||||||
sitekey={siteKey || '_invalid_key'}
|
sitekey={siteKey || '_invalid_key'}
|
||||||
onVerify={(response) => {
|
className='mt-6 flex justify-center'
|
||||||
setToken(response);
|
retry='never'
|
||||||
submitForm();
|
onVerify={(token) => {
|
||||||
|
setToken(token);
|
||||||
|
}}
|
||||||
|
onError={(error) => {
|
||||||
|
console.error('Error verifying captcha: ' + error);
|
||||||
|
addError({ message: 'Error verifying captcha: ' + error });
|
||||||
|
|
||||||
|
setSubmitting(false);
|
||||||
|
setToken('');
|
||||||
}}
|
}}
|
||||||
onExpire={() => {
|
onExpire={() => {
|
||||||
setSubmitting(false);
|
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`}>
|
<div css={tw`mt-6 text-center`}>
|
||||||
<Link
|
<Link
|
||||||
to={'/auth/login'}
|
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 { Link, RouteComponentProps } from 'react-router-dom';
|
||||||
import login from '@/api/auth/login';
|
import login from '@/api/auth/login';
|
||||||
import LoginFormContainer from '@/components/auth/LoginFormContainer';
|
import LoginFormContainer from '@/components/auth/LoginFormContainer';
|
||||||
@ -9,7 +9,7 @@ import { useTranslation } from 'react-i18next';
|
|||||||
import Field from '@/components/elements/Field';
|
import Field from '@/components/elements/Field';
|
||||||
import tw from 'twin.macro';
|
import tw from 'twin.macro';
|
||||||
import Button from '@/components/elements/Button';
|
import Button from '@/components/elements/Button';
|
||||||
import Reaptcha from 'reaptcha';
|
import Turnstile, { useTurnstile } from 'react-turnstile';
|
||||||
import useFlash from '@/plugins/useFlash';
|
import useFlash from '@/plugins/useFlash';
|
||||||
|
|
||||||
interface Values {
|
interface Values {
|
||||||
@ -20,10 +20,10 @@ interface Values {
|
|||||||
const LoginContainer = ({ history }: RouteComponentProps) => {
|
const LoginContainer = ({ history }: RouteComponentProps) => {
|
||||||
const { t } = useTranslation(['auth', 'strings']);
|
const { t } = useTranslation(['auth', 'strings']);
|
||||||
|
|
||||||
const ref = useRef<Reaptcha>(null);
|
const turnstile = useTurnstile();
|
||||||
const [token, setToken] = useState('');
|
const [token, setToken] = useState('');
|
||||||
|
|
||||||
const { clearFlashes, clearAndAddHttpError } = useFlash();
|
const { clearFlashes, clearAndAddHttpError, addError } = useFlash();
|
||||||
const { enabled: recaptchaEnabled, siteKey } = useStoreState((state) => state.settings.data!.recaptcha);
|
const { enabled: recaptchaEnabled, siteKey } = useStoreState((state) => state.settings.data!.recaptcha);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -33,16 +33,10 @@ const LoginContainer = ({ history }: RouteComponentProps) => {
|
|||||||
const onSubmit = (values: Values, { setSubmitting }: FormikHelpers<Values>) => {
|
const onSubmit = (values: Values, { setSubmitting }: FormikHelpers<Values>) => {
|
||||||
clearFlashes();
|
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) {
|
if (recaptchaEnabled && !token) {
|
||||||
ref.current!.execute().catch((error) => {
|
addError({ message: 'No captcha token found.' });
|
||||||
console.error(error);
|
|
||||||
|
|
||||||
setSubmitting(false);
|
|
||||||
clearAndAddHttpError({ error });
|
|
||||||
});
|
|
||||||
|
|
||||||
|
setSubmitting(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -60,7 +54,7 @@ const LoginContainer = ({ history }: RouteComponentProps) => {
|
|||||||
console.error(error);
|
console.error(error);
|
||||||
|
|
||||||
setToken('');
|
setToken('');
|
||||||
if (ref.current) ref.current.reset();
|
turnstile.reset();
|
||||||
|
|
||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
clearAndAddHttpError({ error });
|
clearAndAddHttpError({ error });
|
||||||
@ -76,7 +70,7 @@ const LoginContainer = ({ history }: RouteComponentProps) => {
|
|||||||
password: string().required(t('login.required.password')),
|
password: string().required(t('login.required.password')),
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{({ isSubmitting, setSubmitting, submitForm }) => (
|
{({ isSubmitting, setSubmitting }) => (
|
||||||
<LoginFormContainer title={t('login.title')} css={tw`w-full flex`}>
|
<LoginFormContainer title={t('login.title')} css={tw`w-full flex`}>
|
||||||
<Field
|
<Field
|
||||||
light
|
light
|
||||||
@ -94,19 +88,20 @@ const LoginContainer = ({ history }: RouteComponentProps) => {
|
|||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div css={tw`mt-6`}>
|
|
||||||
<Button type={'submit'} size={'xlarge'} isLoading={isSubmitting} disabled={isSubmitting}>
|
|
||||||
{t('login.button')}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
{recaptchaEnabled && (
|
{recaptchaEnabled && (
|
||||||
<Reaptcha
|
<Turnstile
|
||||||
ref={ref}
|
|
||||||
size={'invisible'}
|
|
||||||
sitekey={siteKey || '_invalid_key'}
|
sitekey={siteKey || '_invalid_key'}
|
||||||
onVerify={(response) => {
|
className='mt-6 flex justify-center'
|
||||||
setToken(response);
|
retry='never'
|
||||||
submitForm();
|
onVerify={(token) => {
|
||||||
|
setToken(token);
|
||||||
|
}}
|
||||||
|
onError={(error) => {
|
||||||
|
console.error('Error verifying captcha: ' + error);
|
||||||
|
addError({ message: 'Error verifying captcha: ' + error });
|
||||||
|
|
||||||
|
setSubmitting(false);
|
||||||
|
setToken('');
|
||||||
}}
|
}}
|
||||||
onExpire={() => {
|
onExpire={() => {
|
||||||
setSubmitting(false);
|
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`}>
|
<div css={tw`mt-6 text-center`}>
|
||||||
<Link
|
<Link
|
||||||
to={'/auth/password'}
|
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"
|
loose-envify "^1.4.0"
|
||||||
prop-types "^15.6.2"
|
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:
|
react@^16.14.0:
|
||||||
version "16.14.0"
|
version "16.14.0"
|
||||||
resolved "https://registry.yarnpkg.com/react/-/react-16.14.0.tgz#94d776ddd0aaa37da3eda8fc5b6b18a4c9a3114d"
|
resolved "https://registry.yarnpkg.com/react/-/react-16.14.0.tgz#94d776ddd0aaa37da3eda8fc5b6b18a4c9a3114d"
|
||||||
@ -7794,11 +7799,6 @@ readdirp@~3.6.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
picomatch "^2.2.1"
|
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:
|
redent@^3.0.0:
|
||||||
version "3.0.0"
|
version "3.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/redent/-/redent-3.0.0.tgz#e557b7998316bb53c9f1f56fa626352c6963059f"
|
resolved "https://registry.yarnpkg.com/redent/-/redent-3.0.0.tgz#e557b7998316bb53c9f1f56fa626352c6963059f"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user