diff --git a/composer.json b/composer.json
index ac5dc42e..cc413acb 100644
--- a/composer.json
+++ b/composer.json
@@ -35,7 +35,8 @@
"spatie/laravel-permission": "^6.3",
"laravel/framework": "^10.0",
"rawilk/filament-password-input": "^2.0",
- "ralphjsmit/laravel-filament-seo": "^1.3"
+ "ralphjsmit/laravel-filament-seo": "^1.3",
+ "laravel/fortify": "^1.21"
},
"require-dev": {
"laravel/sanctum": "^3.3",
diff --git a/composer.lock b/composer.lock
index d3bdde94..f6e6c96c 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "56cfa7f5644501e9357d7f7c9978c15f",
+ "content-hash": "b7b056291e8854fafa84605a4c6683a6",
"packages": [
{
"name": "anourvalar/eloquent-serialize",
@@ -155,6 +155,60 @@
],
"time": "2024-02-28T21:33:43+00:00"
},
+ {
+ "name": "bacon/bacon-qr-code",
+ "version": "2.0.8",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/Bacon/BaconQrCode.git",
+ "reference": "8674e51bb65af933a5ffaf1c308a660387c35c22"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/Bacon/BaconQrCode/zipball/8674e51bb65af933a5ffaf1c308a660387c35c22",
+ "reference": "8674e51bb65af933a5ffaf1c308a660387c35c22",
+ "shasum": ""
+ },
+ "require": {
+ "dasprid/enum": "^1.0.3",
+ "ext-iconv": "*",
+ "php": "^7.1 || ^8.0"
+ },
+ "require-dev": {
+ "phly/keep-a-changelog": "^2.1",
+ "phpunit/phpunit": "^7 | ^8 | ^9",
+ "spatie/phpunit-snapshot-assertions": "^4.2.9",
+ "squizlabs/php_codesniffer": "^3.4"
+ },
+ "suggest": {
+ "ext-imagick": "to generate QR code images"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "BaconQrCode\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-2-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Ben Scholzen 'DASPRiD'",
+ "email": "mail@dasprids.de",
+ "homepage": "https://dasprids.de/",
+ "role": "Developer"
+ }
+ ],
+ "description": "BaconQrCode is a QR code generator for PHP.",
+ "homepage": "https://github.com/Bacon/BaconQrCode",
+ "support": {
+ "issues": "https://github.com/Bacon/BaconQrCode/issues",
+ "source": "https://github.com/Bacon/BaconQrCode/tree/2.0.8"
+ },
+ "time": "2022-12-07T17:46:57+00:00"
+ },
{
"name": "blade-ui-kit/blade-heroicons",
"version": "2.3.0",
@@ -534,6 +588,56 @@
],
"time": "2024-01-21T14:53:34+00:00"
},
+ {
+ "name": "dasprid/enum",
+ "version": "1.0.5",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/DASPRiD/Enum.git",
+ "reference": "6faf451159fb8ba4126b925ed2d78acfce0dc016"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/DASPRiD/Enum/zipball/6faf451159fb8ba4126b925ed2d78acfce0dc016",
+ "reference": "6faf451159fb8ba4126b925ed2d78acfce0dc016",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.1 <9.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^7 | ^8 | ^9",
+ "squizlabs/php_codesniffer": "*"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "DASPRiD\\Enum\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-2-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Ben Scholzen 'DASPRiD'",
+ "email": "mail@dasprids.de",
+ "homepage": "https://dasprids.de/",
+ "role": "Developer"
+ }
+ ],
+ "description": "PHP 7.1 enum implementation",
+ "keywords": [
+ "enum",
+ "map"
+ ],
+ "support": {
+ "issues": "https://github.com/DASPRiD/Enum/issues",
+ "source": "https://github.com/DASPRiD/Enum/tree/1.0.5"
+ },
+ "time": "2023-08-25T16:18:39+00:00"
+ },
{
"name": "dflydev/dot-access-data",
"version": "v3.0.2",
@@ -2332,6 +2436,71 @@
},
"time": "2024-02-13T15:40:14+00:00"
},
+ {
+ "name": "laravel/fortify",
+ "version": "v1.21.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/laravel/fortify.git",
+ "reference": "405388fd399264715573e23ed2f368fbce426da3"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/laravel/fortify/zipball/405388fd399264715573e23ed2f368fbce426da3",
+ "reference": "405388fd399264715573e23ed2f368fbce426da3",
+ "shasum": ""
+ },
+ "require": {
+ "bacon/bacon-qr-code": "^2.0",
+ "ext-json": "*",
+ "illuminate/support": "^10.0|^11.0",
+ "php": "^8.1",
+ "pragmarx/google2fa": "^8.0",
+ "symfony/console": "^6.0|^7.0"
+ },
+ "require-dev": {
+ "mockery/mockery": "^1.0",
+ "orchestra/testbench": "^8.16|^9.0",
+ "phpstan/phpstan": "^1.10",
+ "phpunit/phpunit": "^10.4"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.x-dev"
+ },
+ "laravel": {
+ "providers": [
+ "Laravel\\Fortify\\FortifyServiceProvider"
+ ]
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Laravel\\Fortify\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Taylor Otwell",
+ "email": "taylor@laravel.com"
+ }
+ ],
+ "description": "Backend controllers and scaffolding for Laravel authentication.",
+ "keywords": [
+ "auth",
+ "laravel"
+ ],
+ "support": {
+ "issues": "https://github.com/laravel/fortify/issues",
+ "source": "https://github.com/laravel/fortify"
+ },
+ "time": "2024-03-19T20:08:25+00:00"
+ },
{
"name": "laravel/framework",
"version": "v10.47.0",
@@ -4201,6 +4370,73 @@
],
"time": "2024-01-09T09:30:37+00:00"
},
+ {
+ "name": "paragonie/constant_time_encoding",
+ "version": "v2.6.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/paragonie/constant_time_encoding.git",
+ "reference": "58c3f47f650c94ec05a151692652a868995d2938"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/paragonie/constant_time_encoding/zipball/58c3f47f650c94ec05a151692652a868995d2938",
+ "reference": "58c3f47f650c94ec05a151692652a868995d2938",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7|^8"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^6|^7|^8|^9",
+ "vimeo/psalm": "^1|^2|^3|^4"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "ParagonIE\\ConstantTime\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Paragon Initiative Enterprises",
+ "email": "security@paragonie.com",
+ "homepage": "https://paragonie.com",
+ "role": "Maintainer"
+ },
+ {
+ "name": "Steve 'Sc00bz' Thomas",
+ "email": "steve@tobtu.com",
+ "homepage": "https://www.tobtu.com",
+ "role": "Original Developer"
+ }
+ ],
+ "description": "Constant-time Implementations of RFC 4648 Encoding (Base-64, Base-32, Base-16)",
+ "keywords": [
+ "base16",
+ "base32",
+ "base32_decode",
+ "base32_encode",
+ "base64",
+ "base64_decode",
+ "base64_encode",
+ "bin2hex",
+ "encoding",
+ "hex",
+ "hex2bin",
+ "rfc4648"
+ ],
+ "support": {
+ "email": "info@paragonie.com",
+ "issues": "https://github.com/paragonie/constant_time_encoding/issues",
+ "source": "https://github.com/paragonie/constant_time_encoding"
+ },
+ "time": "2022-06-14T06:56:20+00:00"
+ },
{
"name": "parsecsv/php-parsecsv",
"version": "1.3.2",
@@ -4340,6 +4576,58 @@
],
"time": "2023-11-12T21:59:55+00:00"
},
+ {
+ "name": "pragmarx/google2fa",
+ "version": "v8.0.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/antonioribeiro/google2fa.git",
+ "reference": "80c3d801b31fe165f8fe99ea085e0a37834e1be3"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/antonioribeiro/google2fa/zipball/80c3d801b31fe165f8fe99ea085e0a37834e1be3",
+ "reference": "80c3d801b31fe165f8fe99ea085e0a37834e1be3",
+ "shasum": ""
+ },
+ "require": {
+ "paragonie/constant_time_encoding": "^1.0|^2.0",
+ "php": "^7.1|^8.0"
+ },
+ "require-dev": {
+ "phpstan/phpstan": "^0.12.18",
+ "phpunit/phpunit": "^7.5.15|^8.5|^9.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "PragmaRX\\Google2FA\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Antonio Carlos Ribeiro",
+ "email": "acr@antoniocarlosribeiro.com",
+ "role": "Creator & Designer"
+ }
+ ],
+ "description": "A One Time Password Authentication package, compatible with Google Authenticator.",
+ "keywords": [
+ "2fa",
+ "Authentication",
+ "Two Factor Authentication",
+ "google2fa"
+ ],
+ "support": {
+ "issues": "https://github.com/antonioribeiro/google2fa/issues",
+ "source": "https://github.com/antonioribeiro/google2fa/tree/v8.0.1"
+ },
+ "time": "2022-06-13T21:57:56+00:00"
+ },
{
"name": "psr/cache",
"version": "3.0.0",
@@ -13156,5 +13444,5 @@
"prefer-lowest": false,
"platform": [],
"platform-dev": [],
- "plugin-api-version": "2.6.0"
+ "plugin-api-version": "2.3.0"
}
diff --git a/database/migrations/2024_04_12_000000_create_user_logins_table.php b/database/migrations/2024_04_12_000000_create_user_logins_table.php
new file mode 100644
index 00000000..c842e647
--- /dev/null
+++ b/database/migrations/2024_04_12_000000_create_user_logins_table.php
@@ -0,0 +1,32 @@
+id();
+ /**
+ * We are not sure if they may use UUID or auto-incrementing values
+ * We also cannot guarantee that the auth model will be called "User"
+ * so we cannot put a foreign key to it
+ */
+ $table->string('user_id');
+ $table->timestamps();
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::dropIfExists('user_logins');
+ }
+};
diff --git a/src/Actions/Fortify/ResetUserPassword.php b/src/Actions/Fortify/ResetUserPassword.php
new file mode 100644
index 00000000..96330303
--- /dev/null
+++ b/src/Actions/Fortify/ResetUserPassword.php
@@ -0,0 +1,28 @@
+ 'required|string|confirmed|min:16',
+ ])->validate();
+
+ $user->forceFill([
+ 'password' => Hash::make($input['password']),
+ ])->save();
+ }
+}
diff --git a/src/Commands/AddUserConcerns.php b/src/Commands/AddUserConcerns.php
index 4ae7c3b6..ed159b30 100644
--- a/src/Commands/AddUserConcerns.php
+++ b/src/Commands/AddUserConcerns.php
@@ -7,6 +7,7 @@
use Illuminate\Support\Facades\File;
use ReflectionClass;
use Spatie\Permission\Traits\HasRoles;
+use Portable\FilaCms\Contracts\HasLogin;
class AddUserConcerns extends Command
{
@@ -24,6 +25,7 @@ public function handle()
$traits = [
HasRoles::class,
+ HasLogin::class,
];
$interfaces = [
diff --git a/src/Commands/InstallCommand.php b/src/Commands/InstallCommand.php
index f431c1a4..c7ffe566 100644
--- a/src/Commands/InstallCommand.php
+++ b/src/Commands/InstallCommand.php
@@ -21,6 +21,8 @@ public function handle()
$this->info('Installed Filament Base. Installing Spatie Permissions');
+ $this->call('fortify:install');
+
$this->call('vendor:publish', ['--provider' => "Spatie\Permission\PermissionServiceProvider"]);
$this->call('vendor:publish', ['--provider' => "Venturecraft\Revisionable\RevisionableServiceProvider"]);
$this->call('vendor:publish', ['--tag' => "seo-migrations"]);
diff --git a/src/Contracts/HasLogin.php b/src/Contracts/HasLogin.php
new file mode 100644
index 00000000..dbd5c74f
--- /dev/null
+++ b/src/Contracts/HasLogin.php
@@ -0,0 +1,13 @@
+hasMany(UserLogin::class, 'user_id');
+ }
+}
diff --git a/src/Filament/Resources/UserResource.php b/src/Filament/Resources/UserResource.php
index b0d8db79..2fcea926 100644
--- a/src/Filament/Resources/UserResource.php
+++ b/src/Filament/Resources/UserResource.php
@@ -4,9 +4,14 @@
use Filament\Forms;
use Filament\Forms\Form;
+use Filament\Notifications\Notification;
use Filament\Tables;
+use Filament\Tables\Actions\Action;
use Filament\Tables\Table;
+use Illuminate\Database\Eloquent\Model;
+use Password as PasswordReset;
use Portable\FilaCms\Filament\Resources\UserResource\Pages;
+use Portable\FilaCms\Filament\Resources\UserResource\RelationManagers;
use Portable\FilaCms\Filament\Traits\IsProtectedResource;
use Rawilk\FilamentPasswordInput\Password;
@@ -31,12 +36,12 @@ public static function form(Form $form): Form
Forms\Components\TextInput::make('email')
->email()
->prefixIcon('heroicon-m-envelope')
+ ->unique(ignoreRecord:true)
->required(),
Password::make('password')
->regeneratePassword(color: 'warning')
->copyable(color: 'info')
- ->newPasswordLength(16)
- ->required(),
+ ->newPasswordLength(16),
Forms\Components\Select::make('roles')
->relationship('roles', 'name')
->multiple()
@@ -44,9 +49,15 @@ public static function form(Form $form): Form
]);
}
+ public static function getModel(): string
+ {
+ return config('auth.providers.users.model');
+ }
+
public static function table(Table $table): Table
{
static::$model = config('auth.providers.users.model');
+
return $table
->columns(static::getTableColumns())
->filters([
@@ -54,6 +65,17 @@ public static function table(Table $table): Table
])
->actions([
Tables\Actions\EditAction::make(),
+ Action::make('send_reset_link')
+ ->label('Send Password Reset')
+ ->icon('heroicon-s-inbox')
+ ->action(function (Model $user) {
+ PasswordReset::broker()->sendResetLink(['email' => $user->email]);
+ Notification::make()
+ ->title('Reset Link Sent')
+ ->body('Password reset link has been sent to ' . $user->email)
+ ->success()
+ ->send();
+ })
])
->bulkActions([
Tables\Actions\BulkActionGroup::make([
@@ -65,7 +87,7 @@ public static function table(Table $table): Table
public static function getRelations(): array
{
return [
- //
+ RelationManagers\UserLoginsRelationManager::class,
];
}
diff --git a/src/Filament/Resources/UserResource/Pages/EditUser.php b/src/Filament/Resources/UserResource/Pages/EditUser.php
index b294081e..986d7f9e 100644
--- a/src/Filament/Resources/UserResource/Pages/EditUser.php
+++ b/src/Filament/Resources/UserResource/Pages/EditUser.php
@@ -16,4 +16,13 @@ protected function getHeaderActions(): array
Actions\DeleteAction::make(),
];
}
+
+ public function mutateFormDataBeforeSave(array $data): array
+ {
+ if((isset($data['password']) && ($data['password'] === '')) || is_null($data['password'])) {
+ unset($data['password']);
+ }
+
+ return $data;
+ }
}
diff --git a/src/Filament/Resources/UserResource/RelationManagers/UserLoginsRelationManager.php b/src/Filament/Resources/UserResource/RelationManagers/UserLoginsRelationManager.php
new file mode 100644
index 00000000..b9272d56
--- /dev/null
+++ b/src/Filament/Resources/UserResource/RelationManagers/UserLoginsRelationManager.php
@@ -0,0 +1,35 @@
+schema([
+ Forms\Components\TextInput::make('name')
+ ->required()
+ ->maxLength(255),
+ ]);
+ }
+
+ public function table(Table $table): Table
+ {
+ return $table
+ ->recordTitleAttribute('created_at')
+ ->columns([
+ Tables\Columns\ViewColumn::make('created_at')
+ ->label('Login Time')
+ ->view('fila-cms::tables.columns.created_at'),
+ ]);
+ }
+}
diff --git a/src/Listeners/AuthenticationListener.php b/src/Listeners/AuthenticationListener.php
new file mode 100644
index 00000000..6e2415e0
--- /dev/null
+++ b/src/Listeners/AuthenticationListener.php
@@ -0,0 +1,16 @@
+ $event->user->id,
+ ]);
+ }
+}
diff --git a/src/Models/UserLogin.php b/src/Models/UserLogin.php
new file mode 100644
index 00000000..c3a64240
--- /dev/null
+++ b/src/Models/UserLogin.php
@@ -0,0 +1,19 @@
+belongsTo($userModelClass, 'user_id');
+ }
+}
diff --git a/src/Providers/FilaCmsServiceProvider.php b/src/Providers/FilaCmsServiceProvider.php
index da3b2034..969c7684 100644
--- a/src/Providers/FilaCmsServiceProvider.php
+++ b/src/Providers/FilaCmsServiceProvider.php
@@ -2,14 +2,19 @@
namespace Portable\FilaCms\Providers;
+use FilamentTiptapEditor\TiptapEditor;
+use Illuminate\Auth\Events\Login;
use Illuminate\Foundation\AliasLoader;
use Illuminate\Support\Facades\Blade;
+use Illuminate\Support\Facades\Event;
use Illuminate\Support\ServiceProvider;
+use Laravel\Fortify\Fortify;
use Livewire\Livewire;
+use Portable\FilaCms\Actions\Fortify\ResetUserPassword;
use Portable\FilaCms\Facades\FilaCms as FacadesFilaCms;
use Portable\FilaCms\FilaCms;
-use FilamentTiptapEditor\TiptapEditor;
use Portable\FilaCms\Filament\Blocks\RelatedResourceBlock;
+use Portable\FilaCms\Listeners\AuthenticationListener;
class FilaCmsServiceProvider extends ServiceProvider
{
@@ -42,6 +47,18 @@ public function boot()
Livewire::component('portable.fila-cms.livewire.content-resource-list', \Portable\FilaCms\Livewire\ContentResourceList::class);
Livewire::component('portable.fila-cms.livewire.content-resource-show', \Portable\FilaCms\Livewire\ContentResourceShow::class);
Blade::componentNamespace('Portable\\FilaCms\\Views\\Components', 'fila-cms');
+
+ Event::listen(Login::class, AuthenticationListener::class);
+
+ Fortify::resetPasswordView(function () {
+ return view('fila-cms::auth.reset-password');
+ });
+
+ Fortify::loginView(function () {
+ return redirect(route('filament.admin.auth.login'));
+ });
+
+ Fortify::resetUserPasswordsUsing(ResetUserPassword::class);
}
public function register()
diff --git a/testbench.yaml b/testbench.yaml
index c8998749..65e7ba04 100644
--- a/testbench.yaml
+++ b/testbench.yaml
@@ -14,6 +14,7 @@ providers:
- Spatie\Permission\PermissionServiceProvider
- Laravel\Sanctum\SanctumServiceProvider
- Portable\FilaCms\Tests\Providers\WorkbenchServiceProvider
+ - Laravel\Fortify\FortifyServiceProvider
migrations:
- workbench/database/migrations
diff --git a/tests/Listeners/ServeCommandStartedListener.php b/tests/Listeners/ServeCommandStartedListener.php
index ea97c555..2f03b58e 100644
--- a/tests/Listeners/ServeCommandStartedListener.php
+++ b/tests/Listeners/ServeCommandStartedListener.php
@@ -2,7 +2,6 @@
namespace Portable\FilaCms\Tests\Listeners;
-use Illuminate\Foundation\Auth\User;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Hash;
@@ -11,6 +10,10 @@ class ServeCommandStartedListener
{
public function handle(): void
{
+ // Create the user model stub
+ Artisan::call('make:user-model');
+ config(['auth.providers.users.model', 'Workbench\App\Models\User']);
+
Artisan::call('package:create-sqlite-db');
Artisan::call('migrate:fresh');
@@ -26,7 +29,7 @@ public function handle(): void
File::delete(resource_path('css/filament/admin/tailwind.config.js'));
File::delete(resource_path('css/filament/admin/theme.css'));
- Artisan::call('fila-cms:install', ['--publish-config' => true,'--run-migrations' => true,'--add-user-traits' => true]);
+ Artisan::call('fila-cms:install', ['--publish-config' => true,'--run-migrations' => true,'--add-user-traits' => false]);
// Ensure there's an admin user
$userModel = config('auth.providers.users.model');
diff --git a/tests/Providers/WorkbenchServiceProvider.php b/tests/Providers/WorkbenchServiceProvider.php
index 82d68542..056697c5 100644
--- a/tests/Providers/WorkbenchServiceProvider.php
+++ b/tests/Providers/WorkbenchServiceProvider.php
@@ -23,6 +23,9 @@ class WorkbenchServiceProvider extends EventServiceProvider
public function register(): void
{
parent::register();
+ if(class_exists('Workbench\App\Models\User')) {
+ config(['auth.providers.users.model' => 'Workbench\App\Models\User']);
+ }
}
/**
diff --git a/tests/User.php b/tests/User.php
index 7c903500..66c613b0 100644
--- a/tests/User.php
+++ b/tests/User.php
@@ -8,6 +8,7 @@
class User extends Authenticatable implements \Filament\Models\Contracts\FilamentUser
{
+ use \Portable\FilaCms\Contracts\HasLogin;
use \Spatie\Permission\Traits\HasRoles;
use HasFactory;
diff --git a/views/auth/reset-password.blade.php b/views/auth/reset-password.blade.php
new file mode 100644
index 00000000..2d9b760a
--- /dev/null
+++ b/views/auth/reset-password.blade.php
@@ -0,0 +1,45 @@
+