├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── bin └── build.js ├── composer.json ├── config └── filament-email-2fa.php ├── database ├── factories │ └── ModelFactory.php └── migrations │ └── create_filament-email-2fa_table.php.stub ├── postcss.config.cjs ├── resources ├── css │ └── index.css ├── dist │ └── .gitkeep ├── js │ └── index.js ├── lang │ └── en │ │ └── filament-email-2fa.php └── views │ ├── .gitkeep │ ├── email-sent.blade.php │ ├── email-template.blade.php │ ├── login-success.blade.php │ └── simple-layout.blade.php ├── src ├── Commands │ └── FilamentEmail2faCommand.php ├── Exceptions │ └── InvalidTwoFACodeException.php ├── Facades │ └── FilamentEmail2fa.php ├── FilamentEmail2fa.php ├── FilamentEmail2faPlugin.php ├── FilamentEmail2faServiceProvider.php ├── Interfaces │ └── RequireTwoFALogin.php ├── Mail │ └── TwoFAEmail.php ├── Middlewares │ └── IsTwoFAVerified.php ├── Models │ ├── TwoFaCode.php │ └── TwoFaVerify.php ├── Pages │ ├── LoginSuccessPage.php │ └── TwoFactorAuth.php ├── Responses │ ├── LoginSuccessResponse.php │ └── TwoFAResponse.php ├── Testing │ └── TestsFilamentEmail2fa.php └── Trait │ └── HasTwoFALogin.php └── stubs └── .gitkeep /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to `filament-email-2fa` will be documented in this file. 4 | 5 | ## 1.0.0 - 202X-XX-XX 6 | 7 | - initial release 8 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) solution-forest 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # filament-email-2fa 2 | 3 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/solution-forest/filament-email-2fa.svg?style=flat-square)](https://packagist.org/packages/solution-forest/filament-email-2fa) 4 | [![GitHub Tests Action Status](https://img.shields.io/github/actions/workflow/status/solution-forest/filament-email-2fa/run-tests.yml?branch=main&label=tests&style=flat-square)](https://github.com/solution-forest/filament-email-2fa/actions?query=workflow%3Arun-tests+branch%3Amain) 5 | [![GitHub Code Style Action Status](https://img.shields.io/github/actions/workflow/status/solution-forest/filament-email-2fa/fix-php-code-styling.yml?branch=main&label=code%20style&style=flat-square)](https://github.com/solution-forest/filament-email-2fa/actions?query=workflow%3A"Fix+PHP+code+styling"+branch%3Amain) 6 | [![Total Downloads](https://img.shields.io/packagist/dt/solution-forest/filament-email-2fa.svg?style=flat-square)](https://packagist.org/packages/solution-forest/filament-email-2fa) 7 | 8 | 9 | ## Secure Your Filament Applications with Email-Based 2FA 10 | 11 | This package seamlessly integrates two-factor authentication (2FA) into your Filament PHP applications using email verification codes. Enhance the security of your user accounts and protect sensitive data. 12 | 13 | ![image](https://github.com/solutionforest/filament-email-2fa/assets/68211972/8fcefe16-c280-41f0-bc26-652f285b8975) 14 | 15 | 16 | ### Key Features: 17 | 18 | - Easy Integration: Quickly add 2FA to your Filament projects with minimal configuration. 19 | - Email Verification: Users receive time-sensitive codes via email for secure login. 20 | - Customizable: Tailor the 2FA experience with configurable options (e.g., code expiry time). 21 | - Seamless User Experience: Provides a user-friendly interface for setting up and using 2FA. 22 | 23 | 24 | ### How it Works: 25 | 26 | - Upon successful login, users are prompted to enter a verification code sent to their email address. 27 | - The package handles code generation, email delivery, and verification logic. 28 | - Once verified, users gain access to the protected Filament panel. 29 | 30 | ### Ideal For: 31 | 32 | Filament applications handling sensitive user data. 33 | Projects requiring an extra layer of account security. 34 | Developers seeking a straightforward 2FA solution. 35 | 36 | 37 | ## Installation 38 | 39 | You can install the package via composer: 40 | 41 | ```bash 42 | composer require solution-forest/filament-email-2fa 43 | ``` 44 | 45 | You can publish and run the migrations with: 46 | 47 | ```bash 48 | php artisan vendor:publish --tag="filament-email-2fa-migrations" 49 | php artisan migrate 50 | ``` 51 | 52 | You can publish the config file with: 53 | 54 | ```bash 55 | php artisan vendor:publish --tag="filament-email-2fa-config" 56 | ``` 57 | 58 | Optionally, you can publish the views using 59 | 60 | ```bash 61 | php artisan vendor:publish --tag="filament-email-2fa-views" 62 | ``` 63 | 64 | This is the contents of the published config file: 65 | 66 | ```php 67 | return [ 68 | 'code_table' => 'filament_email_2fa_codes', 69 | 'verify_table' => 'filament_email_2fa_verify', 70 | 71 | 'code_model' => \Solutionforest\FilamentEmail2fa\Models\TwoFaCode::class, 72 | 'verify_model' => \Solutionforest\FilamentEmail2fa\Models\TwoFaVerify::class, 73 | 74 | 'expiry_time_by_mins' => 10, 75 | 76 | '2fa_page' => \Solutionforest\FilamentEmail2fa\Pages\TwoFactorAuth::class, 77 | 'login_success_page' => \Solutionforest\FilamentEmail2fa\Pages\LoginSuccessPage::class, 78 | ]; 79 | ``` 80 | 81 | ## Adding the plugin to a panel 82 | 83 | ```php 84 | use Solutionforest\FilamentEmail2fa\FilamentEmail2faPlugin; 85 | 86 | return $panel 87 | // ... 88 | ->plugin(FilamentEmail2faPlugin::make()); 89 | ``` 90 | 91 | ## Preparing your filament user class 92 | 93 | Implement the 'RequireTwoFALogin' interface and use the 'HasTwoFALogin' trait 94 | 95 | ```php 96 | use Solutionforest\FilamentEmail2fa\Interfaces\RequireTwoFALogin; 97 | use Solutionforest\FilamentEmail2fa\Trait\HasTwoFALogin; 98 | 99 | class FilamentUser extends Authenticatable implements FilamentUserContract,RequireTwoFALogin{ 100 | use HasTwoFALogin; 101 | } 102 | ``` 103 | 104 | -------------------------------------------------------------------------------- /bin/build.js: -------------------------------------------------------------------------------- 1 | import esbuild from 'esbuild' 2 | 3 | const isDev = process.argv.includes('--dev') 4 | 5 | async function compile(options) { 6 | const context = await esbuild.context(options) 7 | 8 | if (isDev) { 9 | await context.watch() 10 | } else { 11 | await context.rebuild() 12 | await context.dispose() 13 | } 14 | } 15 | 16 | const defaultOptions = { 17 | define: { 18 | 'process.env.NODE_ENV': isDev ? `'development'` : `'production'`, 19 | }, 20 | bundle: true, 21 | mainFields: ['module', 'main'], 22 | platform: 'neutral', 23 | sourcemap: isDev ? 'inline' : false, 24 | sourcesContent: isDev, 25 | treeShaking: true, 26 | target: ['es2020'], 27 | minify: !isDev, 28 | plugins: [{ 29 | name: 'watchPlugin', 30 | setup: function (build) { 31 | build.onStart(() => { 32 | console.log(`Build started at ${new Date(Date.now()).toLocaleTimeString()}: ${build.initialOptions.outfile}`) 33 | }) 34 | 35 | build.onEnd((result) => { 36 | if (result.errors.length > 0) { 37 | console.log(`Build failed at ${new Date(Date.now()).toLocaleTimeString()}: ${build.initialOptions.outfile}`, result.errors) 38 | } else { 39 | console.log(`Build finished at ${new Date(Date.now()).toLocaleTimeString()}: ${build.initialOptions.outfile}`) 40 | } 41 | }) 42 | } 43 | }], 44 | } 45 | 46 | compile({ 47 | ...defaultOptions, 48 | // entryPoints: ['./resources/js/index.js'], 49 | // outfile: './resources/dist/filament-email-2fa.js', 50 | }) 51 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "solution-forest/filament-email-2fa", 3 | "description": "filament-email-2fa", 4 | "keywords": [ 5 | "solution-forest", 6 | "laravel", 7 | "filament-email-2fa" 8 | ], 9 | "homepage": "https://github.com/solution-forest/filament-email-2fa", 10 | "support": { 11 | "issues": "https://github.com/solution-forest/filament-email-2fa/issues", 12 | "source": "https://github.com/solution-forest/filament-email-2fa" 13 | }, 14 | "license": "MIT", 15 | "authors": [ 16 | { 17 | "name": "Angie", 18 | "email": "info@solutionforest.net", 19 | "role": "Developer" 20 | } 21 | ], 22 | "require": { 23 | "php": "^8.1", 24 | "filament/filament": "^3.0", 25 | "spatie/laravel-package-tools": "^1.15.0" 26 | }, 27 | "require-dev": { 28 | "laravel/pint": "^1.0", 29 | "nunomaduro/collision": "^7.9", 30 | "nunomaduro/larastan": "^2.0.1", 31 | "orchestra/testbench": "^8.0", 32 | "pestphp/pest": "^2.1", 33 | "pestphp/pest-plugin-arch": "^2.0", 34 | "pestphp/pest-plugin-laravel": "^2.0", 35 | "phpstan/extension-installer": "^1.1", 36 | "phpstan/phpstan-deprecation-rules": "^1.0", 37 | "phpstan/phpstan-phpunit": "^1.0", 38 | "spatie/laravel-ray": "^1.26" 39 | }, 40 | "autoload": { 41 | "psr-4": { 42 | "Solutionforest\\FilamentEmail2fa\\": "src/", 43 | "Solutionforest\\FilamentEmail2fa\\Database\\Factories\\": "database/factories/" 44 | } 45 | }, 46 | "autoload-dev": { 47 | "psr-4": { 48 | "Solutionforest\\FilamentEmail2fa\\Tests\\": "tests/" 49 | } 50 | }, 51 | "scripts": { 52 | "post-autoload-dump": "@php ./vendor/bin/testbench package:discover --ansi", 53 | "analyse": "vendor/bin/phpstan analyse", 54 | "test": "vendor/bin/pest", 55 | "test-coverage": "vendor/bin/pest --coverage", 56 | "format": "vendor/bin/pint" 57 | }, 58 | "config": { 59 | "sort-packages": true, 60 | "allow-plugins": { 61 | "pestphp/pest-plugin": true, 62 | "phpstan/extension-installer": true 63 | } 64 | }, 65 | "extra": { 66 | "laravel": { 67 | "providers": [ 68 | "Solutionforest\\FilamentEmail2fa\\FilamentEmail2faServiceProvider" 69 | ], 70 | "aliases": { 71 | "FilamentEmail2fa": "Solutionforest\\FilamentEmail2fa\\Facades\\FilamentEmail2fa" 72 | } 73 | } 74 | }, 75 | "minimum-stability": "dev", 76 | "prefer-stable": true 77 | } 78 | -------------------------------------------------------------------------------- /config/filament-email-2fa.php: -------------------------------------------------------------------------------- 1 | 'filament_email_2fa_codes', 6 | 'verify_table' => 'filament_email_2fa_verify', 7 | 8 | 'code_model' => \Solutionforest\FilamentEmail2fa\Models\TwoFaCode::class, 9 | 'verify_model' => \Solutionforest\FilamentEmail2fa\Models\TwoFaVerify::class, 10 | 11 | 'expiry_time_by_mins' => 10, 12 | 13 | '2fa_page' => \Solutionforest\FilamentEmail2fa\Pages\TwoFactorAuth::class, 14 | 'login_success_page' => \Solutionforest\FilamentEmail2fa\Pages\LoginSuccessPage::class, 15 | 16 | ]; 17 | -------------------------------------------------------------------------------- /database/factories/ModelFactory.php: -------------------------------------------------------------------------------- 1 | id(); 15 | $table->timestamps(); 16 | $table->morphs('user'); 17 | $table->string('code',12)->index(); 18 | $table->dateTime('expiry_at'); 19 | }); 20 | 21 | 22 | 23 | 24 | $verify_table = config('filament-email-2fa.verify_table'); 25 | 26 | Schema::create($verify_table, function (Blueprint $table) { 27 | $table->id(); 28 | $table->timestamps(); 29 | $table->morphs('user'); 30 | $table->string('session_id')->index(); 31 | }); 32 | } 33 | }; 34 | -------------------------------------------------------------------------------- /postcss.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | "postcss-import": {}, 4 | "tailwindcss/nesting": {}, 5 | tailwindcss: {}, 6 | autoprefixer: {}, 7 | }, 8 | } 9 | -------------------------------------------------------------------------------- /resources/css/index.css: -------------------------------------------------------------------------------- 1 | @import '../../vendor/filament/filament/resources/css/theme.css'; 2 | -------------------------------------------------------------------------------- /resources/dist/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solutionforest/filament-email-2fa/8c6adbc025c3d9a32ee77cf54a23ba8bd429d253/resources/dist/.gitkeep -------------------------------------------------------------------------------- /resources/js/index.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solutionforest/filament-email-2fa/8c6adbc025c3d9a32ee77cf54a23ba8bd429d253/resources/js/index.js -------------------------------------------------------------------------------- /resources/lang/en/filament-email-2fa.php: -------------------------------------------------------------------------------- 1 | 'Login Success', 6 | 'email_sent' => 'A email has been sent to :email, please follow the instruction of the email', 7 | '2sv' => '2-Step Verification', 8 | 'continue' => 'Continue', 9 | 'confirm' => 'Confirm', 10 | 'resend_email' => 'Resend Email', 11 | 'invalid_code' => 'Invalid 2-FA code', 12 | '2fa-code' => '2-FA Code', 13 | 'resend_success' => 'Resend Success', 14 | 'use_another_ac' => 'Use Another Account', 15 | ]; 16 | -------------------------------------------------------------------------------- /resources/views/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solutionforest/filament-email-2fa/8c6adbc025c3d9a32ee77cf54a23ba8bd429d253/resources/views/.gitkeep -------------------------------------------------------------------------------- /resources/views/email-sent.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | @lang('filament-email-2fa::filament-email-2fa.email_sent', ['email' => $this->getUser()->email]) 5 | 6 | @if (session()->has('resent-success')) 7 | 8 | {{ session('resent-success') }} 9 | 10 | @endif 11 | 12 | {{ $this->form }} 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /resources/views/email-template.blade.php: -------------------------------------------------------------------------------- 1 |

Hi {{ $name }},

2 |

3 | Your 2-fa code is : {{ $code }} 4 |

5 | -------------------------------------------------------------------------------- /resources/views/login-success.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | @lang('filament-email-2fa::filament-email-2fa.continue') 4 | 5 | 6 | -------------------------------------------------------------------------------- /resources/views/simple-layout.blade.php: -------------------------------------------------------------------------------- 1 | @php 2 | use Filament\Support\Enums\MaxWidth; 3 | @endphp 4 | 5 | 6 | @props([ 7 | 'after' => null, 8 | 'heading' => null, 9 | 'subheading' => null, 10 | ]) 11 | 12 |
13 |
16 |
'sm:max-w-xs', 21 | MaxWidth::Small, 'sm' => 'sm:max-w-sm', 22 | MaxWidth::Medium, 'md' => 'sm:max-w-md', 23 | MaxWidth::ExtraLarge, 'xl' => 'sm:max-w-xl', 24 | MaxWidth::TwoExtraLarge, '2xl' => 'sm:max-w-2xl', 25 | MaxWidth::ThreeExtraLarge, '3xl' => 'sm:max-w-3xl', 26 | MaxWidth::FourExtraLarge, '4xl' => 'sm:max-w-4xl', 27 | MaxWidth::FiveExtraLarge, '5xl' => 'sm:max-w-5xl', 28 | MaxWidth::SixExtraLarge, '6xl' => 'sm:max-w-6xl', 29 | MaxWidth::SevenExtraLarge, '7xl' => 'sm:max-w-7xl', 30 | default => 'sm:max-w-lg', 31 | }, 32 | ]) 33 | > 34 | {{ $slot }} 35 |
36 |
37 | 38 | {{ \Filament\Support\Facades\FilamentView::renderHook(\Filament\View\PanelsRenderHook::FOOTER, scopes: $livewire->getRenderHookScopes()) }} 39 |
40 |
41 | -------------------------------------------------------------------------------- /src/Commands/FilamentEmail2faCommand.php: -------------------------------------------------------------------------------- 1 | comment('All done'); 16 | 17 | return self::SUCCESS; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Exceptions/InvalidTwoFACodeException.php: -------------------------------------------------------------------------------- 1 | message = $message ?? __('filament-email-2fa::filament-email-2fa.invalid_code'); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Facades/FilamentEmail2fa.php: -------------------------------------------------------------------------------- 1 | pages([ 24 | $fapage, 25 | $login_success_page, 26 | ]) 27 | ->authMiddleware([ 28 | IsTwoFAVerified::class, 29 | ]); 30 | 31 | } 32 | 33 | public function boot(Panel $panel): void {} 34 | 35 | public static function make(): static 36 | { 37 | return app(static::class); 38 | } 39 | 40 | public static function get(): static 41 | { 42 | /** @var static $plugin */ 43 | $plugin = filament(app(static::class)->getId()); 44 | 45 | return $plugin; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/FilamentEmail2faServiceProvider.php: -------------------------------------------------------------------------------- 1 | name(static::$name) 35 | ->hasCommands($this->getCommands()) 36 | ->hasInstallCommand(function (InstallCommand $command) { 37 | $command 38 | ->publishConfigFile() 39 | ->publishMigrations() 40 | ->publish('lang') 41 | ->askToRunMigrations() 42 | ->askToStarRepoOnGitHub('solution-forest/filament-email-2fa'); 43 | }); 44 | 45 | $configFileName = $package->shortName(); 46 | 47 | if (file_exists($package->basePath("/../config/{$configFileName}.php"))) { 48 | $package->hasConfigFile(); 49 | } 50 | 51 | if (file_exists($package->basePath('/../database/migrations'))) { 52 | $package->hasMigrations($this->getMigrations()); 53 | } 54 | 55 | if (file_exists($package->basePath('/../resources/lang'))) { 56 | $package->hasTranslations(); 57 | } 58 | 59 | if (file_exists($package->basePath('/../resources/views'))) { 60 | $package->hasViews(static::$viewNamespace); 61 | } 62 | 63 | } 64 | 65 | public function packageRegistered(): void {} 66 | 67 | public function packageBooted(): void 68 | { 69 | // Asset Registration 70 | FilamentAsset::register( 71 | $this->getAssets(), 72 | $this->getAssetPackageName() 73 | ); 74 | 75 | FilamentAsset::registerScriptData( 76 | $this->getScriptData(), 77 | $this->getAssetPackageName() 78 | ); 79 | 80 | // Icon Registration 81 | FilamentIcon::register($this->getIcons()); 82 | 83 | // Handle Stubs 84 | if (app()->runningInConsole()) { 85 | foreach (app(Filesystem::class)->files(__DIR__ . '/../stubs/') as $file) { 86 | $this->publishes([ 87 | $file->getRealPath() => base_path("stubs/filament-email-2fa/{$file->getFilename()}"), 88 | ], 'filament-email-2fa-stubs'); 89 | } 90 | } 91 | 92 | // Testing 93 | Testable::mixin(new TestsFilamentEmail2fa); 94 | 95 | $this->app->bind(LoginResponseContract::class, TwoFAResponse::class); 96 | 97 | $this->publishes([ 98 | __DIR__ . '/../resources/lang' => lang_path('vendor/filament-email-2fa'), 99 | ], 'filament-email-2fa-translation'); 100 | 101 | } 102 | 103 | protected function getAssetPackageName(): ?string 104 | { 105 | return 'solution-forest/filament-email-2fa'; 106 | } 107 | 108 | /** 109 | * @return array 110 | */ 111 | protected function getAssets(): array 112 | { 113 | return [ 114 | // AlpineComponent::make('filament-email-2fa', __DIR__ . '/../resources/dist/components/filament-email-2fa.js'), 115 | // Css::make('filament-email-2fa-styles', __DIR__ . '/../resources/dist/filament-email-2fa.css'), 116 | // Js::make('filament-email-2fa-scripts', __DIR__ . '/../resources/dist/filament-email-2fa.js'), 117 | ]; 118 | } 119 | 120 | /** 121 | * @return array 122 | */ 123 | protected function getCommands(): array 124 | { 125 | return [ 126 | FilamentEmail2faCommand::class, 127 | ]; 128 | } 129 | 130 | /** 131 | * @return array 132 | */ 133 | protected function getIcons(): array 134 | { 135 | return []; 136 | } 137 | 138 | /** 139 | * @return array 140 | */ 141 | protected function getRoutes(): array 142 | { 143 | return [ 144 | ]; 145 | } 146 | 147 | /** 148 | * @return array 149 | */ 150 | protected function getScriptData(): array 151 | { 152 | return []; 153 | } 154 | 155 | /** 156 | * @return array 157 | */ 158 | protected function getMigrations(): array 159 | { 160 | return [ 161 | 'create_filament-email-2fa_table', 162 | ]; 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /src/Interfaces/RequireTwoFALogin.php: -------------------------------------------------------------------------------- 1 | name = $name; 26 | $this->code = $code; 27 | } 28 | 29 | /** 30 | * Build the message. 31 | * 32 | * @return $this 33 | */ 34 | public function build() 35 | { 36 | return $this->subject(__('filament-email-2fa::filament-email-2fa.2sv')) 37 | ->view('filament-email-2fa::email-template', ['name' => $this->name, 'code' => $this->code]); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Middlewares/IsTwoFAVerified.php: -------------------------------------------------------------------------------- 1 | user(); 16 | 17 | try { 18 | $routeName = $request->route()->getName(); 19 | } catch (Exception $e) { 20 | $routeName = null; 21 | } 22 | 23 | if ($user == null || $routeName == TwoFactorAuth::getRouteName() 24 | || $routeName == Filament::getCurrentPanel()->generateRouteName('auth.email-verification.prompt') 25 | || $routeName == Filament::getCurrentPanel()->generateRouteName('auth.logout')) { 26 | return $next($request); 27 | } 28 | 29 | if ($user instanceof RequireTwoFALogin && $user->isTwoFaVerfied($request->session()->getId())) { 30 | 31 | return $next($request); 32 | 33 | } 34 | 35 | return redirect(route(TwoFactorAuth::getRouteName())); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Models/TwoFaCode.php: -------------------------------------------------------------------------------- 1 | morphTo(); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Models/TwoFaVerify.php: -------------------------------------------------------------------------------- 1 | morphTo(); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Pages/LoginSuccessPage.php: -------------------------------------------------------------------------------- 1 | user() instanceof RequireTwoFALogin) { 48 | return redirect(Filament::getUrl()); 49 | } 50 | $this->email = Filament::auth()->user()->email; 51 | } 52 | 53 | public function resend() 54 | { 55 | 56 | if ($user = $this->getUser()) { 57 | $user->send2FAEmail(); 58 | session()->flash('resent-success', __('filament-email-2fa::filament-email-2fa.resend_success')); 59 | 60 | return; 61 | } 62 | } 63 | 64 | public function logout() 65 | { 66 | Filament::auth()->logout(); 67 | 68 | session()->invalidate(); 69 | session()->regenerateToken(); 70 | 71 | return redirect()->to( 72 | Filament::hasLogin() ? Filament::getLoginUrl() : Filament::getUrl(), 73 | ); 74 | } 75 | 76 | public function getFormActions(): array 77 | { 78 | return [ 79 | Action::make('save') 80 | ->label(__('filament-email-2fa::filament-email-2fa.confirm')) 81 | ->action('save') 82 | ->keyBindings(['mod+s']), 83 | 84 | Action::make('resend') 85 | ->color('gray') 86 | ->label(__('filament-email-2fa::filament-email-2fa.resend_email')) 87 | ->action('resend') 88 | ->keyBindings(['mod+s']), 89 | 90 | Action::make('logout') 91 | ->color('gray') 92 | ->label(__('filament-email-2fa::filament-email-2fa.use_another_ac')) 93 | ->action('logout'), 94 | ]; 95 | } 96 | 97 | public function save() 98 | { 99 | 100 | $code = $this->data['2fa_code'] ?? null; 101 | 102 | try { 103 | if ($user = $this->getUser()) { 104 | $user->verify2FACode($code ?? ''); 105 | $user->twoFaVerifis()->create([ 106 | 'session_id' => request()->session()->getId(), 107 | ]); 108 | 109 | return app(LoginSuccessResponse::class); 110 | } else { 111 | throw new InvalidTwoFACodeException; 112 | } 113 | } catch (InvalidTwoFACodeException $e) { 114 | $this->addError('data.2fa_code', $e->getMessage()); 115 | 116 | return; 117 | } 118 | } 119 | 120 | public function getUser() 121 | { 122 | $guard = $this->getCurrentGuard(); 123 | $model = config("auth.providers.{$guard}.model"); 124 | 125 | $user = $model::where('email', $this->email)->first(); 126 | 127 | return $user; 128 | } 129 | 130 | public function getCurrentGuard() 131 | { 132 | return Filament::getCurrentPanel()->getAuthGuard(); 133 | } 134 | 135 | public function form(Form $form): Form 136 | { 137 | return $form; 138 | } 139 | 140 | /** 141 | * @return array 142 | */ 143 | protected function getForms(): array 144 | { 145 | return [ 146 | 'form' => $this->form( 147 | $this->makeForm() 148 | ->schema([ 149 | TextInput::make('2fa_code')->label(__('filament-email-2fa::filament-email-2fa.2fa-code')), 150 | ]) 151 | ->statePath('data'), 152 | ), 153 | ]; 154 | } 155 | 156 | public function hasFullWidthFormActions(): bool 157 | { 158 | return false; 159 | } 160 | 161 | public function getFormActionsAlignment(): string | Alignment 162 | { 163 | return Alignment::Start; 164 | } 165 | 166 | public function getTitle(): string | Htmlable 167 | { 168 | return static::getLabel(); 169 | } 170 | 171 | public function hasLogo(): bool 172 | { 173 | return false; 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /src/Responses/LoginSuccessResponse.php: -------------------------------------------------------------------------------- 1 | intended(route(LoginSuccessPage::getRouteName())); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Responses/TwoFAResponse.php: -------------------------------------------------------------------------------- 1 | hasPlugin('filament-email-2fa')); 22 | Filament::auth()->user()->send2FAEmail(); 23 | 24 | return redirect()->intended(route(TwoFactorAuth::getRouteName())); 25 | 26 | return redirect()->intended(Filament::getUrl()); 27 | 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Testing/TestsFilamentEmail2fa.php: -------------------------------------------------------------------------------- 1 | email)) 17 | ->send(new TwoFAEmail($this->name, $this->generate2FACode())); 18 | } 19 | 20 | public function twoFaCodes(): Relation 21 | { 22 | return $this->morphMany(config('filament-email-2fa.code_model'), 'user'); 23 | } 24 | 25 | public function twoFaVerifis(): Relation 26 | { 27 | return $this->morphMany(config('filament-email-2fa.verify_model'), 'user'); 28 | } 29 | 30 | public function latest_2fa_code(): Relation 31 | { 32 | return $this->morphOne(config('filament-email-2fa.code_model'), 'user')->where('expiry_at', '>=', now())->ofMany('expiry_at', 'max'); 33 | } 34 | 35 | public function generate2FACode(): string 36 | { 37 | $this->twoFaCodes()->delete(); 38 | $code = sprintf('%06d', mt_rand(1, 999999)); 39 | $this->twoFaCodes()->create([ 40 | 'code' => $code, 41 | 'expiry_at' => now()->addMinutes((int) config('filament-email-2fa.expiry_time_by_mins', 10)), 42 | ]); 43 | 44 | return $code; 45 | 46 | } 47 | 48 | public function verify2FACode(string $code) 49 | { 50 | $latestCode = $this->latest_2fa_code?->code; 51 | if ($code !== null && $code === $latestCode) { 52 | return; 53 | } 54 | 55 | throw new InvalidTwoFACodeException; 56 | } 57 | 58 | public function isTwoFaVerfied(?string $session_id = null): bool 59 | { 60 | $session_id = $session_id ?? request()->session()->getId(); 61 | 62 | return $this->twoFaVerifis()->where('session_id', $session_id)->exists(); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /stubs/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solutionforest/filament-email-2fa/8c6adbc025c3d9a32ee77cf54a23ba8bd429d253/stubs/.gitkeep --------------------------------------------------------------------------------