diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index b5d6720..480a667 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -15,7 +15,7 @@ jobs: os: [ubuntu-latest, windows-latest] php: [8.2, 8.1] laravel: [10.*] - stability: [prefer-lowest, prefer-stable] + stability: [prefer-stable] include: - laravel: 10.* testbench: 8.* diff --git a/README.md b/README.md index 7d5ff56..0971990 100644 --- a/README.md +++ b/README.md @@ -11,90 +11,116 @@
-__Filament Turnstile__, is a plugin to help you implement the Cloudflare turnstile. +**Filament Turnstile** is an essential plugin designed to seamlessly integrate Cloudflare's turnstile into your applications. -This plugin uses [Laravel Turnstile](https://github.com/coderflexx/laravel-turnstile) Behind the scene, you can head to the page __README__ to learn more. +This plugin uses [Laravel Turnstile](https://github.com/coderflexx/laravel-turnstile) under the hood. For detailed information, explore the [Laravel Turnstile README](https://github.com/coderflexx/laravel-turnstile). ## Installation -You can install the package via composer: - +Install the package via Composer: ```bash composer require coderflex/filament-turnstile ``` +For users still on **Filament V2**, install the package using: + +```bash +composer require coderflex/filament-turnstil "^1.0" +``` ## Turnstile Keys -To be able to use __Cloudflare Turnstile__, you need to get the `SiteKey`, and the `SecretKey` from your [Cloudflare dashboard](https://developers.cloudflare.com/turnstile/get-started/#get-a-sitekey-and-secret-key) +To utilize **Cloudflare Turnstile**, obtain your `SiteKey` and `SecretKey` from your Cloudflare Dashboard. -After Generating the __keys__, use `TURNSTILE_SITE_KEY`, and `TURNSTILE_SECRET_KEY` in your `.env` file +Refer to the [documentation](https://developers.cloudflare.com/turnstile/get-started/#get-a-sitekey-and-secret-key) for detailed instructions. -```.env -TURNSTILE_SITE_KEY=2x00000000000000000000AB -TURNSTILE_SECRET_KEY=2x0000000000000000000000000000000AA +After generating the **keys**, include them in your `.env` file using the following format: + +```env +TURNSTILE_SITE_KEY=1x00000000000000000000AA +TURNSTILE_SECRET_KEY=1x0000000000000000000000000000000AA ``` -If you want to test the widget, you can use the [Dummy site keys and secret keys](https://developers.cloudflare.com/turnstile/reference/testing/) that Cloudflare provides. +For testing purposes, you can use [Dummy site keys and secret keys](https://developers.cloudflare.com/turnstile/reference/testing/) provided by Cloudflare. ## Usage -The usage of this plugin, is really straight - forward. In your form, use the following code: +Utilizing this plugin is incredibly straightforward. In your form, incorporate the following code: ```php -... use Coderflex\FilamentTurnstile\Forms\Components\Turnstile; -... - Turnstile::make('captcha') - ->theme('auto') - ->language('fr') - ->size('normal'), +Turnstile::make('captcha') + ->theme('auto') // accepts light, dark, auto + ->language('en-US') // see below + ->size('normal'), // accepts normal, compact ``` -The `Turnstile` field, has few options to use. You can learn more about them in [the Cloudflare configuration section](https://developers.cloudflare.com/turnstile/get-started/client-side-rendering/#configurations) +For a list of supported languages, refer to the [supported languages section](https://developers.cloudflare.com/turnstile/reference/supported-languages/). + +The `Turnstile` field offers various options; you can learn more about them in [the Cloudflare configuration section](https://developers.cloudflare.com/turnstile/get-started/client-side-rendering/#configurations). + -## Real Life Example: -In order to use __Turnstile__ captcha with the `Login` page in filament, use the following steps: +## Real-Life Example: -Create a new `App/Filament/Pages/Login.php` class +To implement the **Turnstile** captcha with the `Login` page in Filament, follow these steps: + +Create a new `App/Filament/Pages/Auth/Login.php` class: ```php - + */ + protected function getForms(): array { - return array_merge( - parent::getFormSchema(), - [ - Turnstile::make('cf-captcha') - ->theme('auto') - ->language('en-US') - ->size('normal'), - ] - ); + return [ + 'form' => $this->form( + $this->makeForm() + ->schema([ + $this->getEmailFormComponent(), + $this->getPasswordFormComponent(), + $this->getRememberFormComponent(), + Turnstile::make('captcha') + ->label('Captcha') + ->theme('auto'), + ]) + ->statePath('data'), + ), + ]; } } ``` -Then override the `Login` class in the `filament.php` config file. +Then, override the `login()` method in your `PanelProvider` (e.g., `AdminPanelProvider`): ```php - return [ - .... - 'auth' => [ - 'guard' => env('FILAMENT_AUTH_GUARD', 'web'), - 'pages' => [ - 'login' => \App\Filament\Pages\Login::class, - ], - ], - ... - ] +namespace App\Providers\Filament; + +use App\Filament\Auth\Login; +use Filament\Panel; +use Filament\PanelProvider; + +class AdminPanelProvider extends PanelProvider +{ + public function panel(Panel $panel): Panel + { + return $panel + ->default() + ->id('admin') + ->path('admin') + ->login(Login::class); // override the login page class. + ... + } +} ``` ## Testing diff --git a/art/login_screen.png b/art/login_screen.png index cf58fa5..d984f30 100644 Binary files a/art/login_screen.png and b/art/login_screen.png differ diff --git a/composer.json b/composer.json index 9f73980..fe087f4 100644 --- a/composer.json +++ b/composer.json @@ -26,7 +26,7 @@ "spatie/laravel-package-tools": "^1.14.0" }, "require-dev": { - "filament/filament": "^2.17", + "filament/filament": "^3.0", "laravel/pint": "^1.0", "nunomaduro/collision": "^7.9", "nunomaduro/larastan": "^2.0.1", diff --git a/resources/views/components/turnstile.blade.php b/resources/views/components/turnstile.blade.php index e359b12..278133b 100644 --- a/resources/views/components/turnstile.blade.php +++ b/resources/views/components/turnstile.blade.php @@ -1,48 +1,44 @@ - -
+ +
-
-
-
- - @push('scripts') - - + })()" + > +
+
+
+ + @push('scripts') + @endpush
\ No newline at end of file diff --git a/src/FilamentTurnstileServiceProvider.php b/src/FilamentTurnstileServiceProvider.php index cfe6ce4..02fa4a1 100644 --- a/src/FilamentTurnstileServiceProvider.php +++ b/src/FilamentTurnstileServiceProvider.php @@ -2,10 +2,10 @@ namespace Coderflex\FilamentTurnstile; -use Filament\PluginServiceProvider; use Spatie\LaravelPackageTools\Package; +use Spatie\LaravelPackageTools\PackageServiceProvider; -class FilamentTurnstileServiceProvider extends PluginServiceProvider +class FilamentTurnstileServiceProvider extends PackageServiceProvider { public function configurePackage(Package $package): void { diff --git a/src/Forms/Components/Turnstile.php b/src/Forms/Components/Turnstile.php index 309d11b..bd27458 100644 --- a/src/Forms/Components/Turnstile.php +++ b/src/Forms/Components/Turnstile.php @@ -7,6 +7,8 @@ class Turnstile extends Field { + protected string $viewIdentifier = 'turnstile'; + protected string $view = 'turnstile::components.turnstile'; protected string $theme = 'auto'; @@ -21,7 +23,9 @@ protected function setUp(): void $this->label(''); - $this->rules(['required', new TurnstileCheck()]); + $this->required(); + + $this->rule(new TurnstileCheck()); $this->dehydrated(false); } @@ -47,17 +51,26 @@ public function language(string $language): static return $this; } - public function getTheme(): string + /** + * @return string + */ + public function getTheme() { return $this->evaluate($this->theme); } - public function getSize(): string + /** + * @return string + */ + public function getSize() { return $this->evaluate($this->size); } - public function getLanguage(): string + /** + * @return string + */ + public function getLanguage() { return $this->evaluate($this->language); } diff --git a/tests/Database/Factories/UserFactory.php b/tests/Database/Factories/UserFactory.php new file mode 100644 index 0000000..9e4518a --- /dev/null +++ b/tests/Database/Factories/UserFactory.php @@ -0,0 +1,10 @@ +id(); + $table->string('name'); + $table->string('email'); + $table->text('content'); + $table->timestamps(); + }); + } +}; diff --git a/tests/Database/Migrations/create_users_table.php b/tests/Database/Migrations/create_users_table.php new file mode 100644 index 0000000..57f0097 --- /dev/null +++ b/tests/Database/Migrations/create_users_table.php @@ -0,0 +1,21 @@ +id(); + $table->string('name'); + $table->string('email')->unique(); + $table->timestamp('email_verified_at')->nullable(); + $table->string('password'); + $table->rememberToken(); + $table->timestamps(); + }); + } +}; diff --git a/tests/Fixtures/ContactUs.php b/tests/Fixtures/ContactUs.php new file mode 100644 index 0000000..3081880 --- /dev/null +++ b/tests/Fixtures/ContactUs.php @@ -0,0 +1,58 @@ +form->fill(); + } + + public function form(Form $form): Form + { + return $form; + } + + protected function getForms(): array + { + return [ + 'form' => $this->form( + $this->makeForm() + ->schema([ + Forms\Components\TextInput::make('name') + ->label('Name') + ->required(), + Forms\Components\TextInput::make('email') + ->label('Email') + ->required(), + Forms\Components\TextInput::make('content') + ->label('Content') + ->required(), + Turnstile::make('cf-captcha') + ->theme('auto'), + ]) + ) + ->statePath('data') + ->model(Contact::class), + ]; + } + + public function send() + { + Contact::create($this->form->getState()); + } + + public function render() + { + return 'fixtures.contact-us'; + } +} diff --git a/tests/Fixtures/Login.php b/tests/Fixtures/Login.php deleted file mode 100644 index ae2d1b8..0000000 --- a/tests/Fixtures/Login.php +++ /dev/null @@ -1,19 +0,0 @@ -theme('auto'), - ] - ); - } -} diff --git a/tests/Models/Contact.php b/tests/Models/Contact.php new file mode 100644 index 0000000..c45a7d5 --- /dev/null +++ b/tests/Models/Contact.php @@ -0,0 +1,10 @@ + 'Coderflex\\FilamentTurnstile\\Tests\\Database\\Factories\\'.class_basename($modelName).'Factory' + ); + config()->set('app.key', '6rE9Nz59bGRbeMATftriyQjrpF7DcOQm'); - $this->registerLivewireComponents(); + $this->setCurrentFilamentPanel(); } protected function getPackageProviders($app) { return [ + ActionsServiceProvider::class, + BladeCaptureDirectiveServiceProvider::class, + BladeHeroiconsServiceProvider::class, + BladeIconsServiceProvider::class, FilamentServiceProvider::class, FormsServiceProvider::class, + InfolistsServiceProvider::class, LivewireServiceProvider::class, + NotificationsServiceProvider::class, SupportServiceProvider::class, + TablesServiceProvider::class, + WidgetsServiceProvider::class, FilamentTurnstileServiceProvider::class, + TurnstilePanelProvider::class, ]; } public function getEnvironmentSetUp($app) { - // + config()->set('database.default', 'testing'); + + $app['config']->set('view.paths', [ + ...$app['config']->get('view.paths'), + __DIR__.'/resources/views', + ]); + + $migrations = [ + include __DIR__.'/Database/Migrations/create_users_table.php', + include __DIR__.'/Database/Migrations/create_contacts_table.php', + ]; + + collect($migrations)->each( + fn ($migration) => $migration->up() + ); } - protected function registerLivewireComponents(): void + protected function setCurrentFilamentPanel(): void { - Livewire::component('login', Login::class); + Filament::setCurrentPanel( + Filament::getPanel('turnstile') + ); } } diff --git a/tests/TurnstilePanelProvider.php b/tests/TurnstilePanelProvider.php new file mode 100644 index 0000000..aa42894 --- /dev/null +++ b/tests/TurnstilePanelProvider.php @@ -0,0 +1,43 @@ +id('turnstile') + ->login() + ->registration() + ->resources([]) + ->pages([]) + ->middleware([ + EncryptCookies::class, + AddQueuedCookiesToResponse::class, + StartSession::class, + AuthenticateSession::class, + ShareErrorsFromSession::class, + VerifyCsrfToken::class, + SubstituteBindings::class, + DisableBladeIconComponents::class, + DispatchServingFilamentEvent::class, + ]) + ->authMiddleware([ + Authenticate::class, + ]); + } +} diff --git a/tests/TurnstileTest.php b/tests/TurnstileTest.php index 2394942..50d5c8a 100644 --- a/tests/TurnstileTest.php +++ b/tests/TurnstileTest.php @@ -1,9 +1,96 @@ assertFormFieldExists('cf-captcha'); +it('can render contact page', function () { + livewire(ContactUs::class) + ->assertSuccessful(); +}); + +test('contact page has captcha field', function () { + livewire(ContactUs::class) + ->assertFormFieldExists('cf-captcha', 'form'); +}); + +it('can return success response', function () { + /** + * Setting Turnstile keys to always pass the request + * + * @link https://developers.cloudflare.com/turnstile/reference/testing/#dummy-sitekeys-and-secret-keys + */ + Config::set('turnstile', [ + 'turnstile_site_key' => '1x00000000000000000000AA', + 'turnstile_secret_key' => '1x0000000000000000000000000000000AA', + ]); + + $response = LaravelTurnstile::validate('XXXX.DUMMY.TOKEN.XXXX'); + + expect($response['success'])->toBeTrue(); +}); + +it('can does not return success response', function () { + /** + * Setting Turnstile keys to always block the request + * + * @link https://developers.cloudflare.com/turnstile/reference/testing/#dummy-sitekeys-and-secret-keys + */ + Config::set('turnstile', [ + 'turnstile_site_key' => '2x00000000000000000000AB', + 'turnstile_secret_key' => '2x0000000000000000000000000000000AA', + ]); + + $response = LaravelTurnstile::validate('XXXX.DUMMY.TOKEN.XXXX'); + + expect($response['success'])->toBeFalse(); +}); + +it('can send a message', function () { + /** + * In this context, Alpine.js didn't function as expected due to the need to pass the `cf-captcha` field. + * The value of the mentioned key is dynamically determined by the response from Cloudflare (CF) in the UI. + * For more information, refer to the Cloudflare Turnstile documentation: + * + * @link https://developers.cloudflare.com/turnstile/ + */ + Config::set('turnstile', [ + 'turnstile_site_key' => '1x00000000000000000000AA', + 'turnstile_secret_key' => '1x0000000000000000000000000000000AA', + ]); + + livewire(ContactUs::class) + ->fillForm([ + 'name' => 'John Doe', + 'email' => 'john@example.com', + 'content' => 'This is a simple message', + 'cf-captcha' => 'XXXX.DUMMY.TOKEN.XXXX', + ]) + ->call('send') + ->assertHasNoFormErrors(); + + expect(Contact::get()) + ->toHaveCount(1); +}); + +it('cannot send a message', function () { + Config::set('turnstile', [ + 'turnstile_site_key' => '2x00000000000000000000AB', + 'turnstile_secret_key' => '2x0000000000000000000000000000000AA', + ]); + + livewire(ContactUs::class) + ->fillForm([ + 'name' => 'John Doe', + 'email' => 'john@example.com', + 'content' => 'This is a simple message', + ]) + ->call('send') + ->assertHasFormErrors(['cf-captcha']); + + expect(Contact::get()) + ->toHaveCount(0); }); diff --git a/tests/resources/views/fixtures/contact-us.blade.php b/tests/resources/views/fixtures/contact-us.blade.php new file mode 100644 index 0000000..fccf563 --- /dev/null +++ b/tests/resources/views/fixtures/contact-us.blade.php @@ -0,0 +1,7 @@ +
+ + {{ $this->form }} + + + +
\ No newline at end of file