mirror of
https://github.com/pelican-dev/panel.git
synced 2025-06-05 14:19:00 +02:00
Captcha
This commit is contained in:
parent
09e3506efa
commit
4a89e75a09
43
app/Extensions/Captcha/CaptchaProvider.php
Normal file
43
app/Extensions/Captcha/CaptchaProvider.php
Normal file
@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace App\Extensions\Captcha;
|
||||
|
||||
use App\Extensions\Captcha\Schemas\CaptchaSchemaInterface;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class CaptchaProvider
|
||||
{
|
||||
/** @var array<string, CaptchaSchemaInterface> */
|
||||
private array $schemas = [];
|
||||
|
||||
/**
|
||||
* @return array<string, CaptchaSchemaInterface> | CaptchaSchemaInterface
|
||||
*/
|
||||
public function get(?string $id = null): array|CaptchaSchemaInterface
|
||||
{
|
||||
return $id ? $this->schemas[$id] : $this->schemas;
|
||||
}
|
||||
|
||||
public function register(CaptchaSchemaInterface $schema): void
|
||||
{
|
||||
if (array_key_exists($schema->getId(), $this->schemas)) {
|
||||
return;
|
||||
}
|
||||
|
||||
config()->set('captcha.' . Str::lower($schema->getId()), $schema->getConfig());
|
||||
$this->schemas[$schema->getId()] = $schema;
|
||||
}
|
||||
|
||||
/** @return Collection<CaptchaSchemaInterface> */
|
||||
public function getActiveSchemas(): Collection
|
||||
{
|
||||
return collect($this->schemas)
|
||||
->filter(fn (CaptchaSchemaInterface $schema) => $schema->isEnabled());
|
||||
}
|
||||
|
||||
public function getActiveSchema(): ?CaptchaSchemaInterface
|
||||
{
|
||||
return $this->getActiveSchemas()->first();
|
||||
}
|
||||
}
|
35
app/Extensions/Captcha/Schemas/CaptchaSchemaInterface.php
Normal file
35
app/Extensions/Captcha/Schemas/CaptchaSchemaInterface.php
Normal file
@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace App\Extensions\Captcha\Schemas;
|
||||
|
||||
use Filament\Forms\Components\Component;
|
||||
|
||||
interface CaptchaSchemaInterface
|
||||
{
|
||||
public function getId(): string;
|
||||
|
||||
public function getName(): string;
|
||||
|
||||
/**
|
||||
* @return array<string, string|string[]|bool|null>
|
||||
*/
|
||||
public function getConfig(): array;
|
||||
|
||||
public function isEnabled(): bool;
|
||||
|
||||
public function getFormComponent(): Component;
|
||||
|
||||
/**
|
||||
* @return Component[]
|
||||
*/
|
||||
public function getSettingsForm(): array;
|
||||
|
||||
public function getIcon(): ?string;
|
||||
|
||||
/**
|
||||
* @return array<string, string|bool>
|
||||
*/
|
||||
public function validateResponse(?string $captchaResponse = null): array;
|
||||
|
||||
public function verifyDomain(string $hostname, ?string $requestUrl = null): bool;
|
||||
}
|
@ -1,45 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Extensions\Captcha\Providers;
|
||||
namespace App\Extensions\Captcha\Schemas;
|
||||
|
||||
use Filament\Forms\Components\Component;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Illuminate\Foundation\Application;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
abstract class CaptchaProvider
|
||||
abstract class CommonSchema
|
||||
{
|
||||
/**
|
||||
* @var array<string, static>
|
||||
*/
|
||||
protected static array $providers = [];
|
||||
|
||||
/**
|
||||
* @return self|static[]
|
||||
*/
|
||||
public static function get(?string $id = null): array|self
|
||||
{
|
||||
return $id ? static::$providers[$id] : static::$providers;
|
||||
}
|
||||
|
||||
protected function __construct(protected Application $app)
|
||||
{
|
||||
if (array_key_exists($this->getId(), static::$providers)) {
|
||||
if (!$this->app->runningUnitTests()) {
|
||||
logger()->warning("Tried to create duplicate Captcha provider with id '{$this->getId()}'");
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
config()->set('captcha.' . Str::lower($this->getId()), $this->getConfig());
|
||||
|
||||
static::$providers[$this->getId()] = $this;
|
||||
}
|
||||
|
||||
abstract public function getId(): string;
|
||||
|
||||
abstract public function getComponent(): Component;
|
||||
public function getName(): string
|
||||
{
|
||||
return Str::upper($this->getId());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string|string[]|bool|null>
|
||||
@ -83,34 +57,6 @@ abstract class CaptchaProvider
|
||||
];
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return Str::title($this->getId());
|
||||
}
|
||||
|
||||
public function getIcon(): ?string
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
public function isEnabled(): bool
|
||||
{
|
||||
$id = Str::upper($this->getId());
|
||||
|
||||
return env("CAPTCHA_{$id}_ENABLED", false);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string|bool>
|
||||
*/
|
||||
public function validateResponse(?string $captchaResponse = null): array
|
||||
{
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'validateResponse not defined',
|
||||
];
|
||||
}
|
||||
|
||||
public function verifyDomain(string $hostname, ?string $requestUrl = null): bool
|
||||
{
|
||||
return true;
|
@ -1,11 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Components\Forms\Fields;
|
||||
namespace App\Extensions\Captcha\Schemas\Turnstile;
|
||||
|
||||
use App\Rules\ValidTurnstileCaptcha;
|
||||
use Filament\Forms\Components\Field;
|
||||
|
||||
class TurnstileCaptcha extends Field
|
||||
class Component extends Field
|
||||
{
|
||||
protected string $viewIdentifier = 'turnstile';
|
||||
|
||||
@ -19,8 +18,6 @@ class TurnstileCaptcha extends Field
|
||||
|
||||
$this->required();
|
||||
|
||||
$this->after(function (TurnstileCaptcha $component) {
|
||||
$component->rule(new ValidTurnstileCaptcha());
|
||||
});
|
||||
$this->rule(new Rule());
|
||||
}
|
||||
}
|
@ -1,16 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace App\Rules;
|
||||
namespace App\Extensions\Captcha\Schemas\Turnstile;
|
||||
|
||||
use App\Extensions\Captcha\Providers\CaptchaProvider;
|
||||
use App\Extensions\Captcha\CaptchaProvider;
|
||||
use Closure;
|
||||
use Illuminate\Contracts\Validation\ValidationRule;
|
||||
use Illuminate\Support\Facades\App;
|
||||
|
||||
class ValidTurnstileCaptcha implements ValidationRule
|
||||
class Rule implements ValidationRule
|
||||
{
|
||||
public function validate(string $attribute, mixed $value, Closure $fail): void
|
||||
{
|
||||
$response = CaptchaProvider::get('turnstile')->validateResponse($value);
|
||||
$response = App::call(fn (CaptchaProvider $provider) => $provider->getActiveSchema()->validateResponse($value));
|
||||
|
||||
if (!$response['success']) {
|
||||
$fail($response['message'] ?? 'Unknown error occurred, please try again');
|
@ -1,26 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Extensions\Captcha\Providers;
|
||||
namespace App\Extensions\Captcha\Schemas\Turnstile;
|
||||
|
||||
use App\Filament\Components\Forms\Fields\TurnstileCaptcha;
|
||||
use App\Extensions\Captcha\Schemas\CaptchaSchemaInterface;
|
||||
use App\Extensions\Captcha\Schemas\CommonSchema;
|
||||
use Exception;
|
||||
use Filament\Forms\Components\Component;
|
||||
use Filament\Forms\Components\Component as BaseComponent;
|
||||
use Filament\Forms\Components\Placeholder;
|
||||
use Filament\Forms\Components\Toggle;
|
||||
use Illuminate\Foundation\Application;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\HtmlString;
|
||||
|
||||
class TurnstileProvider extends CaptchaProvider
|
||||
class TurnstileSchema extends CommonSchema implements CaptchaSchemaInterface
|
||||
{
|
||||
public function getId(): string
|
||||
{
|
||||
return 'turnstile';
|
||||
}
|
||||
|
||||
public function getComponent(): Component
|
||||
public function isEnabled(): bool
|
||||
{
|
||||
return TurnstileCaptcha::make('turnstile');
|
||||
return env('CAPTCHA_TURNSTILE_ENABLED', false);
|
||||
}
|
||||
|
||||
public function getFormComponent(): BaseComponent
|
||||
{
|
||||
return Component::make('turnstile');
|
||||
}
|
||||
|
||||
/**
|
||||
@ -34,7 +39,7 @@ class TurnstileProvider extends CaptchaProvider
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Component[]
|
||||
* @return BaseComponent[]
|
||||
*/
|
||||
public function getSettingsForm(): array
|
||||
{
|
||||
@ -52,20 +57,14 @@ class TurnstileProvider extends CaptchaProvider
|
||||
->label(trans('admin/setting.captcha.info_label'))
|
||||
->columnSpan(2)
|
||||
->content(new HtmlString(trans('admin/setting.captcha.info'))),
|
||||
|
||||
]);
|
||||
}
|
||||
|
||||
public function getIcon(): string
|
||||
public function getIcon(): ?string
|
||||
{
|
||||
return 'tabler-brand-cloudflare';
|
||||
}
|
||||
|
||||
public static function register(Application $app): self
|
||||
{
|
||||
return new self($app);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string|bool>
|
||||
*/
|
@ -3,7 +3,7 @@
|
||||
namespace App\Filament\Admin\Pages;
|
||||
|
||||
use App\Extensions\Avatar\AvatarProvider;
|
||||
use App\Extensions\Captcha\Providers\CaptchaProvider;
|
||||
use App\Extensions\Captcha\CaptchaProvider;
|
||||
use App\Extensions\OAuth\OAuthProvider;
|
||||
use App\Models\Backup;
|
||||
use App\Notifications\MailTested;
|
||||
@ -56,6 +56,8 @@ class Settings extends Page implements HasForms
|
||||
|
||||
protected AvatarProvider $avatarProvider;
|
||||
|
||||
protected CaptchaProvider $captchaProvider;
|
||||
|
||||
/** @var array<mixed>|null */
|
||||
public ?array $data = [];
|
||||
|
||||
@ -64,10 +66,11 @@ class Settings extends Page implements HasForms
|
||||
$this->form->fill();
|
||||
}
|
||||
|
||||
public function boot(OAuthProvider $oauthProvider, AvatarProvider $avatarProvider): void
|
||||
public function boot(OAuthProvider $oauthProvider, AvatarProvider $avatarProvider, CaptchaProvider $captchaProvider): void
|
||||
{
|
||||
$this->oauthProvider = $oauthProvider;
|
||||
$this->avatarProvider = $avatarProvider;
|
||||
$this->captchaProvider = $captchaProvider;
|
||||
}
|
||||
|
||||
public static function canAccess(): bool
|
||||
@ -268,15 +271,14 @@ class Settings extends Page implements HasForms
|
||||
{
|
||||
$formFields = [];
|
||||
|
||||
$captchaProviders = CaptchaProvider::get();
|
||||
foreach ($captchaProviders as $captchaProvider) {
|
||||
$id = Str::upper($captchaProvider->getId());
|
||||
$name = Str::title($captchaProvider->getId());
|
||||
$captchaSchemas = $this->captchaProvider->get();
|
||||
foreach ($captchaSchemas as $captchaSchema) {
|
||||
$id = Str::upper($captchaSchema->getId());
|
||||
|
||||
$formFields[] = Section::make($name)
|
||||
$formFields[] = Section::make($captchaSchema->getName())
|
||||
->columns(5)
|
||||
->icon($captchaProvider->getIcon() ?? 'tabler-shield')
|
||||
->collapsed(fn () => !env("CAPTCHA_{$id}_ENABLED", false))
|
||||
->icon($captchaSchema->getIcon() ?? 'tabler-shield')
|
||||
->collapsed(fn () => !$captchaSchema->isEnabled())
|
||||
->collapsible()
|
||||
->schema([
|
||||
Hidden::make("CAPTCHA_{$id}_ENABLED")
|
||||
@ -287,21 +289,14 @@ class Settings extends Page implements HasForms
|
||||
->visible(fn (Get $get) => $get("CAPTCHA_{$id}_ENABLED"))
|
||||
->label(trans('admin/setting.captcha.disable'))
|
||||
->color('danger')
|
||||
->action(function (Set $set) use ($id) {
|
||||
$set("CAPTCHA_{$id}_ENABLED", false);
|
||||
}),
|
||||
->action(fn (Set $set) => $set("CAPTCHA_{$id}_ENABLED", false)),
|
||||
FormAction::make("enable_captcha_$id")
|
||||
->visible(fn (Get $get) => !$get("CAPTCHA_{$id}_ENABLED"))
|
||||
->label(trans('admin/setting.captcha.enable'))
|
||||
->color('success')
|
||||
->action(function (Set $set) use ($id, $captchaProviders) {
|
||||
foreach ($captchaProviders as $captchaProvider) {
|
||||
$loopId = Str::upper($captchaProvider->getId());
|
||||
$set("CAPTCHA_{$loopId}_ENABLED", $loopId === $id);
|
||||
}
|
||||
}),
|
||||
->action(fn (Set $set) => $set("CAPTCHA_{$id}_ENABLED", true)),
|
||||
])->columnSpan(1),
|
||||
Group::make($captchaProvider->getSettingsForm())
|
||||
Group::make($captchaSchema->getSettingsForm())
|
||||
->visible(fn (Get $get) => $get("CAPTCHA_{$id}_ENABLED"))
|
||||
->columns(4)
|
||||
->columnSpan(4),
|
||||
|
@ -3,7 +3,7 @@
|
||||
namespace App\Filament\Pages\Auth;
|
||||
|
||||
use App\Events\Auth\ProvidedAuthenticationToken;
|
||||
use App\Extensions\Captcha\Providers\CaptchaProvider;
|
||||
use App\Extensions\Captcha\CaptchaProvider;
|
||||
use App\Extensions\OAuth\OAuthProvider;
|
||||
use App\Facades\Activity;
|
||||
use App\Models\User;
|
||||
@ -29,10 +29,13 @@ class Login extends BaseLogin
|
||||
|
||||
protected OAuthProvider $oauthProvider;
|
||||
|
||||
public function boot(Google2FA $google2FA, OAuthProvider $oauthProvider): void
|
||||
protected CaptchaProvider $captchaProvider;
|
||||
|
||||
public function boot(Google2FA $google2FA, OAuthProvider $oauthProvider, CaptchaProvider $captchaProvider): void
|
||||
{
|
||||
$this->google2FA = $google2FA;
|
||||
$this->oauthProvider = $oauthProvider;
|
||||
$this->captchaProvider = $captchaProvider;
|
||||
}
|
||||
|
||||
public function authenticate(): ?LoginResponse
|
||||
@ -145,13 +148,7 @@ class Login extends BaseLogin
|
||||
|
||||
private function getCaptchaComponent(): ?Component
|
||||
{
|
||||
$captchaProvider = collect(CaptchaProvider::get())->filter(fn (CaptchaProvider $provider) => $provider->isEnabled())->first();
|
||||
|
||||
if (!$captchaProvider) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $captchaProvider->getComponent();
|
||||
return $this->captchaProvider->getActiveSchema()?->getFormComponent();
|
||||
}
|
||||
|
||||
protected function throwFailureValidationException(): never
|
||||
|
@ -2,35 +2,35 @@
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use App\Extensions\Captcha\CaptchaProvider;
|
||||
use Closure;
|
||||
use Illuminate\Foundation\Application;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
use App\Events\Auth\FailedCaptcha;
|
||||
use App\Extensions\Captcha\Providers\CaptchaProvider;
|
||||
use Symfony\Component\HttpKernel\Exception\HttpException;
|
||||
|
||||
readonly class VerifyCaptcha
|
||||
{
|
||||
public function __construct(private Application $app) {}
|
||||
|
||||
public function handle(Request $request, Closure $next): mixed
|
||||
public function handle(Request $request, Closure $next, CaptchaProvider $captchaProvider): mixed
|
||||
{
|
||||
if ($this->app->isLocal()) {
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
$captchaProviders = collect(CaptchaProvider::get())->filter(fn (CaptchaProvider $provider) => $provider->isEnabled())->all();
|
||||
foreach ($captchaProviders as $captchaProvider) {
|
||||
$response = $captchaProvider->validateResponse();
|
||||
$schemas = $captchaProvider->getActiveSchemas();
|
||||
foreach ($schemas as $schema) {
|
||||
$response = $schema->validateResponse();
|
||||
|
||||
if ($response['success'] && $captchaProvider->verifyDomain($response['hostname'] ?? '', $request->url())) {
|
||||
if ($response['success'] && $schema->verifyDomain($response['hostname'] ?? '', $request->url())) {
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
event(new FailedCaptcha($request->ip(), $response['message'] ?? null));
|
||||
|
||||
throw new HttpException(Response::HTTP_BAD_REQUEST, "Failed to validate {$captchaProvider->getId()} captcha data.");
|
||||
throw new HttpException(Response::HTTP_BAD_REQUEST, "Failed to validate {$schema->getId()} captcha data.");
|
||||
}
|
||||
|
||||
// No captcha enabled
|
||||
|
@ -10,7 +10,6 @@ use App\Checks\NodeVersionsCheck;
|
||||
use App\Checks\PanelVersionCheck;
|
||||
use App\Checks\ScheduleCheck;
|
||||
use App\Checks\UsedDiskSpaceCheck;
|
||||
use App\Extensions\Captcha\Providers\TurnstileProvider;
|
||||
use App\Models;
|
||||
use App\Services\Helpers\SoftwareVersionService;
|
||||
use Dedoc\Scramble\Scramble;
|
||||
@ -92,9 +91,6 @@ class AppServiceProvider extends ServiceProvider
|
||||
Scramble::registerApi('application', ['api_path' => 'api/application', 'info' => ['version' => '1.0']])->afterOpenApiGenerated($bearerTokens);
|
||||
Scramble::registerApi('client', ['api_path' => 'api/client', 'info' => ['version' => '1.0']])->afterOpenApiGenerated($bearerTokens);
|
||||
|
||||
// Default Captcha provider
|
||||
TurnstileProvider::register($app);
|
||||
|
||||
FilamentColor::register([
|
||||
'danger' => Color::Red,
|
||||
'gray' => Color::Zinc,
|
||||
|
22
app/Providers/Extensions/CaptchaServiceProvider.php
Normal file
22
app/Providers/Extensions/CaptchaServiceProvider.php
Normal file
@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace App\Providers\Extensions;
|
||||
|
||||
use App\Extensions\Captcha\CaptchaProvider;
|
||||
use App\Extensions\Captcha\Schemas\Turnstile\TurnstileSchema;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
class CaptchaServiceProvider extends ServiceProvider
|
||||
{
|
||||
public function register(): void
|
||||
{
|
||||
$this->app->singleton(CaptchaProvider::class, function ($app) {
|
||||
$service = new CaptchaProvider();
|
||||
|
||||
// Default Captcha providers
|
||||
$service->register(new TurnstileSchema());
|
||||
|
||||
return $service;
|
||||
});
|
||||
}
|
||||
}
|
@ -9,6 +9,7 @@ return [
|
||||
App\Providers\Filament\AppPanelProvider::class,
|
||||
App\Providers\Filament\ServerPanelProvider::class,
|
||||
App\Providers\Extensions\AvatarServiceProvider::class,
|
||||
App\Providers\Extensions\CaptchaServiceProvider::class,
|
||||
App\Providers\Extensions\FeatureServiceProvider::class,
|
||||
App\Providers\Extensions\OAuthServiceProvider::class,
|
||||
App\Providers\RouteServiceProvider::class,
|
||||
|
@ -20,6 +20,6 @@ parameters:
|
||||
identifier: larastan.noEnvCallsOutsideOfConfig
|
||||
paths:
|
||||
- app/Console/Commands/Environment/*.php
|
||||
- app/Extensions/Captcha/Providers/*.php
|
||||
- app/Extensions/Captcha/Schemas/*.php
|
||||
- app/Extensions/OAuth/Schemas/*.php
|
||||
- app/Filament/Admin/Pages/Settings.php
|
||||
|
Loading…
x
Reference in New Issue
Block a user