├── .github ├── FUNDING.yml └── workflows │ └── release.yml ├── .gitignore ├── .releaserc ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── UPGRADE.md ├── composer.json ├── config └── filament-authentication.php ├── database └── migrations │ ├── create_filament_authentication_tables.php.stub │ ├── create_filament_password_renew_table.php.stub │ └── tracks_filament_password_hashes.php.stub ├── docs └── .gitkeep ├── larastan └── Facades.stub ├── phpcs.xml ├── phpstan-baseline.neon ├── phpstan.neon.dist ├── phpunit.xml ├── pint.json ├── resources ├── lang │ ├── en │ │ └── filament-authentication.php │ ├── ja │ │ └── filament-authentication.php │ ├── pt_BR │ │ └── filament-authentication.php │ └── vi │ │ └── filament-authentication.php └── views │ ├── components │ └── banner.blade.php │ ├── impersonating-banner.blade.php │ └── pages │ └── auth │ └── renew-password.blade.php ├── routes └── web.php ├── src ├── .DS_Store ├── Actions │ └── ImpersonateLink.php ├── Commands │ ├── InstallCommand.php │ └── UpdateUserPasswordToUpdatedCommand.php ├── Events │ ├── UserCreated.php │ └── UserUpdated.php ├── FilamentAuthentication.php ├── FilamentAuthenticationProvider.php ├── Http │ └── Middleware │ │ ├── ImpersonatingMiddleware.php │ │ └── RenewPasswordMiddleware.php ├── Models │ ├── AuthenticationLog.php │ └── PasswordRenewLog.php ├── Pages │ └── Auth │ │ └── RenewPassword.php ├── Resources │ ├── AuthenticationLogResource.php │ ├── AuthenticationLogResource │ │ └── Pages │ │ │ └── ListAuthenticationLogs.php │ ├── PermissionResource.php │ ├── PermissionResource │ │ ├── Pages │ │ │ ├── CreatePermission.php │ │ │ ├── EditPermission.php │ │ │ ├── ListPermissions.php │ │ │ └── ViewPermission.php │ │ └── RelationManager │ │ │ └── RoleRelationManager.php │ ├── RoleResource.php │ ├── RoleResource │ │ ├── Pages │ │ │ ├── CreateRole.php │ │ │ ├── EditRole.php │ │ │ ├── ListRoles.php │ │ │ └── ViewRole.php │ │ └── RelationManager │ │ │ ├── PermissionRelationManager.php │ │ │ └── UserRelationManager.php │ ├── UserResource.php │ └── UserResource │ │ ├── Pages │ │ ├── CreateUser.php │ │ ├── EditUser.php │ │ ├── ListUsers.php │ │ └── ViewUser.php │ │ └── RelationManager │ │ ├── AuthenticationLogsRelationManager.php │ │ └── RoleRelationManager.php ├── Rules │ └── PreventPasswordReuseRule.php ├── Subscribers │ └── AuthenticationLoggingSubscriber.php ├── Traits │ ├── CanRenewPassword.php │ ├── LogsAuthentication.php │ ├── PagePolicyTrait.php │ └── SettingsPagePolicyTrait.php └── Widgets │ └── LatestUsersWidget.php └── tests └── DefaultTest.php /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: phpsa 2 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Semantic Release 2 | 3 | on: [workflow_dispatch] 4 | 5 | jobs: 6 | release: 7 | name: release 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout 11 | uses: actions/checkout@v3 12 | 13 | - name: Semantic Release 14 | uses: cycjimmy/semantic-release-action@v3 15 | id: semantic 16 | with: 17 | branches: | 18 | [ 19 | '+([0-9])?(.{+([0-9]),x}).x', 20 | 'master', 21 | 'main', 22 | 'next', 23 | 'next-major', 24 | { 25 | name: 'beta', 26 | prerelease: true 27 | }, 28 | { 29 | name: 'alpha', 30 | prerelease: true 31 | } 32 | ] 33 | extra_plugins: | 34 | @semantic-release/commit-analyzer 35 | @semantic-release/git 36 | @semantic-release/changelog 37 | @semantic-release/exec 38 | env: 39 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 40 | SEMANTIC_RELEASE_PACKAGE: Filament Authentication 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | vendor/ 2 | composer.lock 3 | -------------------------------------------------------------------------------- /.releaserc: -------------------------------------------------------------------------------- 1 | plugins: 2 | - "@semantic-release/commit-analyzer" 3 | - "@semantic-release/release-notes-generator" 4 | - - "@semantic-release/exec" 5 | - verifyReleaseCmd: "echo ${nextRelease.version} > VERSION.txt" 6 | - - "@semantic-release/changelog" 7 | - changelogFile: CHANGELOG.md 8 | - - "@semantic-release/github" 9 | - - "@semantic-release/git" 10 | - assets: 11 | - CHANGELOG.md 12 | message: "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}" 13 | branches: 14 | - "master" 15 | - "main" 16 | - "next" 17 | - "next-major" 18 | - "+([0-9])?(.{+([0-9]),x}).x" 19 | - name: "beta" 20 | prerelease: true 21 | - name: "alpha" 22 | prerelease: true 23 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [2.4.2](https://github.com/phpsa/filament-authentication/compare/v2.4.1...v2.4.2) (2023-05-15) 2 | 3 | 4 | ### Bug Fixes 5 | 6 | * missing ; on widget ([4c4d91d](https://github.com/phpsa/filament-authentication/commit/4c4d91d1cff87d00009c3665175022128906e93b)) 7 | 8 | ## [2.4.1](https://github.com/phpsa/filament-authentication/compare/v2.4.0...v2.4.1) (2023-05-15) 9 | 10 | 11 | ### Bug Fixes 12 | 13 | * Add password type to input password ([#32](https://github.com/phpsa/filament-authentication/issues/32)) ([a5bd787](https://github.com/phpsa/filament-authentication/commit/a5bd78753d650a9772935fd6c3fbb429cd4e769e)) 14 | * **widgets:** update LatestUsers query to use the configured resource ([#34](https://github.com/phpsa/filament-authentication/issues/34)) ([a09a2a8](https://github.com/phpsa/filament-authentication/commit/a09a2a80427effbe3e447e2073413c3eb8f6adcd)) 15 | 16 | # [2.4.0](https://github.com/phpsa/filament-authentication/compare/v2.3.0...v2.4.0) (2023-02-06) 17 | 18 | 19 | ### Features 20 | 21 | * Portuguese translations ([#30](https://github.com/phpsa/filament-authentication/issues/30)) ([93f838e](https://github.com/phpsa/filament-authentication/commit/93f838e7c8dcd24d0e0f77f9c8b997addace98ea)) 22 | 23 | # [2.3.0](https://github.com/phpsa/filament-authentication/compare/v2.2.1...v2.3.0) (2022-11-23) 24 | 25 | 26 | ### Features 27 | 28 | * Attach user to role ([62d7c19](https://github.com/phpsa/filament-authentication/commit/62d7c19c394a06ad5341c327952e3468a6eb7798)) 29 | 30 | ## [2.2.1](https://github.com/phpsa/filament-authentication/compare/v2.2.0...v2.2.1) (2022-09-11) 31 | 32 | 33 | ### Bug Fixes 34 | 35 | * Clear permissions cache on attach/detach ([#22](https://github.com/phpsa/filament-authentication/issues/22)) ([50952ca](https://github.com/phpsa/filament-authentication/commit/50952ca04144e85f175f167211e29f48dc7df454)) 36 | * Make Role and Permission Name and Guard Name required for form ([#23](https://github.com/phpsa/filament-authentication/issues/23)) ([0f948e9](https://github.com/phpsa/filament-authentication/commit/0f948e93cd7b939221a6b0fd1e76cdd021129207)) 37 | 38 | # [2.2.0](https://github.com/phpsa/filament-authentication/compare/v2.1.2...v2.2.0) (2022-08-11) 39 | 40 | 41 | ### Features 42 | 43 | * Vietnamese translations ([#19](https://github.com/phpsa/filament-authentication/issues/19)) ([616bb75](https://github.com/phpsa/filament-authentication/commit/616bb754e6bf8ba99df6b04df941e2ee26294d5b)) 44 | 45 | ## [2.2.0] (to be released) 46 | 47 | ### Features 48 | 49 | * [2.2.0] introduce ability to override User, Role and Permission Model (instead of just RoleResource and PermissionResource) 50 | 51 | ## [2.1.2](https://github.com/phpsa/filament-authentication/compare/v2.1.1...v2.1.2) (2022-06-16) 52 | 53 | 54 | ### Bug Fixes 55 | 56 | * tableActions removed ([91b1526](https://github.com/phpsa/filament-authentication/commit/91b152674ef6f7a0b1d658d5efc40bd08546cd61)) 57 | 58 | ## [2.1.1](https://github.com/phpsa/filament-authentication/compare/v2.1.0...v2.1.1) (2022-06-06) 59 | 60 | 61 | ### Bug Fixes 62 | 63 | * added ability to set sort order of the LatestUsers widget ([7198cff](https://github.com/phpsa/filament-authentication/commit/7198cfffbc37e788f46cde363709cf2d91332bc1)) 64 | 65 | # [2.1.0](https://github.com/phpsa/filament-authentication/compare/v2.0.0...v2.1.0) (2022-06-02) 66 | 67 | 68 | ### Bug Fixes 69 | 70 | * mount not static ([3b9e4cc](https://github.com/phpsa/filament-authentication/commit/3b9e4cc0fe9ca259135b5eee15ebfdccbe05d22e)) 71 | * use Terniary Filter ([9d9a07a](https://github.com/phpsa/filament-authentication/commit/9d9a07ad9672739c96b3128bc80649614536e824)) 72 | * use user name by default on edit ([b3d4a90](https://github.com/phpsa/filament-authentication/commit/b3d4a90356622901ff71bf3c95381fe9df07fc8b)) 73 | 74 | 75 | ### Features 76 | 77 | * added event triggers for create and update ([59dd8ef](https://github.com/phpsa/filament-authentication/commit/59dd8ef6f62591b2e6a8f89e425e5f54e340df71)) 78 | 79 | # [2.1.0-beta.2](https://github.com/phpsa/filament-authentication/compare/v2.1.0-beta.1...v2.1.0-beta.2) (2022-05-31) 80 | 81 | 82 | ### Bug Fixes 83 | 84 | * use Terniary Filter ([9d9a07a](https://github.com/phpsa/filament-authentication/commit/9d9a07ad9672739c96b3128bc80649614536e824)) 85 | * use user name by default on edit ([b3d4a90](https://github.com/phpsa/filament-authentication/commit/b3d4a90356622901ff71bf3c95381fe9df07fc8b)) 86 | 87 | # [2.1.0-beta.1](https://github.com/phpsa/filament-authentication/compare/v2.0.0...v2.1.0-beta.1) (2022-05-30) 88 | 89 | 90 | ### Bug Fixes 91 | 92 | * mount not static ([3b9e4cc](https://github.com/phpsa/filament-authentication/commit/3b9e4cc0fe9ca259135b5eee15ebfdccbe05d22e)) 93 | 94 | 95 | ### Features 96 | 97 | * added event triggers for create and update ([59dd8ef](https://github.com/phpsa/filament-authentication/commit/59dd8ef6f62591b2e6a8f89e425e5f54e340df71)) 98 | 99 | # [2.0.0](https://github.com/phpsa/filament-authentication/compare/v1.1.2...v2.0.0) (2022-05-27) 100 | 101 | 102 | ### Bug Fixes 103 | 104 | * use Filament::makeTableAction for table action ([864610a](https://github.com/phpsa/filament-authentication/commit/864610acaf9d05498d4c430c51deb50027bc1fc1)) 105 | 106 | 107 | ### Features 108 | 109 | * Timezone display set to use Filament Core system ([f5d5507](https://github.com/phpsa/filament-authentication/commit/f5d550710ff63b018c660a6b5be3262de469318b)) 110 | 111 | 112 | ### BREAKING CHANGES 113 | 114 | * Visual display of dates will need to be updated 115 | 116 | ## [1.1.2](https://github.com/phpsa/filament-authentication/compare/v1.1.1...v1.1.2) (2022-05-06) 117 | 118 | 119 | ### Bug Fixes 120 | 121 | * spelling issue in config file ([4160da9](https://github.com/phpsa/filament-authentication/commit/4160da954fa0163653560abd3824904a8a426d06)) 122 | 123 | ## [1.1.1](https://github.com/phpsa/filament-authentication/compare/v1.1.0...v1.1.1) (2022-05-03) 124 | 125 | 126 | ### Bug Fixes 127 | 128 | * code quality updates ([ce9c8d1](https://github.com/phpsa/filament-authentication/commit/ce9c8d113ab1c3769d63b59b544813bb107c2f1c)) 129 | * user Widget laravel 8 compatability ([f9dd25f](https://github.com/phpsa/filament-authentication/commit/f9dd25f5eff7d110367cf61c666752b49ba60c20)) 130 | 131 | # [1.1.0](https://github.com/phpsa/filament-authentication/compare/v1.0.1...v1.1.0) (2022-04-23) 132 | 133 | 134 | ### Features 135 | 136 | * allow enable / disable / config of user widget ([74329cb](https://github.com/phpsa/filament-authentication/commit/74329cb4ff4db269796a9bb8750f8d39a9452dfa)) 137 | * User Impersonation ([fb30d9e](https://github.com/phpsa/filament-authentication/commit/fb30d9e2b47d5a8f04a5ac12fa22782b51c0556e)) 138 | 139 | ## [1.0.1](https://github.com/phpsa/filament-authentication/compare/v1.0.0...v1.0.1) (2022-04-22) 140 | 141 | 142 | ### Bug Fixes 143 | 144 | * set page resources to use config value for resource ([f239ca9](https://github.com/phpsa/filament-authentication/commit/f239ca9ab732895147e1cc606780870ce8bf58df)) 145 | 146 | # 1.0.0 (2022-04-20) 147 | 148 | 149 | ### Bug Fixes 150 | 151 | * remove pagination on widget ([b8a5e99](https://github.com/phpsa/filament-authentication/commit/b8a5e9947d666715c8f82637d6b363bdbfacfca4)) 152 | 153 | 154 | ### Features 155 | 156 | * Initial Commit ([a5b7299](https://github.com/phpsa/filament-authentication/commit/a5b72991624e1049369c3ff453c14449aa391885)) 157 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright 2022 phspa 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/phpsa/filament-authentication.svg?style=flat-square)](https://packagist.org/packages/phpsa/filament-authentication) 2 | [![Semantic Release](https://github.com/phpsa/filament-authentication/actions/workflows/release.yml/badge.svg)](https://github.com/phpsa/filament-authentication/actions/workflows/release.yml) 3 | [![Total Downloads](https://img.shields.io/packagist/dt/phpsa/filament-authentication.svg?style=flat-square)](https://packagist.org/packages/phpsa/filament-authentication) 4 | 5 | # Filament User Authentication 6 | 7 | User Resource For Filament Admin along with Roles & Permissions using Spatie 8 | 9 | ## Package Installation 10 | 11 | 12 | You can install the package via composer: 13 | 14 | ```bash 15 | composer require phpsa/filament-authentication 16 | ``` 17 | and run the install command 18 | 19 | ```bash 20 | php artisan filament-authentication:install 21 | ``` 22 | this will publish the config file and migrations 23 | 24 | 25 | optionally publish views / translations 26 | ```bash 27 | artisan vendor:publish --tag=filament-authentication-views 28 | artisan vendor:publish --tag=filament-authentication-translations 29 | ``` 30 | 31 | ### Spatie Roles & Permissions 32 | If you have not yet configured this package it is automatically added by this installer, run the following steps: 33 | 34 | 1. You should publish the migration and the config/permission.php config file with: 35 | 36 | ```php 37 | php artisan vendor:publish --provider="Spatie\Permission\PermissionServiceProvider" 38 | php artisan migrate 39 | ``` 40 | 41 | 2. Add the `Spatie\Permission\Traits\HasRoles` trait to your Users model 42 | 43 | 3. Add Roles & Permissions as required 44 | 45 | For more see: https://spatie.be/docs/laravel-permission/v6/introduction 46 | 47 | 48 | ## Setup & Config 49 | 50 | in your Filament panel file you need to add the following to the Plugins section 51 | 52 | add the resources 53 | ```php 54 | public function panel(Panel $panel): Panel 55 | { 56 | return $panel 57 | ... 58 | ->plugins([ 59 | \Phpsa\FilamentAuthentication\FilamentAuthentication::make(), 60 | ]) 61 | ... 62 | ``` 63 | 64 | You can configure this via either the config file or the plugin. 65 | 66 | 67 | # Features 68 | ## 1. Widgets 69 | `LatestUsersWidget` can be added to your dashboard by adding it to your panel widgets area.. 70 | ``` 71 | LatestUsersWidget::class 72 | ``` 73 | 74 | Note that it is also attached to the UserPolicy::viewAny policy value if the policy exists 75 | 76 | 77 | ## 2. Laravel Impersonate 78 | If you have not configured this package it is automatically added by this install, run the following steps: 79 | 80 | 1. Add the trait `Lab404\Impersonate\Models\Impersonate` to your User model. 81 | 2. edit the config file and set impersonate->enabled to true 82 | 83 | ### Defining impersonation authorization 84 | 85 | By default all users can **impersonate** an user. 86 | You need to add the method `canImpersonate()` to your user model: 87 | 88 | ```php 89 | /** 90 | * @return bool 91 | */ 92 | public function canImpersonate() 93 | { 94 | // For example 95 | return $this->is_admin == 1; 96 | } 97 | ``` 98 | 99 | By default all users can **be impersonated**. 100 | You need to add the method `canBeImpersonated()` to your user model to extend this behavior: 101 | 102 | ```php 103 | /** 104 | * @return bool 105 | */ 106 | public function canBeImpersonated() 107 | { 108 | // For example 109 | return $this->can_be_impersonated == 1; 110 | } 111 | ``` 112 | 113 | **Protect From Impersonation** 114 | 115 | You can use the middleware `impersonate.protect` to protect your routes against user impersonation. 116 | This middleware can be useful when you want to protect specific pages like users subscriptions, users credit cards, ... 117 | 118 | ```php 119 | Router::get('/my-credit-card', function() { 120 | echo "Can't be accessed by an impersonator"; 121 | })->middleware('impersonate.protect'); 122 | ``` 123 | 124 | **Events** 125 | There are two events available that can be used to improve your workflow: 126 | - `TakeImpersonation` is fired when an impersonation is taken. 127 | - `LeaveImpersonation` is fired when an impersonation is leaved. 128 | 129 | Each events returns two properties `$event->impersonator` and `$event->impersonated` containing User model instance. 130 | 131 | ## 3. Password Renewal 132 | 133 | Introduced in V4.2.0 - this allows you to enforce a user to change their password every X days. 134 | 135 | Enable this & configure this as Follows: 136 | 1. add the `Phpsa\FilamentAuthentication\Traits\CanRenewPassword` trait to your user model 137 | 2. configure the options for pruning and renewal day period in the config file 138 | 3. if not published, publish migration `artisan vendor:publish --tag filament-authentication-migrations` 139 | 140 | this will force a user to update their password, note -- all existing users will initially be foreced to, this can be ignored by running the following command: 141 | 142 | From V5.0.0 - there is a new validation rule that can be added to validate that a password has not been used before. 143 | `Phpsa\FilamentAuthentication\Rules\PreventPasswordReuseRule` - this will use the value from config `filament-authentication.password_renew.prevent_password_reuse` 0 to disable, any number of previous to block out fro re-use. 144 | 145 | -- If using socialite / Filament-socialite etc, you will need to override the `public function needsRenewal(): bool` method in the trait, 146 | EG: 147 | ```php 148 | use CanRenewPassword { 149 | CanRenewPassword::needsRenewal as traitNeedsRenewal; 150 | } 151 | 152 | public function needsRenewal(): bool 153 | { 154 | if ($this->password === null && SocialiteUser::where('user_id', $this->id)->exists()) { 155 | return false; 156 | } 157 | return $this->traitNeedsRenewal(); 158 | } 159 | ``` 160 | 161 | ## Authentication Log 162 | 163 | Introduced in V4.2.0 - this allows you to log each user login attempt. 164 | 165 | Enable this & configure this as follows: 166 | 1. add the `Phpsa\FilamentAuthentication\Traits\LogsAuthentication` trait to your user model 167 | 2. configure the options for prune in the authentication_log section of the config 168 | 3. optionally enable the resource in navigation section of the config file. 169 | 4. if not published, publish migration `artisan vendor:publish --tag filament-authentication-migrations` 170 | 171 | this will now log login and logouts on the system. 172 | 173 | ## Security 174 | Roles & Permissions can be secured using Laravel Policies, 175 | create your policies and register then in the AuthServiceProvider 176 | 177 | ```php 178 | protected $policies = [ 179 | Role::class => RolePolicy::class, 180 | Permission::class => PermissionPolicy::class, 181 | CustomPage::class => CustomPagePolicy::class, 182 | SettingsPage::class => SettingsPagePolicy::class 183 | // 'App\Models\Model' => 'App\Policies\ModelPolicy', 184 | ]; 185 | ``` 186 | 187 | We have a Custom Page Trait: `Phpsa\FilamentAuthentication\Traits\PagePolicyTrait` and a Spatie Settings Page Trait `Phpsa\FilamentAuthentication\Traits\SettingsPage\PolicyTrait` that you can add to your pages / settings pages. 188 | By defining a model and mapping it with a `viewAny($user)` method you can define per policies whether or not to show the page in navigation. 189 | 190 | 191 | 192 | 193 | ## Events 194 | 195 | `Phpsa\FilamentAuthentication\Events\UserCreated` is triggered when a user is created via the Resource 196 | 197 | `Phpsa\FilamentAuthentication\Events\UserUpdated` is triggered when a user is updated via the Resource 198 | 199 | ## Future Plans 200 | * MFA Authentication 201 | * Socialite Authentication 202 | * Biometrics Athentication 203 | 204 | ## Changelog 205 | 206 | Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently. 207 | 208 | ## Credits 209 | 210 | - [Phpsa](https://github.com/phpsa) 211 | 212 | ## License 213 | 214 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 215 | -------------------------------------------------------------------------------- /UPGRADE.md: -------------------------------------------------------------------------------- 1 | # Upgrading V4 - V5 2 | 3 | Breaking Changes: 4 | - Impersonation - links now generated based on panels, so route name and path will be slightly different 5 | - Password Renew - added phash to the table to store previous passwords, this will be used to validate if a password has been used before. 6 | 7 | 8 | 9 | Upgrading V2 - V3 10 | 11 | - Widget / panels no longer auto-published 12 | - Profile screen removed in favour of Filaments default one 13 | 14 | Breaking Change V1 - V2 15 | ***** 16 | 17 | -- no longer customises the DateTime values in the user tables, makes use of Filament core verson. 18 | 19 | in your AppServiceProvider add the following to the boot method: 20 | ```php 21 | DateTimePicker::configureUsing(fn (DateTimePicker $component) => $component->timezone(config('app.user_timezone'))); 22 | TextColumn::configureUsing(fn (TextColumn $column) => $column->timezone(config('app.user_timezone'))); 23 | ``` 24 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "phpsa/filament-authentication", 3 | "description": "User & Role (via Spatie Roles/Permissions) Manager Resource For Filament Admin", 4 | "keywords": [ 5 | "laravel", 6 | "user", 7 | "cli", 8 | "resource", 9 | "ui", 10 | "filament" 11 | ], 12 | "homepage": "https://cgs4k.nz", 13 | "license": "MIT", 14 | "autoload": { 15 | "psr-4": { 16 | "Phpsa\\FilamentAuthentication\\": "src/" 17 | } 18 | }, 19 | "authors": [ 20 | { 21 | "name": "Craig G Smith", 22 | "email": "vxdhost@gmail.com" 23 | } 24 | ], 25 | "minimum-stability": "dev", 26 | "require": { 27 | "php": "^8.1", 28 | "filament/filament": "^3.0", 29 | "illuminate/support": "^9.0|^10|^11|^12.0", 30 | "lab404/laravel-impersonate": "^1.7", 31 | "spatie/laravel-package-tools": "^1.13", 32 | "spatie/laravel-permission": "^5.5|^6.0" 33 | }, 34 | "require-dev": { 35 | "laravel/pint": "^1.2", 36 | "larastan/larastan": "^3.0", 37 | "orchestra/testbench": "^7.0|^8.0|^9.0|^10.0" 38 | }, 39 | "config": { 40 | "sort-packages": true 41 | }, 42 | "extra": { 43 | "laravel": { 44 | "providers": [ 45 | "Phpsa\\FilamentAuthentication\\FilamentAuthenticationProvider" 46 | ] 47 | } 48 | }, 49 | "prefer-stable": true, 50 | "scripts": { 51 | "test": "phpunit", 52 | "phpstan": "phpstan analyse" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /config/filament-authentication.php: -------------------------------------------------------------------------------- 1 | [ 5 | 'User' => \App\Models\User::class, 6 | 'Role' => \Spatie\Permission\Models\Role::class, 7 | 'Permission' => \Spatie\Permission\Models\Permission::class, 8 | 'AuthenticationLog' => \Phpsa\FilamentAuthentication\Models\AuthenticationLog::class, 9 | 'PasswordRenewLog' => \Phpsa\FilamentAuthentication\Models\PasswordRenewLog::class, 10 | ], 11 | 'resources' => [ 12 | 'UserResource' => \Phpsa\FilamentAuthentication\Resources\UserResource::class, 13 | 'RoleResource' => \Phpsa\FilamentAuthentication\Resources\RoleResource::class, 14 | 'PermissionResource' => \Phpsa\FilamentAuthentication\Resources\PermissionResource::class, 15 | 'AuthenticationLogResource' => \Phpsa\FilamentAuthentication\Resources\AuthenticationLogResource::class, 16 | ], 17 | 'navigation' => [ 18 | 'user' => [ 19 | 'register' => true, 20 | 'sort' => 1, 21 | 'icon' => 'heroicon-o-user' 22 | ], 23 | 'role' => [ 24 | 'register' => true, 25 | 'sort' => 3, 26 | 'icon' => 'heroicon-o-user-group' 27 | ], 28 | 'permission' => [ 29 | 'register' => true, 30 | 'sort' => 4, 31 | 'icon' => 'heroicon-o-lock-closed' 32 | ], 33 | 'authentication_log' => [ 34 | 'register' => false, 35 | 'sort' => 2, 36 | 'icon' => 'heroicon-o-shield-check' 37 | ], 38 | ], 39 | 'preload_roles' => true, 40 | 'preload_permissions' => true, 41 | 'impersonate' => [ 42 | 'enabled' => false, 43 | 'guard' => 'web', 44 | 'redirect' => '/' 45 | ], 46 | 'soft_deletes' => false, 47 | 'authentication_log' => [ 48 | 'table_name' => 'authentication_log', 49 | //The database connection where the authentication_log table resides. Leave empty to use the default 50 | 'db_connection' => null, 51 | //The number of days to keep the authentication logs for. Set to 0 to keep forever 52 | // remeber to schedule 53 | //Schedule::command('model:prune')->daily(); 54 | 'prune' => 365, 55 | ], 56 | 'password_renew' => [ 57 | 'table_name' => 'password_renew_log', 58 | //The database connection where the password_logs table resides. Leave empty to use the default 59 | 'db_connection' => null, 60 | //The number of days to keep the password logs for. Set to 0 to keep forever 61 | // remeber to schedule 62 | //Schedule::command('model:prune')->daily(); 63 | 'prune' => 365, 64 | //renew password days period, 0 to disable 65 | 'renew_password_days_period' => 90, 66 | //prevent password reuse for x times, 0 to disable 67 | 'prevent_password_reuse' => 0, 68 | ], 69 | 70 | 71 | ]; 72 | -------------------------------------------------------------------------------- /database/migrations/create_filament_authentication_tables.php.stub: -------------------------------------------------------------------------------- 1 | id(); 13 | $table->morphs('authenticatable'); 14 | $table->string('ip_address', 45)->nullable(); 15 | $table->text('user_agent')->nullable(); 16 | $table->timestamp('login_at')->nullable(); 17 | $table->boolean('login_successful')->default(false); 18 | $table->timestamp('logout_at')->nullable(); 19 | $table->boolean('cleared_by_user')->default(false); 20 | $table->json('location')->nullable(); 21 | }); 22 | } 23 | 24 | public function down(): void 25 | { 26 | Schema::dropIfExists(config('filament-authentication.authentication_log.table_name')); 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /database/migrations/create_filament_password_renew_table.php.stub: -------------------------------------------------------------------------------- 1 | id(); 13 | $table->morphs('renewable'); 14 | $table->timestamps(); 15 | }); 16 | } 17 | 18 | public function down(): void 19 | { 20 | Schema::dropIfExists(config('filament-authentication.password_renew.table_name')); 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /database/migrations/tracks_filament_password_hashes.php.stub: -------------------------------------------------------------------------------- 1 | string('phash', 255)->nullable()->after('renewable_id'); 13 | }); 14 | } 15 | 16 | }; 17 | -------------------------------------------------------------------------------- /docs/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phpsa/filament-authentication/f1c48294344cdb8d36f028fd5cf54610ac34e095/docs/.gitkeep -------------------------------------------------------------------------------- /larastan/Facades.stub: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The Laravel Coding Standards 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 10 33 | 34 | 35 | 10 36 | 37 | 38 | 10 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | */_ide_helper.php 51 | */cache/* 52 | */*.js 53 | */*.css 54 | */*.xml 55 | */*.blade.php 56 | */autoload.php 57 | */storage/* 58 | */docs/* 59 | */vendor/* 60 | */migrations/* 61 | */config/* 62 | */public/index.php 63 | 64 | app 65 | database 66 | config 67 | public 68 | resources 69 | routes 70 | tests 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /phpstan-baseline.neon: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phpsa/filament-authentication/f1c48294344cdb8d36f028fd5cf54610ac34e095/phpstan-baseline.neon -------------------------------------------------------------------------------- /phpstan.neon.dist: -------------------------------------------------------------------------------- 1 | includes: 2 | - ./vendor/larastan/larastan/extension.neon 3 | - ./phpstan-baseline.neon 4 | 5 | parameters: 6 | 7 | paths: 8 | - src 9 | 10 | # The level 9 is the highest level 11 | level: 5 12 | 13 | ignoreErrors: 14 | # - '#Unsafe usage of new static#' 15 | 16 | excludePaths: 17 | # - ./*/*/FileToBeExcluded.php 18 | 19 | stubFiles: 20 | - ./larastan/Facades.stub 21 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | tests 6 | 7 | 8 | 9 | 10 | src 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /pint.json: -------------------------------------------------------------------------------- 1 | { 2 | "preset": "laravel", 3 | "exclude": ["config", "resources"] 4 | } 5 | -------------------------------------------------------------------------------- /resources/lang/en/filament-authentication.php: -------------------------------------------------------------------------------- 1 | 'Authentication', 6 | 'section.users' => 'Users', 7 | 'section.user' => 'User', 8 | 9 | 10 | /* 11 | |-------------------------------------------------------------------------- 12 | | Fields 13 | |-------------------------------------------------------------------------- 14 | */ 15 | 'field.id' => 'ID', 16 | 'field.user.name' => 'Name', 17 | 'field.user.email' => 'Email', 18 | 'field.user.roles' => 'Roles', 19 | 'field.user.verified_at' => 'Verified', 20 | 'field.user.created_at' => 'Joined', 21 | 'field.user.last_login_at' => 'Last Login', 22 | 'field.user.password' => 'Password', 23 | 'field.user.confirm_password' => 'Confirm Password', 24 | 'field.user.current_password' => 'Current Password', 25 | 26 | 'field.guard_name' => 'Guard Name', 27 | 'field.name' => 'Name', 28 | 'field.permissions' => 'Permissions', 29 | 'field.roles' => 'Roles', 30 | 31 | /* 32 | |-------------------------------------------------------------------------- 33 | | Labels 34 | |-------------------------------------------------------------------------- 35 | */ 36 | 37 | 'section.permission' => 'Permission', 38 | 'section.permissions' => 'Permissions', 39 | 'section.role' => 'Role', 40 | 'section.roles' => 'Roles', 41 | 'section.roles_and_permissions' => 'Roles and Permissions', 42 | 'section.authentication_log.label' => 'Authentication Log', 43 | 'section.authentication_log.plural-label' => 'Authentication Logs', 44 | 45 | 46 | /* 47 | |-------------------------------------------------------------------------- 48 | | Filters 49 | |-------------------------------------------------------------------------- 50 | */ 51 | 'filter.verified' => 'Email Verified', 52 | 53 | 'button.impersonate' => "Impersonate User", 54 | 'text.impersonating' => 'Impersonating User: ', 55 | 'text.impersonating.end' => ' - End Impersonation ', 56 | 57 | 'text.profile' => 'Profile', 58 | 59 | 60 | 'authentication-log.table.heading' => 'Authentication Logs', 61 | 'authentication-log.column.authenticatable' => 'User', 62 | 'authentication-log.column.ip_address' => 'IP Address', 63 | 'authentication-log.column.user_agent' => 'User Agent', 64 | 'authentication-log.column.login_at' => 'Login At', 65 | 'authentication-log.column.login_successful' => 'Login Successful', 66 | 'authentication-log.column.logout_at' => 'Logout At', 67 | 'authentication-log.column.cleared_by_user' => 'Cleared By User', 68 | 69 | 70 | 'field.user.last_password_updated' => 'Password Updated At', 71 | 72 | 'renew_notifications.title' => 'Password Renewed', 73 | 'renew_notifications.body' => 'Your password has been successfully renewed.', 74 | 75 | 'renew_page_title' => 'Renew Password', 76 | 'renew_page_heading' => 'Please renew your password.', 77 | 'form.actions.renew.label' => 'Renew Password', 78 | ]; 79 | -------------------------------------------------------------------------------- /resources/lang/ja/filament-authentication.php: -------------------------------------------------------------------------------- 1 | '認証', 6 | 'section.users' => 'ユーザー', 7 | 'section.user' => 'ユーザー', 8 | 9 | 10 | /* 11 | |-------------------------------------------------------------------------- 12 | | Fields 13 | |-------------------------------------------------------------------------- 14 | */ 15 | 'field.id' => 'ID', 16 | 'field.user.name' => '名前', 17 | 'field.user.email' => 'メールアドレス', 18 | 'field.user.roles' => 'ロール', 19 | 'field.user.verified_at' => '認証済み', 20 | 'field.user.created_at' => '参加', 21 | 'field.user.password' => 'パスワード', 22 | 'field.user.confirm_password' => 'パスワードの確認', 23 | 24 | 'field.guard_name' => 'ガード名', 25 | 'field.name' => '名称', 26 | 'field.permissions' => '権限', 27 | 'field.roles' => 'ロール', 28 | 29 | /* 30 | |-------------------------------------------------------------------------- 31 | | Labels 32 | |-------------------------------------------------------------------------- 33 | */ 34 | 35 | 'section.permission' => '権限', 36 | 'section.permissions' => '権限', 37 | 'section.role' => 'ロール', 38 | 'section.roles' => 'ロール', 39 | 'section.roles_and_permissions' => 'ロールと権限', 40 | 41 | 42 | /* 43 | |-------------------------------------------------------------------------- 44 | | Filters 45 | |-------------------------------------------------------------------------- 46 | */ 47 | 'filter.verified' => '認証済みEメール', 48 | 49 | 'button.impersonate' => "なりすましユーザー", 50 | 'text.impersonating' => 'なりすましユーザー: ', 51 | 'text.impersonating.end' => ' - なりすまし終了 ', 52 | ]; 53 | -------------------------------------------------------------------------------- /resources/lang/pt_BR/filament-authentication.php: -------------------------------------------------------------------------------- 1 | 'Autenticação', 6 | 'section.users' => 'Usuários', 7 | 'section.user' => 'Usuário', 8 | 9 | 10 | /* 11 | |-------------------------------------------------------------------------- 12 | | Fields 13 | |-------------------------------------------------------------------------- 14 | */ 15 | 'field.id' => 'ID', 16 | 'field.user.name' => 'Nome', 17 | 'field.user.email' => 'E-mail', 18 | 'field.user.roles' => 'Função', 19 | 'field.user.verified_at' => 'Verificado em', 20 | 'field.user.created_at' => 'Criado em', 21 | 'field.user.password' => 'Senha', 22 | 'field.user.confirm_password' => 'Confimação da Senha', 23 | 24 | 'field.guard_name' => 'Nome do Guarda', 25 | 'field.name' => 'Nome', 26 | 'field.permissions' => 'Permissões', 27 | 'field.roles' => 'Funções', 28 | 29 | /* 30 | |-------------------------------------------------------------------------- 31 | | Labels 32 | |-------------------------------------------------------------------------- 33 | */ 34 | 35 | 'section.permission' => 'Permissão', 36 | 'section.permissions' => 'Permissões', 37 | 'section.role' => 'Função', 38 | 'section.roles' => 'Funções', 39 | 'section.roles_and_permissions' => 'Funções e Permissões', 40 | 41 | 42 | /* 43 | |-------------------------------------------------------------------------- 44 | | Filters 45 | |-------------------------------------------------------------------------- 46 | */ 47 | 'filter.verified' => 'E-mail Verificado', 48 | 49 | 'button.impersonate' => "Personificar Usuário", 50 | 'text.impersonating' => 'Personificando o Usuário: ', 51 | 'text.impersonating.end' => ' - Encerrar Perssonificação ', 52 | ]; 53 | -------------------------------------------------------------------------------- /resources/lang/vi/filament-authentication.php: -------------------------------------------------------------------------------- 1 | 'Xác thực', 6 | 'section.users' => 'Người dùng', 7 | 'section.user' => 'Người dùng', 8 | 9 | 10 | /* 11 | |-------------------------------------------------------------------------- 12 | | Fields 13 | |-------------------------------------------------------------------------- 14 | */ 15 | 'field.id' => 'ID', 16 | 'field.user.name' => 'Họ tên', 17 | 'field.user.email' => 'Email', 18 | 'field.user.roles' => 'Vai trò', 19 | 'field.user.verified_at' => 'Đã xác thực', 20 | 'field.user.created_at' => 'Tham gia lúc', 21 | 'field.user.password' => 'Mật khẩu', 22 | 'field.user.confirm_password' => 'Mật khẩu xác nhận', 23 | 24 | 'field.guard_name' => 'Tên guard', 25 | 'field.name' => 'Tên', 26 | 'field.permissions' => 'Quyền', 27 | 'field.roles' => 'Vai trò', 28 | 29 | /* 30 | |-------------------------------------------------------------------------- 31 | | Labels 32 | |-------------------------------------------------------------------------- 33 | */ 34 | 35 | 'section.permission' => 'Quyền', 36 | 'section.permissions' => 'Quyền', 37 | 'section.role' => 'Vai trò', 38 | 'section.roles' => 'Vai trò', 39 | 'section.roles_and_permissions' => 'Vai trò và Quyền', 40 | 41 | 42 | /* 43 | |-------------------------------------------------------------------------- 44 | | Filters 45 | |-------------------------------------------------------------------------- 46 | */ 47 | 'filter.verified' => 'Đã xác thực email', 48 | 49 | 'button.impersonate' => "Người dùng mạo danh", 50 | 'text.impersonating' => 'Mạo danh người dùng: ', 51 | 'text.impersonating.end' => ' - Kết thúc Mạo danh ', 52 | ]; 53 | -------------------------------------------------------------------------------- /resources/views/components/banner.blade.php: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phpsa/filament-authentication/f1c48294344cdb8d36f028fd5cf54610ac34e095/resources/views/components/banner.blade.php -------------------------------------------------------------------------------- /resources/views/impersonating-banner.blade.php: -------------------------------------------------------------------------------- 1 | 12 | 13 |
17 |
18 | {{ __('filament-authentication::filament-authentication.text.impersonating') }} {{ $impersonating }} 19 | {{ __('filament-authentication::filament-authentication.text.impersonating.end') }} 20 |
21 | 22 |
23 | -------------------------------------------------------------------------------- /resources/views/pages/auth/renew-password.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{ $this->form }} 4 | 5 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /routes/web.php: -------------------------------------------------------------------------------- 1 | group(function () { 9 | foreach (Filament::getPanels() as $panel) { 10 | if ($panel->hasPlugin('filament-authentication') === false) { 11 | continue; 12 | } 13 | 14 | /** @var \Phpsa\FilamentAuthentication\FilamentAuthentication */ 15 | $plugin = $panel->getPlugin('filament-authentication'); 16 | 17 | $domains = $panel->getDomains(); 18 | 19 | foreach ((empty($domains) ? [null] : $domains) as $domain) { 20 | Route::domain($domain) 21 | ->middleware($panel->getMiddleware()) 22 | ->name($panel->getId() . '.') 23 | ->prefix($panel->getPath()) 24 | ->group(function () use ($panel, $plugin) { 25 | 26 | if ($plugin->impersonateEnabled()) { 27 | Route::get('/impersonate/stop', fn () => ImpersonateLink::leave()) 28 | ->name('fa.stop.impersonation'); 29 | } 30 | 31 | if ((int) config('filament-authentication.password_renew.renew_password_days_period', 0) > 0) { 32 | Route::get('password/renew', RenewPassword::class)->name('fa.password.renew'); 33 | } 34 | }); 35 | } 36 | } 37 | }); 38 | -------------------------------------------------------------------------------- /src/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phpsa/filament-authentication/f1c48294344cdb8d36f028fd5cf54610ac34e095/src/.DS_Store -------------------------------------------------------------------------------- /src/Actions/ImpersonateLink.php: -------------------------------------------------------------------------------- 1 | label(__('filament-authentication::filament-authentication.button.impersonate')) 20 | ->icon('heroicon-o-identification') 21 | ->action(fn ($record) => static::impersonate($record)) 22 | ->hidden(fn ($record) => ! static::allowed(Filament::auth()->user(), $record)); 23 | } 24 | 25 | /** 26 | * Undocumented function 27 | * 28 | * @param \Illuminate\Database\Eloquent\Model&\Illuminate\Contracts\Auth\Authenticatable $current 29 | * @param \Illuminate\Database\Eloquent\Model&\Illuminate\Contracts\Auth\Authenticatable $target 30 | * @return bool 31 | */ 32 | public static function allowed(User $current, User $target): bool 33 | { 34 | $enabled = FilamentAuthentication::getPlugin()->impersonateEnabled(); 35 | return $enabled 36 | && $current->isNot($target) 37 | && ! app(ImpersonateManager::class)->isImpersonating() 38 | && (! method_exists($current, 'canImpersonate') || $current->canImpersonate()) 39 | && (! method_exists($target, 'canBeImpersonated') || $target->canBeImpersonated()); 40 | } 41 | 42 | /** 43 | * 44 | * @param \Illuminate\Database\Eloquent\Model&\Illuminate\Contracts\Auth\Authenticatable $record 45 | * @return false|Redirector|RedirectResponse 46 | * @throws BindingResolutionException 47 | */ 48 | public static function impersonate(User $record): false|Redirector|RedirectResponse 49 | { 50 | if (! static::allowed(Filament::auth()->user(), $record)) { 51 | return false; 52 | } 53 | 54 | app(ImpersonateManager::class)->take( 55 | Filament::auth()->user(), 56 | $record, 57 | FilamentAuthentication::getPlugin()->getImpersonateGuard() 58 | ); 59 | 60 | session()->put('impersonate.back_to', url()->previous()); 61 | 62 | session()->forget(array_unique([ 63 | 'password_hash_' . FilamentAuthentication::getPlugin()->getImpersonateGuard(), 64 | 'password_hash_' . config('filament.auth.guard'), 65 | ])); 66 | 67 | return redirect(FilamentAuthentication::getPlugin()->getImpersonateRedirect()); 68 | } 69 | 70 | public static function leave(): Redirector|RedirectResponse 71 | { 72 | 73 | if (! app(ImpersonateManager::class)->isImpersonating()) { 74 | return redirect('/'); 75 | } 76 | 77 | app(ImpersonateManager::class)->leave(); 78 | 79 | session()->forget(array_unique([ 80 | 'password_hash_' . FilamentAuthentication::getPlugin()->getImpersonateGuard(), 81 | 'password_hash_' . config('filament.auth.guard'), 82 | ])); 83 | 84 | return redirect( 85 | session()->pull('impersonate.back_to') ?? '/' 86 | ); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/Commands/InstallCommand.php: -------------------------------------------------------------------------------- 1 | line('*********************************'); 29 | $this->line('* FILAMENT AUTHENTICATION *'); 30 | $this->line('*********************************'); 31 | $this->newLine(2); 32 | $this->info('Thank you for choosing Filament authentication!'); 33 | $this->newLine(); 34 | $this->info('Publishing assets...'); 35 | 36 | $this->callSilent('vendor:publish', [ 37 | '--tag' => 'filament-authentication-migrations', 38 | ]); 39 | $this->callSilent('vendor:publish', [ 40 | '--tag' => 'tags-authentication-config', 41 | ]); 42 | 43 | if ($this->confirm('Do you want to run migrations now?', true)) { 44 | $this->call('migrate'); 45 | } 46 | 47 | $this->newLine(); 48 | if ($this->confirm('All done! Would you like to show some love by starring on GitHub?', true)) { 49 | if (PHP_OS_FAMILY === 'Darwin') { 50 | exec('open https://github.com/phpsa/filament-authentication'); 51 | } 52 | if (PHP_OS_FAMILY === 'Linux') { 53 | exec('xdg-open https://github.com/phpsa/filament-authentication'); 54 | } 55 | if (PHP_OS_FAMILY === 'Windows') { 56 | exec('start https://github.com/phpsa/filament-headless-cms'); 57 | } 58 | 59 | $this->components->info('Thank you!'); 60 | } 61 | 62 | return static::SUCCESS; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/Commands/UpdateUserPasswordToUpdatedCommand.php: -------------------------------------------------------------------------------- 1 | line('*********************************'); 29 | $this->line('* FILAMENT AUTHENTICATION *'); 30 | $this->line('*********************************'); 31 | 32 | config('filament-authentication.models.User')::whereDoesntHave('renewables')->get()->each(function ($user) { 33 | $user->renewables()->create([ 34 | ]); 35 | }); 36 | 37 | $this->components->info('Thank you!'); 38 | 39 | return static::SUCCESS; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Events/UserCreated.php: -------------------------------------------------------------------------------- 1 | user = $user; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Events/UserUpdated.php: -------------------------------------------------------------------------------- 1 | user = $user; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/FilamentAuthentication.php: -------------------------------------------------------------------------------- 1 | 24 | */ 25 | protected array $models = []; 26 | 27 | /** 28 | * @var array 29 | */ 30 | protected array $resources = []; 31 | 32 | public static function make(): self 33 | { 34 | $instance = new static(); //@phpstan-ignore new.static 35 | $config = config('filament-authentication'); 36 | $instance->overrideModels($config['models']); 37 | $instance->overrideResources($config['resources']); 38 | $instance->setPreload($config['preload_roles'], $config['preload_permissions']); 39 | $instance->setImpersonation($config['impersonate']['enabled'], $config['impersonate']['guard'], $config['impersonate']['redirect']); 40 | $instance->withSoftDeletes($config['soft_deletes']); 41 | 42 | return $instance; 43 | } 44 | 45 | public static function getPlugin(): self 46 | { 47 | /** 48 | * @var FilamentAuthentication 49 | */ 50 | return filament('filament-authentication'); 51 | } 52 | 53 | public function getId(): string 54 | { 55 | return 'filament-authentication'; 56 | } 57 | 58 | public function register(Panel $panel): void 59 | { 60 | $panel->resources($this->resources); 61 | if ($this->impersonateEnabled()) { 62 | $panel->middleware([ImpersonatingMiddleware::class]); 63 | } 64 | if ((int) config('filament-authentication.password_renew.renew_password_days_period', 0) > 0) { 65 | $panel->middleware([RenewPasswordMiddleware::class]); 66 | } 67 | } 68 | 69 | public function boot(Panel $panel): void 70 | { 71 | /** 72 | * @mixin \Filament\Tables\Columns\TextColumn 73 | */ 74 | TextColumn::macro('humanDate', function () { 75 | /** 76 | * @var \Filament\Tables\Columns\TextColumn $this 77 | * @phpstan-ignore varTag.nativeType 78 | * */ 79 | $this->formatStateUsing(fn ($state): ?string => $state ? $state->diffForHumans() : null); 80 | 81 | return $this; 82 | }); 83 | } 84 | 85 | public function setPreload(bool $roles = true, bool $permissions = true): self 86 | { 87 | $this->preloadRoles = $roles; 88 | $this->preloadPermissions = $permissions; 89 | 90 | return $this; 91 | } 92 | 93 | public function getPreloadRoles(): bool 94 | { 95 | return $this->preloadRoles; 96 | } 97 | 98 | public function getPreloadPermissions(): bool 99 | { 100 | return $this->preloadPermissions; 101 | } 102 | 103 | public function setImpersonation(bool $enabled = true, string $guard = 'web', string|Closure $redirect = '/'): self 104 | { 105 | $this->impersonate = $enabled; 106 | $this->impersonateGuard = $guard; 107 | $this->impersonateRedirect = $redirect; 108 | 109 | return $this; 110 | } 111 | 112 | public function impersonateEnabled(): bool 113 | { 114 | return $this->impersonate; 115 | } 116 | 117 | public function getImpersonateGuard(): string 118 | { 119 | return $this->impersonateGuard; 120 | } 121 | 122 | public function getImpersonateRedirect(): string|Closure 123 | { 124 | $value = $this->impersonateRedirect; 125 | if (! $value instanceof Closure) { 126 | return $value; 127 | } 128 | return $value(); 129 | } 130 | 131 | /** 132 | * @param array $overrides 133 | */ 134 | public function overrideModels(array $overrides): self 135 | { 136 | $this->models = array_merge($this->models, $overrides); 137 | return $this; 138 | } 139 | 140 | public function getModel(string $model): string 141 | { 142 | return $this->models[$model]; 143 | } 144 | 145 | /** 146 | * @param array $overrides 147 | */ 148 | public function overrideResources(array $overrides): self 149 | { 150 | $resources = array_merge($this->resources, $overrides); 151 | $this->resources = Arr::whereNotNull($resources); 152 | return $this; 153 | } 154 | 155 | public function getResource(string $resource): string 156 | { 157 | return $this->resources[$resource]; 158 | } 159 | 160 | public function withSoftDeletes(bool $enabled = true): self 161 | { 162 | $this->softDeletes = $enabled; 163 | return $this; 164 | } 165 | 166 | public function usesSoftDeletes(): bool 167 | { 168 | return $this->softDeletes; 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /src/FilamentAuthenticationProvider.php: -------------------------------------------------------------------------------- 1 | name('filament-authentication') 23 | ->hasViews() 24 | ->hasRoute('web') 25 | ->hasMigration('create_filament_authentication_tables') 26 | ->hasMigration('create_filament_password_renew_table') 27 | ->hasMigration('tracks_filament_password_hashes') 28 | ->hasConfigFile('filament-authentication') 29 | ->hasCommand(InstallCommand::class) 30 | ->hasCommand(UpdateUserPasswordToUpdatedCommand::class) 31 | ->hasTranslations(); 32 | 33 | Event::subscribe(AuthenticationLoggingSubscriber::class); 34 | } 35 | 36 | public function packageBooted() 37 | { 38 | 39 | Livewire::component('phpsa.filament-authentication.pages.auth.renew-password', RenewPassword::class); 40 | 41 | parent::packageBooted(); 42 | 43 | Arr::macro('pushBefore', function (array $existing, $key, $new): array { 44 | $keys = array_keys($existing); 45 | $index = array_search($key, $keys); 46 | $pos = false === $index ? count($existing) : $index - 1; 47 | return array_merge(array_slice($existing, 0, $pos), $new, array_slice($existing, $pos)); 48 | }); 49 | 50 | Arr::macro('pushAfter', function (array $existing, $key, $new): array { 51 | $keys = array_keys($existing); 52 | $index = array_search($key, $keys); 53 | $pos = false === $index ? count($existing) : $index + 1; 54 | return array_merge(array_slice($existing, 0, $pos), $new, array_slice($existing, $pos)); 55 | }); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Http/Middleware/ImpersonatingMiddleware.php: -------------------------------------------------------------------------------- 1 | isImpersonating()) { 27 | return $response; 28 | } 29 | 30 | return $request->wantsJson() 31 | ? $this->setJsonContent($response) : 32 | $response->setContent( 33 | str_replace( 34 | '', 35 | $this->getHtmlContent($request) . '', 36 | $response->getContent() 37 | ) 38 | ); 39 | } 40 | 41 | protected function getHtmlContent($request): string 42 | { 43 | $panel = Filament::getCurrentPanel()->getId(); 44 | return view($this->view, [ 45 | 'panel' => $panel, 46 | 'impersonating' => Filament::getUserName(auth()->user()) 47 | ])->render(); 48 | } 49 | 50 | protected function setJsonContent(JsonResponse|Response $response): JsonResponse|Response 51 | { 52 | $data = $this->getResponseData($response); 53 | if ($data === false || ! is_object($data)) { 54 | return $response; 55 | } 56 | 57 | $data->impersonating = true; 58 | 59 | return $this->setResponseData($response, $data); 60 | } 61 | 62 | protected function getResponseData(JsonResponse|Response $response) 63 | { 64 | if ($response instanceof JsonResponse) { 65 | return $response->getData() ?: new \StdClass(); 66 | } 67 | 68 | $content = $response->getContent(); 69 | 70 | return json_decode($content) ?: false; 71 | } 72 | 73 | protected function setResponseData(JsonResponse|Response $response, $data): JsonResponse|Response 74 | { 75 | if ($response instanceof JsonResponse) { 76 | return $response->setData($data); 77 | } 78 | 79 | $content = json_encode($data, JsonResponse::DEFAULT_ENCODING_OPTIONS); 80 | 81 | return $response->setContent($content); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/Http/Middleware/RenewPasswordMiddleware.php: -------------------------------------------------------------------------------- 1 | user(); 22 | 23 | $ignore = ! ($response instanceof Response) || $user === null 24 | || $request->routeIs( 25 | Filament::getCurrentPanel()->generateRouteName('auth.logout') 26 | ) 27 | || $request->routeIs( 28 | Filament::getCurrentPanel()->generateRouteName('fa.password.renew') 29 | ) 30 | || app(ImpersonateManager::class)->isImpersonating() 31 | || ! in_array( 32 | CanRenewPassword::class, 33 | class_uses_recursive(FilamentAuthentication::getPlugin()->getModel('User')) 34 | ) || ! $user->needsRenewal(); //@phpstan-ignore method.notFound (part of trait) 35 | 36 | // Only touch illuminate responses (avoid binary, etc) 37 | if ($ignore) { 38 | return $response; 39 | } 40 | 41 | if ($request->wantsJson()) { 42 | $response->header('X-Needs-Password-Renewal', 'true'); 43 | return $response; 44 | } 45 | 46 | $panelId = Filament::getCurrentPanel()->getId(); 47 | return Redirect::guest(URL::route("filament.{$panelId}.fa.password.renew")); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Models/AuthenticationLog.php: -------------------------------------------------------------------------------- 1 | 'boolean', 40 | 'location' => 'array', 41 | 'login_successful' => 'boolean', 42 | 'login_at' => 'datetime', 43 | 'logout_at' => 'datetime', 44 | ]; 45 | 46 | 47 | public function getConnectionName() 48 | { 49 | return config('filament-authentication.authentication_log.db_connection', null) ?? $this->connection; 50 | } 51 | 52 | public function getTable() 53 | { 54 | return config('filament-authentication.authentication_log.table_name', 'authentication_log'); 55 | } 56 | 57 | public function authenticatable(): MorphTo 58 | { 59 | return $this->morphTo(); 60 | } 61 | 62 | public function prunable(): Builder 63 | { 64 | $days = config('filament-authentication.authentication_log.prune', 365); 65 | if ((int) $days <= 0) { 66 | // If the value is 0 or less, we don't want to prune anything. 67 | return static::whereNull('id'); 68 | } 69 | return static::where('login_at', '<=', now()->subDays(365)); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/Models/PasswordRenewLog.php: -------------------------------------------------------------------------------- 1 | connection; 32 | } 33 | 34 | public function getTable() 35 | { 36 | return config('filament-authentication.password_renew.table_name', 'authentication_log'); 37 | } 38 | 39 | public function authenticatable(): MorphTo 40 | { 41 | return $this->morphTo(); 42 | } 43 | 44 | public function prunable(): Builder 45 | { 46 | $days = config('filament-authentication.password_renew.prune', 365); 47 | if ((int) $days <= 0) { 48 | // If the value is 0 or less, we don't want to prune anything. 49 | return static::whereNull('id'); 50 | } 51 | return static::where('created_at', '<=', now()->subDays(365)); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Pages/Auth/RenewPassword.php: -------------------------------------------------------------------------------- 1 | | null 38 | */ 39 | public ?array $data = []; 40 | 41 | public function mount(): void 42 | { 43 | $user = Filament::auth()->user(); 44 | 45 | if (! in_array(CanRenewPassword::class, class_uses_recursive($user)) 46 | || ! $user->needsRenewal() //@phpstan-ignore method.notFound (part of trait) 47 | ) { 48 | redirect()->intended(Filament::getUrl()); 49 | } 50 | 51 | $this->form->fill(); 52 | } 53 | 54 | public function renew() 55 | { 56 | /** 57 | * @var array{password: string, currentPassword: string} $data 58 | */ 59 | $data = $this->form->getState(); 60 | 61 | $user = Filament::auth()->user(); 62 | 63 | $user->password = $data['password']; //@phpstan-ignore property.notFound 64 | $user->save(); 65 | 66 | if (request()->hasSession()) { 67 | request()->session()->put([ 68 | 'password_hash_' . Filament::getAuthGuard() => $user->password, 69 | ]); 70 | } 71 | 72 | Notification::make() 73 | ->title(__('filament-authentication::filament-authentication.renew_notifications.title')) 74 | ->body(__('filament-authentication::filament-authentication.renew_notifications.body')) 75 | ->success() 76 | ->send(); 77 | 78 | return redirect()->intended(Filament::getUrl()); 79 | } 80 | 81 | public function form(Form $form): Form 82 | { 83 | return $form; 84 | } 85 | 86 | /** 87 | * @return array 88 | */ 89 | protected function getForms(): array 90 | { 91 | 92 | return [ 93 | 'form' => $this->form( 94 | $this->makeForm() 95 | ->schema([ 96 | TextInput::make('currentPassword') 97 | ->label(__('filament-authentication::filament-authentication.field.user.current_password')) 98 | ->password() 99 | ->required() 100 | ->rule('current_password:' . filament()->getAuthGuard()), 101 | TextInput::make('password') 102 | ->label(__('filament-authentication::filament-authentication.field.user.password')) 103 | ->password() 104 | ->revealable(filament()->arePasswordsRevealable()) 105 | ->required() 106 | ->rules(['different:data.currentPassword', PasswordRule::default(), new PreventPasswordReuseRule()]), 107 | TextInput::make('PasswordConfirmation') 108 | ->label(__('filament-authentication::filament-authentication.field.user.confirm_password')) 109 | ->password() 110 | ->revealable(filament()->arePasswordsRevealable()) 111 | ->required() 112 | ->same('password'), 113 | ]) 114 | ->statePath('data'), 115 | ), 116 | ]; 117 | } 118 | 119 | protected function hasFullWidthFormActions(): bool 120 | { 121 | return true; 122 | } 123 | 124 | protected function getFormActions(): array 125 | { 126 | return [ 127 | $this->getRenewFormAction(), 128 | ]; 129 | } 130 | 131 | protected function getRenewFormAction(): Action 132 | { 133 | 134 | return Action::make('renew') 135 | ->label(__('filament-authentication::filament-authentication.form.actions.renew.label')) 136 | ->submit('renew'); 137 | } 138 | 139 | public function getTitle(): string | Htmlable 140 | { 141 | return __('filament-authentication::filament-authentication.renew_page_title'); 142 | } 143 | 144 | public function getHeading(): string | Htmlable 145 | { 146 | return __('filament-authentication::filament-authentication.renew_page_heading'); 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /src/Resources/AuthenticationLogResource.php: -------------------------------------------------------------------------------- 1 | getModel('AuthenticationLog'); 25 | } 26 | 27 | public static function shouldRegisterNavigation(): bool 28 | { 29 | return config('filament-authentication.navigation.authentication_log.register', true); 30 | } 31 | 32 | public static function getNavigationIcon(): string 33 | { 34 | return config('filament-authentication.navigation.authentication_log.icon'); 35 | } 36 | 37 | public static function getNavigationSort(): ?int 38 | { 39 | return config('filament-authentication.navigation.authentication_log.sort'); 40 | } 41 | 42 | public static function getNavigationGroup(): ?string 43 | { 44 | return strval(__(config('filament-authentication.section.group') ?? 'filament-authentication::filament-authentication.section.group')); 45 | } 46 | 47 | public static function getLabel(): string 48 | { 49 | return __('filament-authentication::filament-authentication.section.authentication_log.label'); 50 | } 51 | 52 | public static function getPluralLabel(): string 53 | { 54 | return __('filament-authentication::filament-authentication.section.authentication_log.plural-label'); 55 | } 56 | 57 | public static function form(Form $form): Form 58 | { 59 | return $form 60 | ->schema([ 61 | // Forms\Components\MorphToSelect::make('authenticable') 62 | // ->types(self::authenticableResources()) 63 | // ->required(), 64 | // Forms\Components\TextInput::make('Ip Address'), 65 | // Forms\Components\TextInput::make('User Agent'), 66 | // Forms\Components\DateTimePicker::make('Login At'), 67 | // Forms\Components\Toggle::make('Login Successful'), 68 | // Forms\Components\DateTimePicker::make('Logout At'), 69 | // Forms\Components\Toggle::make('Cleared By User'), 70 | // Forms\Components\KeyValue::make('Location'), 71 | ]); 72 | } 73 | 74 | public static function table(Table $table): Table 75 | { 76 | return $table 77 | ->columns([ 78 | Tables\Columns\TextColumn::make('authenticatable') 79 | ->label(trans('filament-authentication::filament-authentication.authentication-log.column.authenticatable')) 80 | ->formatStateUsing(function (?string $state, Model $record) { 81 | //@phpstan-ignore property.notFound (model is dynamic) 82 | if (! $record->authenticatable_id) { 83 | return new HtmlString('—'); 84 | } 85 | //@phpstan-ignore property.notFound (model is dynamic) 86 | $authClass = $record->authenticatable::class; 87 | return new HtmlString('' . class_basename($authClass) . ''); 88 | }) 89 | ->sortable(), 90 | Tables\Columns\TextColumn::make('ip_address') 91 | ->label(trans('filament-authentication::filament-authentication.authentication-log.column.ip_address')) 92 | ->searchable() 93 | ->sortable(), 94 | Tables\Columns\TextColumn::make('user_agent') 95 | ->label(trans('filament-authentication::filament-authentication.authentication-log.column.user_agent')) 96 | ->searchable() 97 | ->sortable() 98 | ->limit(50) 99 | ->tooltip(function (TextColumn $column): ?string { 100 | $state = $column->getState(); 101 | 102 | if (strlen($state) <= $column->getCharacterLimit()) { 103 | return null; 104 | } 105 | 106 | return $state; 107 | }), 108 | Tables\Columns\TextColumn::make('login_at') 109 | ->label(trans('filament-authentication::filament-authentication.authentication-log.column.login_at')) 110 | ->dateTime() 111 | ->sortable(), 112 | Tables\Columns\IconColumn::make('login_successful') 113 | ->label(trans('filament-authentication::filament-authentication.authentication-log.column.login_successful')) 114 | ->boolean() 115 | ->sortable(), 116 | Tables\Columns\TextColumn::make('logout_at') 117 | ->label(trans('filament-authentication::filament-authentication.authentication-log.column.logout_at')) 118 | ->dateTime() 119 | ->sortable(), 120 | Tables\Columns\IconColumn::make('cleared_by_user') 121 | ->label(trans('filament-authentication::filament-authentication.authentication-log.column.cleared_by_user')) 122 | ->boolean() 123 | ->sortable(), 124 | //Tables\Columns\TextColumn::make('location'), 125 | ]) 126 | ->actions([ 127 | // 128 | ]) 129 | ->filters([ 130 | Filter::make('login_successful') 131 | ->toggle() 132 | ->query(fn (Builder $query): Builder => $query->where('login_successful', true)), 133 | Filter::make('login_at') 134 | ->form([ 135 | DatePicker::make('login_from'), 136 | DatePicker::make('login_until'), 137 | ]) 138 | ->query(function (Builder $query, array $data): Builder { 139 | return $query 140 | ->when( 141 | $data['login_from'], 142 | fn (Builder $query, $date): Builder => $query->whereDate('login_at', '>=', $date), 143 | ) 144 | ->when( 145 | $data['login_until'], 146 | fn (Builder $query, $date): Builder => $query->whereDate('login_at', '<=', $date), 147 | ); 148 | }), 149 | Filter::make('cleared_by_user') 150 | ->toggle() 151 | ->query(fn (Builder $query): Builder => $query->where('cleared_by_user', true)), 152 | ]); 153 | } 154 | 155 | public static function getRelations(): array 156 | { 157 | return [ 158 | // 159 | ]; 160 | } 161 | 162 | public static function getPages(): array 163 | { 164 | return [ 165 | 'index' => ListAuthenticationLogs::route('/'), 166 | ]; 167 | } 168 | 169 | public static function getWidgets(): array 170 | { 171 | return [ 172 | // 173 | ]; 174 | } 175 | 176 | // public static function authenticableResources(): array 177 | // { 178 | // return config('filament-authentication.authenticable-resources', [ 179 | // \App\Models\User::class, 180 | // ]); 181 | // } 182 | } 183 | -------------------------------------------------------------------------------- /src/Resources/AuthenticationLogResource/Pages/ListAuthenticationLogs.php: -------------------------------------------------------------------------------- 1 | getResource('AuthenticationLogResource'); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Resources/PermissionResource.php: -------------------------------------------------------------------------------- 1 | getModel('Permission'); 34 | } 35 | 36 | public static function getLabel(): string 37 | { 38 | return strval(__('filament-authentication::filament-authentication.section.permission')); 39 | } 40 | 41 | public static function getNavigationGroup(): ?string 42 | { 43 | return strval(__(config('filament-authentication.section.group') ?? 'filament-authentication::filament-authentication.section.group')); 44 | } 45 | 46 | public static function getPluralLabel(): string 47 | { 48 | return strval(__('filament-authentication::filament-authentication.section.permissions')); 49 | } 50 | 51 | public static function shouldRegisterNavigation(): bool 52 | { 53 | return config('filament-authentication.navigation.permission.register', true); 54 | } 55 | 56 | public static function getNavigationIcon(): string 57 | { 58 | return config('filament-authentication.navigation.permission.icon'); 59 | } 60 | 61 | public static function getNavigationSort(): ?int 62 | { 63 | return config('filament-authentication.navigation.permission.sort'); 64 | } 65 | 66 | 67 | public static function form(Form $form): Form 68 | { 69 | return $form 70 | ->schema([ 71 | Card::make() 72 | ->schema([ 73 | Grid::make(2)->schema([ 74 | TextInput::make('name') 75 | ->required() 76 | ->label(strval(__('filament-authentication::filament-authentication.field.name'))), 77 | TextInput::make('guard_name') 78 | ->required() 79 | ->label(strval(__('filament-authentication::filament-authentication.field.guard_name'))) 80 | ->default(config('auth.defaults.guard')), 81 | 82 | ]), 83 | ]), 84 | ]); 85 | } 86 | 87 | public static function table(Table $table): Table 88 | { 89 | return $table 90 | ->columns([ 91 | TextColumn::make('id') 92 | ->label('ID') 93 | ->searchable(), 94 | TextColumn::make('name') 95 | ->label(strval(__('filament-authentication::filament-authentication.field.name'))) 96 | ->searchable(), 97 | TextColumn::make('guard_name') 98 | ->label(strval(__('filament-authentication::filament-authentication.field.guard_name'))) 99 | ->searchable(), 100 | ]) 101 | ->filters([ 102 | // 103 | ]) 104 | ->actions([ 105 | ViewAction::make(), 106 | EditAction::make(), 107 | DeleteAction::make(), 108 | ]) 109 | ->bulkActions([ 110 | DeleteBulkAction::make(), 111 | ]); 112 | } 113 | 114 | public static function getRelations(): array 115 | { 116 | return [ 117 | RoleRelationManager::class, 118 | ]; 119 | } 120 | 121 | public static function getPages(): array 122 | { 123 | return [ 124 | 'index' => ListPermissions::route('/'), 125 | 'create' => CreatePermission::route('/create'), 126 | 'edit' => EditPermission::route('/{record}/edit'), 127 | 'view' => ViewPermission::route('/{record}'), 128 | ]; 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/Resources/PermissionResource/Pages/CreatePermission.php: -------------------------------------------------------------------------------- 1 | getResource('PermissionResource'); 16 | } 17 | 18 | public function afterSave(): void 19 | { 20 | if (! $this->record instanceof Permission) { 21 | return; 22 | } 23 | 24 | app(PermissionRegistrar::class)->forgetCachedPermissions(); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Resources/PermissionResource/Pages/EditPermission.php: -------------------------------------------------------------------------------- 1 | getResource('PermissionResource'); 18 | } 19 | 20 | public function afterSave(): void 21 | { 22 | if (! $this->record instanceof Permission) { 23 | return; 24 | } 25 | 26 | app(PermissionRegistrar::class)->forgetCachedPermissions(); 27 | } 28 | 29 | protected function getHeaderActions(): array 30 | { 31 | return [ 32 | ViewAction::make(), 33 | DeleteAction::make(), 34 | ]; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Resources/PermissionResource/Pages/ListPermissions.php: -------------------------------------------------------------------------------- 1 | getResource('PermissionResource'); 18 | } 19 | 20 | protected function getHeaderActions(): array 21 | { 22 | return [ 23 | CreateAction::make(), 24 | ]; 25 | } 26 | 27 | // protected function getTableBulkActions(): array 28 | // { 29 | // $roleClass = config('filament-authentication.models.Role'); 30 | 31 | // return [ 32 | // BulkAction::make('Attach Role') 33 | // ->action(function (Collection $records, array $data): void { 34 | // // dd($data); 35 | // foreach ($records as $record) { 36 | // $record->roles()->sync($data['role']); 37 | // $record->save(); 38 | // } 39 | // }) 40 | // ->form([ 41 | // Select::make('role') 42 | // ->label(strval(__('filament-authentication::filament-authentication.field.role'))) 43 | // ->options((new $roleClass())::query()->pluck('name', 'id')) 44 | // ->required(), 45 | // ])->deselectRecordsAfterCompletion(), 46 | // ]; 47 | // } 48 | } 49 | -------------------------------------------------------------------------------- /src/Resources/PermissionResource/Pages/ViewPermission.php: -------------------------------------------------------------------------------- 1 | getResource('PermissionResource'); 15 | } 16 | 17 | protected function getHeaderActions(): array 18 | { 19 | return[ 20 | EditAction::make(), 21 | ]; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Resources/PermissionResource/RelationManager/RoleRelationManager.php: -------------------------------------------------------------------------------- 1 | schema([ 27 | TextInput::make('name') 28 | ->label(strval(__('filament-authentication::filament-authentication.field.name'))), 29 | TextInput::make('guard_name') 30 | ->label(strval(__('filament-authentication::filament-authentication.field.guard_name'))) 31 | ->default(config('auth.defaults.guard')), 32 | 33 | ]); 34 | } 35 | 36 | public function table(Table $table): Table 37 | { 38 | return $table 39 | ->columns([ 40 | TextColumn::make('name') 41 | ->label(strval(__('filament-authentication::filament-authentication.field.name'))), 42 | TextColumn::make('guard_name') 43 | ->label(strval(__('filament-authentication::filament-authentication.field.guard_name'))), 44 | 45 | ]) 46 | ->headerActions([ 47 | CreateAction::make(), 48 | AttachAction::make()->preloadRecordSelect(FilamentAuthentication::getPlugin()->getPreloadRoles()) 49 | ->recordSelect(fn($select) => $select->multiple()) 50 | ->closeModalByClickingAway(false), 51 | ]) 52 | ->actions([ 53 | DetachAction::make() 54 | ]) 55 | 56 | ->bulkActions([ 57 | 58 | DissociateBulkAction::make(), 59 | 60 | ]); 61 | } 62 | 63 | public function afterAttach(): void 64 | { 65 | app(PermissionRegistrar::class)->forgetCachedPermissions(); 66 | } 67 | 68 | public function afterDetach(): void 69 | { 70 | app(PermissionRegistrar::class)->forgetCachedPermissions(); 71 | } 72 | 73 | public function isReadOnly(): bool 74 | { 75 | return false; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/Resources/RoleResource.php: -------------------------------------------------------------------------------- 1 | getModel('Role'); 35 | } 36 | 37 | public static function getLabel(): string 38 | { 39 | return strval(__('filament-authentication::filament-authentication.section.role')); 40 | } 41 | 42 | public static function getNavigationGroup(): ?string 43 | { 44 | return strval(__(config('filament-authentication.section.group') ?? 'filament-authentication::filament-authentication.section.group')); 45 | } 46 | 47 | public static function getPluralLabel(): string 48 | { 49 | return strval(__('filament-authentication::filament-authentication.section.roles')); 50 | } 51 | 52 | public static function shouldRegisterNavigation(): bool 53 | { 54 | return config('filament-authentication.navigation.role.register', true); 55 | } 56 | 57 | public static function getNavigationIcon(): string 58 | { 59 | return config('filament-authentication.navigation.role.icon'); 60 | } 61 | 62 | public static function getNavigationSort(): ?int 63 | { 64 | return config('filament-authentication.navigation.role.sort'); 65 | } 66 | 67 | 68 | public static function form(Form $form): Form 69 | { 70 | return $form 71 | ->schema([ 72 | Card::make() 73 | ->schema([ 74 | Grid::make(2) 75 | ->schema([ 76 | TextInput::make('name') 77 | ->label(strval(__('filament-authentication::filament-authentication.field.name'))) 78 | ->required(), 79 | TextInput::make('guard_name') 80 | ->label(strval(__('filament-authentication::filament-authentication.field.guard_name'))) 81 | ->required() 82 | ->default(config('auth.defaults.guard')), 83 | 84 | ]), 85 | ]), 86 | ]); 87 | } 88 | 89 | public static function table(Table $table): Table 90 | { 91 | return $table 92 | ->columns([ 93 | TextColumn::make('id') 94 | ->label('ID') 95 | ->searchable(), 96 | TextColumn::make('name') 97 | ->label(strval(__('filament-authentication::filament-authentication.field.name'))) 98 | ->searchable(), 99 | TextColumn::make('guard_name') 100 | ->label(strval(__('filament-authentication::filament-authentication.field.guard_name'))) 101 | ->searchable(), 102 | ]) 103 | ->filters([ 104 | SelectFilter::make('guard_name') 105 | ->multiple() 106 | ->options(fn() => collect(config('auth.guards'))->keys()->mapWithKeys(fn($g) => [$g => $g])->toArray()) 107 | ]) 108 | ->actions([ 109 | ViewAction::make(), 110 | EditAction::make(), 111 | DeleteAction::make(), 112 | ]) 113 | ->bulkActions([ 114 | DeleteBulkAction::make(), 115 | ]); 116 | } 117 | 118 | public static function getRelations(): array 119 | { 120 | return [ 121 | PermissionRelationManager::class, 122 | UserRelationManager::class, 123 | ]; 124 | } 125 | 126 | public static function getPages(): array 127 | { 128 | return [ 129 | 'index' => ListRoles::route('/'), 130 | 'create' => CreateRole::route('/create'), 131 | 'edit' => EditRole::route('/{record}/edit'), 132 | 'view' => ViewRole::route('/{record}'), 133 | ]; 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/Resources/RoleResource/Pages/CreateRole.php: -------------------------------------------------------------------------------- 1 | getResource('RoleResource'); 16 | } 17 | 18 | public function afterSave(): void 19 | { 20 | if (! $this->record instanceof Role) { 21 | return; 22 | } 23 | 24 | app(PermissionRegistrar::class)->forgetCachedPermissions(); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Resources/RoleResource/Pages/EditRole.php: -------------------------------------------------------------------------------- 1 | getResource('RoleResource'); 18 | } 19 | 20 | public function afterSave(): void 21 | { 22 | if (! $this->record instanceof Role) { 23 | return; 24 | } 25 | 26 | app(PermissionRegistrar::class)->forgetCachedPermissions(); 27 | } 28 | 29 | protected function getHeaderActions(): array 30 | { 31 | return [ 32 | ViewAction::make(), 33 | DeleteAction::make(), 34 | ]; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Resources/RoleResource/Pages/ListRoles.php: -------------------------------------------------------------------------------- 1 | getResource('RoleResource'); 15 | } 16 | 17 | protected function getHeaderActions(): array 18 | { 19 | return [ 20 | CreateAction::make(), 21 | ]; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Resources/RoleResource/Pages/ViewRole.php: -------------------------------------------------------------------------------- 1 | getResource('RoleResource'); 15 | } 16 | 17 | protected function getHeaderActions(): array 18 | { 19 | return[ 20 | EditAction::make(), 21 | ]; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Resources/RoleResource/RelationManager/PermissionRelationManager.php: -------------------------------------------------------------------------------- 1 | schema([ 27 | TextInput::make('name') 28 | ->required() 29 | ->label(strval(__('filament-authentication::filament-authentication.field.name'))), 30 | TextInput::make('guard_name') 31 | ->required() 32 | ->label(strval(__('filament-authentication::filament-authentication.field.guard_name'))) 33 | ->default(config('auth.defaults.guard')), 34 | 35 | ]); 36 | } 37 | 38 | public function table(Table $table): Table 39 | { 40 | return $table 41 | ->columns([ 42 | TextColumn::make('name') 43 | ->label(strval(__('filament-authentication::filament-authentication.field.name'))) 44 | ->searchable(), 45 | TextColumn::make('guard_name') 46 | ->label(strval(__('filament-authentication::filament-authentication.field.guard_name'))), 47 | 48 | ]) 49 | ->headerActions([ 50 | CreateAction::make(), 51 | AttachAction::make() 52 | ->preloadRecordSelect(FilamentAuthentication::getPlugin()->getPreloadPermissions()) 53 | ->recordSelect(fn($select) => $select->multiple()) 54 | ->closeModalByClickingAway(false), 55 | ]) 56 | ->actions([ 57 | DetachAction::make() 58 | ]) 59 | 60 | ->bulkActions([ 61 | 62 | DetachBulkAction::make(), 63 | 64 | ]); 65 | } 66 | 67 | public function afterAttach(): void 68 | { 69 | app(PermissionRegistrar::class)->forgetCachedPermissions(); 70 | } 71 | 72 | public function afterDetach(): void 73 | { 74 | app(PermissionRegistrar::class)->forgetCachedPermissions(); 75 | } 76 | 77 | 78 | public function isReadOnly(): bool 79 | { 80 | return false; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/Resources/RoleResource/RelationManager/UserRelationManager.php: -------------------------------------------------------------------------------- 1 | columns([ 23 | TextColumn::make('id') 24 | ->label(strval(__('filament-authentication::filament-authentication.field.id'))) 25 | ->searchable(), 26 | TextColumn::make('name') 27 | ->label(strval(__('filament-authentication::filament-authentication.field.name'))) 28 | ->searchable(), 29 | TextColumn::make('email') 30 | ->searchable() 31 | ->label(strval(__('filament-authentication::filament-authentication.field.user.email'))), 32 | 33 | ]) 34 | ->filters([ 35 | // 36 | ]) 37 | ->headerActions([ 38 | // ... 39 | AttachAction::make(), 40 | ]) 41 | ->actions([ 42 | DetachAction::make() 43 | ]) 44 | 45 | ->bulkActions([ 46 | 47 | DissociateBulkAction::make(), 48 | 49 | ]); 50 | } 51 | 52 | public function afterAttach(): void 53 | { 54 | } 55 | 56 | public function afterDetach(): void 57 | { 58 | } 59 | 60 | public function isReadOnly(): bool 61 | { 62 | return false; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/Resources/UserResource.php: -------------------------------------------------------------------------------- 1 | getModel('User'); 46 | } 47 | 48 | public static function getNavigationGroup(): ?string 49 | { 50 | return strval(__(config('filament-authentication.section.group') ?? 'filament-authentication::filament-authentication.section.group')); 51 | } 52 | 53 | public static function getLabel(): string 54 | { 55 | return strval(__('filament-authentication::filament-authentication.section.user')); 56 | } 57 | 58 | public static function getPluralLabel(): string 59 | { 60 | return strval(__('filament-authentication::filament-authentication.section.users')); 61 | } 62 | 63 | public static function shouldRegisterNavigation(): bool 64 | { 65 | return config('filament-authentication.navigation.user.register', true); 66 | } 67 | 68 | public static function getNavigationIcon(): string 69 | { 70 | return config('filament-authentication.navigation.user.icon'); 71 | } 72 | 73 | public static function getNavigationSort(): ?int 74 | { 75 | return config('filament-authentication.navigation.user.sort'); 76 | } 77 | public static function form(Form $form): Form 78 | { 79 | 80 | return $form 81 | ->schema([ 82 | Card::make() 83 | ->schema([ 84 | 'name' => TextInput::make('name') 85 | ->label(strval(__('filament-authentication::filament-authentication.field.user.name'))) 86 | ->required(), 87 | 'email' => TextInput::make('email') 88 | ->required() 89 | ->email() 90 | ->unique(table: static::$model, ignorable: fn ($record) => $record) 91 | ->label(strval(__('filament-authentication::filament-authentication.field.user.email'))), 92 | 'password' => TextInput::make('password') 93 | ->same('passwordConfirmation') 94 | ->hiddenOn('view') 95 | ->live(debounce: 250) 96 | ->password() 97 | ->maxLength(255) 98 | ->required(fn ($component, $get, $livewire, $model, $record, $set, $state) => $record === null) 99 | ->dehydrateStateUsing(fn ($state) => ! empty($state) ? Hash::make($state) : '') 100 | ->label(strval(__('filament-authentication::filament-authentication.field.user.password'))), 101 | 'passwordConfirmation' => TextInput::make('passwordConfirmation') 102 | ->password() 103 | ->dehydrated(false) 104 | ->visible(fn(Get $get) => filled($get('password'))) 105 | ->maxLength(255) 106 | ->label(strval(__('filament-authentication::filament-authentication.field.user.confirm_password'))), 107 | 'roles' => Select::make('roles') 108 | ->multiple() 109 | ->relationship('roles', 'name') 110 | ->preload(FilamentAuthentication::getPlugin()->getPreloadRoles()) 111 | ->label(strval(__('filament-authentication::filament-authentication.field.user.roles'))), 112 | ])->columns(2), 113 | ]); 114 | } 115 | 116 | 117 | 118 | protected static function getTableColumns(): array 119 | { 120 | $columns = [ 121 | 'id' => TextColumn::make('id') 122 | ->sortable() 123 | ->label(strval(__('filament-authentication::filament-authentication.field.id'))), 124 | 'name' => TextColumn::make('name') 125 | ->searchable() 126 | ->sortable() 127 | ->label(strval(__('filament-authentication::filament-authentication.field.user.name'))), 128 | 'email' => TextColumn::make('email') 129 | ->searchable() 130 | ->sortable() 131 | ->label(strval(__('filament-authentication::filament-authentication.field.user.email'))), 132 | 133 | 'email_verified_at' => IconColumn::make('email_verified_at') 134 | ->default(false) 135 | ->boolean() 136 | ->label(strval(__('filament-authentication::filament-authentication.field.user.verified_at'))), 137 | 'roles.name' => TextColumn::make('roles.name')->badge() 138 | ->label(strval(__('filament-authentication::filament-authentication.field.user.roles'))), 139 | 'created_at' => TextColumn::make('created_at') 140 | ->dateTime('Y-m-d H:i:s') 141 | ->label(strval(__('filament-authentication::filament-authentication.field.user.created_at'))), 142 | ]; 143 | 144 | if (in_array(LogsAuthentication::class, class_uses_recursive(FilamentAuthentication::getPlugin()->getModel('User')))) { 145 | $columns['last_login'] = TextColumn::make('latestSuccessfullAuthentication.login_at') 146 | ->dateTime('Y-m-d H:i:s') 147 | //@phpstan-ignore nullsafe.neverNull 148 | ->description(fn(Model $record) => $record->latestSuccessfullAuthentication?->ip_address ?? '-') 149 | ->label(strval(__('filament-authentication::filament-authentication.field.user.last_login_at'))); 150 | } 151 | 152 | if (in_array(CanRenewPassword::class, class_uses_recursive(FilamentAuthentication::getPlugin()->getModel('User')))) { 153 | $columns['last_password_changed'] = TextColumn::make('latestRenewable.created_at') 154 | ->dateTime('Y-m-d H:i:s') 155 | ->label(strval(__('filament-authentication::filament-authentication.field.user.last_password_updated'))); 156 | } 157 | 158 | return $columns; 159 | } 160 | 161 | protected static function getTableFilters(): array 162 | { 163 | $filters = [ 164 | 'email_verified_at' => TernaryFilter::make('email_verified_at') 165 | ->label(strval(__('filament-authentication::filament-authentication.filter.verified'))) 166 | ->nullable(), 167 | ]; 168 | 169 | if (FilamentAuthentication::getPlugin()->usesSoftDeletes()) { 170 | $filters['trashed'] = \Filament\Tables\Filters\TrashedFilter::make(); 171 | } 172 | 173 | return $filters; 174 | } 175 | 176 | protected static function getTableActions(): array 177 | { 178 | $actions = [ 179 | 'view' => ViewAction::make(), 180 | 'edit' => EditAction::make(), 181 | 'impersonate' => FilamentAuthentication::getPlugin()->impersonateEnabled() ? ImpersonateLink::make() : null, 182 | 'delete' => DeleteAction::make(), 183 | 'force_delete' => FilamentAuthentication::getPlugin()->usesSoftDeletes() ? ForceDeleteAction::make() : null, 184 | 'restore' => FilamentAuthentication::getPlugin()->usesSoftDeletes() ? RestoreAction::make() : null, 185 | ]; 186 | 187 | return array_filter($actions); 188 | } 189 | 190 | protected static function getTableBulkActions(): array 191 | { 192 | $actions = [ 193 | DeleteBulkAction::make(), 194 | FilamentAuthentication::getPlugin()->usesSoftDeletes() ? RestoreBulkAction::make() : null, 195 | FilamentAuthentication::getPlugin()->usesSoftDeletes() ? ForceDeleteBulkAction::make() : null, 196 | ]; 197 | 198 | return array_filter($actions); 199 | } 200 | 201 | 202 | public static function table(Table $table): Table 203 | { 204 | return $table 205 | ->columns(static::getTableColumns()) 206 | ->filters(static::getTableFilters()) 207 | ->actions(static::getTableActions()) 208 | ->bulkActions(static::getTableBulkActions()); 209 | } 210 | 211 | public static function getRelations(): array 212 | { 213 | return [ 214 | AuthenticationLogsRelationManager::class 215 | ]; 216 | } 217 | 218 | public static function getPages(): array 219 | { 220 | return [ 221 | 'index' => ListUsers::route('/'), 222 | 'create' => CreateUser::route('/create'), 223 | 'edit' => EditUser::route('/{record}/edit'), 224 | 'view' => ViewUser::route('/{record}'), 225 | ]; 226 | } 227 | 228 | 229 | public static function getEloquentQuery(): Builder 230 | { 231 | return parent::getEloquentQuery() 232 | ->when( 233 | FilamentAuthentication::getPlugin()->usesSoftDeletes(), 234 | //@phpstan-ignore method.notFound (softDeletes) 235 | fn(Builder $builder) => $builder->withTrashed() 236 | ); 237 | } 238 | } 239 | -------------------------------------------------------------------------------- /src/Resources/UserResource/Pages/CreateUser.php: -------------------------------------------------------------------------------- 1 | getResource('UserResource'); 16 | } 17 | 18 | protected function afterCreate(): void 19 | { 20 | // @phpstan-ignore argument.type (record is an authenticatable user) 21 | Event::dispatch(new UserCreated($this->record)); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Resources/UserResource/Pages/EditUser.php: -------------------------------------------------------------------------------- 1 | getResource('UserResource'); 19 | } 20 | 21 | protected function mutateFormDataBeforeSave(array $data): array 22 | { 23 | if (empty($data['password'])) { 24 | unset($data['password']); 25 | } 26 | 27 | return $data; 28 | } 29 | 30 | protected function afterSave(): void 31 | { 32 | // @phpstan-ignore argument.type (record is an authenticatable user) 33 | Event::dispatch(new UserUpdated($this->record)); 34 | } 35 | 36 | protected function getHeaderActions(): array 37 | { 38 | return [ 39 | ViewAction::make(), 40 | DeleteAction::make(), 41 | RestoreAction::make(), 42 | ]; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Resources/UserResource/Pages/ListUsers.php: -------------------------------------------------------------------------------- 1 | getResource('UserResource'); 15 | } 16 | 17 | protected function getActions(): array 18 | { 19 | return [ 20 | CreateAction::make(), 21 | ]; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Resources/UserResource/Pages/ViewUser.php: -------------------------------------------------------------------------------- 1 | getResource('UserResource'); 19 | } 20 | 21 | protected function getHeaderActions(): array 22 | { 23 | return collect([ 24 | EditAction::make(), 25 | $this->impersonateAction(), 26 | ])->filter()->toArray(); 27 | } 28 | 29 | protected function impersonateAction(): ?Action 30 | { 31 | /** @var \Illuminate\Database\Eloquent\Model&\Illuminate\Contracts\Auth\Authenticatable */ 32 | $record = $this->getRecord(); 33 | $user = Filament::auth()->user(); 34 | if ($user === null || ImpersonateLink::allowed($user, $record) === false) { 35 | return null; 36 | } 37 | 38 | return Action::make('impersonate') 39 | ->label(__('filament-authentication::filament-authentication.button.impersonate')) 40 | ->action(fn() => ImpersonateLink::impersonate($record)); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Resources/UserResource/RelationManager/AuthenticationLogsRelationManager.php: -------------------------------------------------------------------------------- 1 | getModel('User')))) { 29 | return parent::canViewForRecord($ownerRecord, $pageClass); 30 | } 31 | return false; 32 | } 33 | 34 | public static function getTitle(Model $ownerRecord, string $pageClass): string 35 | { 36 | return trans('filament-authentication::filament-authentication.authentication-log.table.heading'); 37 | } 38 | 39 | public function table(Table $table): Table 40 | { 41 | return $table 42 | ->modifyQueryUsing(fn (Builder $query) => $query->orderByDesc('login_at')) 43 | ->columns([ 44 | TextColumn::make('authenticatable') 45 | ->label(trans('filament-authentication::filament-authentication.authentication-log.column.authenticatable')) 46 | ->formatStateUsing(function (?string $state, Model $record) { 47 | // @phpstan-ignore property.notFound (model is dynamic) 48 | if (! $record->authenticatable_id) { 49 | return new HtmlString('—'); 50 | } 51 | // @phpstan-ignore property.notFound (model is dynamic) 52 | $AuthClass = $record->authenticatable::class; 53 | return new HtmlString('' . class_basename($AuthClass) . ''); 54 | }) 55 | ->sortable(), 56 | TextColumn::make('ip_address') 57 | ->label(trans('filament-authentication::filament-authentication.authentication-log.column.ip_address')) 58 | ->searchable() 59 | ->sortable(), 60 | TextColumn::make('user_agent') 61 | ->label(trans('filament-authentication::filament-authentication.authentication-log.column.user_agent')) 62 | ->searchable() 63 | ->sortable() 64 | ->limit(50) 65 | ->tooltip(function (TextColumn $column): ?string { 66 | $state = $column->getState(); 67 | 68 | if (strlen($state) <= $column->getCharacterLimit()) { 69 | return null; 70 | } 71 | 72 | return $state; 73 | }), 74 | TextColumn::make('login_at') 75 | ->label(trans('filament-authentication::filament-authentication.authentication-log.column.login_at')) 76 | ->since() 77 | ->sortable(), 78 | IconColumn::make('login_successful') 79 | ->label(trans('filament-authentication::filament-authentication.authentication-log.column.login_successful')) 80 | ->boolean() 81 | ->sortable(), 82 | TextColumn::make('logout_at') 83 | ->label(trans('filament-authentication::filament-authentication.authentication-log.column.logout_at')) 84 | ->since() 85 | ->sortable(), 86 | IconColumn::make('cleared_by_user') 87 | ->label(trans('filament-authentication::filament-authentication.authentication-log.column.cleared_by_user')) 88 | ->boolean() 89 | ->sortable(), 90 | ]) 91 | ->filters([ 92 | // 93 | ]) 94 | ->headerActions([ 95 | // 96 | ]) 97 | ->actions([ 98 | // 99 | ]) 100 | ->bulkActions([ 101 | // 102 | ]); 103 | } 104 | 105 | protected function canCreate(): bool 106 | { 107 | return false; 108 | } 109 | 110 | protected function canEdit(Model $record): bool 111 | { 112 | return false; 113 | } 114 | 115 | protected function canDelete(Model $record): bool 116 | { 117 | return false; 118 | } 119 | 120 | protected function canView(Model $record): bool 121 | { 122 | return $this->can('view', $record); 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/Resources/UserResource/RelationManager/RoleRelationManager.php: -------------------------------------------------------------------------------- 1 | schema([ 22 | TextInput::make('name') 23 | ->label(strval(__('filament-authentication::filament-authentication.field.name'))), 24 | TextInput::make('guard_name') 25 | ->label(strval(__('filament-authentication::filament-authentication.field.guard_name'))) 26 | ->default(config('auth.defaults.guard')), 27 | 28 | ]); 29 | } 30 | 31 | public function table(Table $table): Table 32 | { 33 | return $table 34 | ->columns([ 35 | TextColumn::make('name') 36 | ->label(strval(__('filament-authentication::filament-authentication.field.name'))) 37 | ->searchable(), 38 | TextColumn::make('guard_name') 39 | ->label(strval(__('filament-authentication::filament-authentication.field.guard_name'))), 40 | 41 | ]) 42 | ->filters([ 43 | // 44 | ]); 45 | } 46 | 47 | public function afterAttach(): void 48 | { 49 | app(PermissionRegistrar::class)->forgetCachedPermissions(); 50 | } 51 | 52 | public function afterDetach(): void 53 | { 54 | app(PermissionRegistrar::class)->forgetCachedPermissions(); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Rules/PreventPasswordReuseRule.php: -------------------------------------------------------------------------------- 1 | user = $user ?? auth()->user(); 18 | } 19 | 20 | public function validate(string $attribute, mixed $value, Closure $fail): void 21 | { 22 | 23 | //if config is disabled we don't wanna 24 | if ((int) config('filament-authentication.password_renew.prevent_password_reuse') <= 0) { 25 | return; 26 | } 27 | 28 | $previous = PasswordRenewLog::where('renewable_id', $this->user->getAuthIdentifier()) 29 | ->where('renewable_type', get_class($this->user)) 30 | ->latest() 31 | ->limit(config('filament-authentication.password_renew.prevent_password_reuse')) 32 | ->pluck('phash') 33 | ->filter(fn($hash) => Hash::check($value, $hash)) 34 | ->isNotEmpty(); 35 | 36 | if ($previous) { 37 | $fail('You cannot use the same password as a previous one.'); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Subscribers/AuthenticationLoggingSubscriber.php: -------------------------------------------------------------------------------- 1 | 'handleUserLogin', 27 | Logout::class => 'handleUserLogout', 28 | Failed::class => 'handleUserFailedLogin', 29 | OtherDeviceLogout::class => 'handleOtherDeviceLogout', 30 | ]; 31 | } 32 | 33 | protected function shouldLogAuthentication(?Authenticatable $user): bool 34 | { 35 | return ! is_null($user) && in_array(LogsAuthentication::class, class_uses_recursive(get_class($user))); 36 | } 37 | 38 | protected function logEvent(?Authenticatable $user, bool $wasSuccessful, array $overrides = []) 39 | { 40 | if (! $this->shouldLogAuthentication($user)) { 41 | return; 42 | } 43 | //@phpstan-ignore method.notFound (authentications added in trait) 44 | return $user->authentications()->create([ 45 | 'ip_address' => $this->request->ip(), 46 | 'user_agent' => $this->request->userAgent(), 47 | 'login_at' => now(), 48 | 'login_successful' => $wasSuccessful, 49 | // @todo 50 | // 'location' => config('filament-authentication.notifications.new-device.location') ? optional(geoip()->getLocation($ip))->toArray() : null, 51 | ... $overrides, 52 | ]); 53 | } 54 | 55 | public function handleUserLogin(Login $event): void 56 | { 57 | $user = $event->user; 58 | $log = $this->logEvent($user, true); 59 | 60 | // $newUser = Carbon::parse($user->{$user->getCreatedAtColumn()})->diffInMinutes(Carbon::now()) < 1; 61 | // if (! $known && ! $newUser && config('filament-authentication.notifications.new-device.enabled')) { 62 | // $newDevice = config('filament-authentication.notifications.new-device.template') ?? NewDevice::class; 63 | // $user->notify(new $newDevice($log)); 64 | // } 65 | // if (LogsAuthentication) { 66 | // } 67 | } 68 | public function handleUserLogout(Logout $event) 69 | { 70 | $user = $event->user; 71 | if (! $this->shouldLogAuthentication($user)) { 72 | return; 73 | } 74 | 75 | $ip = $this->request->ip(); 76 | $userAgent = $this->request->userAgent(); 77 | //@phpstan-ignore method.notFound (authentications added in trait) 78 | $log = $user->authentications() 79 | ->whereIpAddress($ip) 80 | ->whereUserAgent($userAgent) 81 | ->whereNull('logout_at') 82 | ->firstOrNew([ 83 | 84 | 'ip_address' => $ip, 85 | 'user_agent' => $userAgent, 86 | 87 | ]); 88 | $log->logout_at = now(); 89 | 90 | //@phpstan-ignore method.notFound (authentications added in trait) 91 | $user->authentications()->save($log); 92 | } 93 | 94 | public function handleUserFailedLogin(Failed $event): void 95 | { 96 | $log = $this->logEvent($event->user, false); 97 | //todo: notify ?? with AuthRecord ? 98 | // if (config('filament-authentication.notifications.failed-login.enabled')) { 99 | // $failedLogin = config('filament-authentication.notifications.failed-login.template') ?? FailedLogin::class; 100 | // $event->user->notify(new $failedLogin($log)); 101 | // } 102 | } 103 | 104 | public function handleOtherDeviceLogout(OtherDeviceLogout $event) 105 | { 106 | $user = $event->user; 107 | if (! $this->shouldLogAuthentication($user)) { 108 | return; 109 | } 110 | 111 | $ip = $this->request->ip(); 112 | $userAgent = $this->request->userAgent(); 113 | //@phpstan-ignore method.notFound (authentications added in trait) 114 | $log = $user->authentications() 115 | ->whereIpAddress($ip) 116 | ->whereUserAgent($userAgent) 117 | ->whereNull('logout_at') 118 | ->firstOrCreate([ 119 | 120 | 'ip_address' => $ip, 121 | 'user_agent' => $userAgent, 122 | 123 | ]); 124 | 125 | //@phpstan-ignore method.notFound (authentications added in trait) 126 | $user->authentications()->whereLoginSuccessful(true)->whereNull('logout_at') 127 | ->whereKeyNot($log->getKey())->update([ 128 | 'logout_at' => now(), 129 | 'cleared_by_user' => true, 130 | ]); 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/Traits/CanRenewPassword.php: -------------------------------------------------------------------------------- 1 | getAuthPasswordName() : 'password'; 15 | 16 | if ($user->isDirty($field)) { 17 | $user->renewables()->where('created_at', now())->firstOrCreate([ 18 | 'phash' => $user->getOriginal($field), 19 | ]); 20 | } 21 | }); 22 | } 23 | 24 | public function renewables() 25 | { 26 | return $this->morphMany(config('filament-authentication.models.PasswordRenewLog'), 'renewable')->latest(); 27 | } 28 | 29 | public function latestRenewable() 30 | { 31 | return $this->morphOne(config('filament-authentication.models.PasswordRenewLog'), 'renewable')->latestOfMany(); 32 | } 33 | 34 | public function needsRenewal(): bool 35 | { 36 | 37 | $period = config('filament-authentication.password_renew.renew_password_days_period'); 38 | 39 | if (! is_numeric($period) || $period <= 0) { 40 | return false; 41 | } 42 | 43 | return $this->latestRenewable()->where('created_at', '>=', now()->subDays($period))->doesntExist(); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Traits/LogsAuthentication.php: -------------------------------------------------------------------------------- 1 | morphMany(config('filament-authentication.models.AuthenticationLog'), 'authenticatable')->latest('login_at'); 13 | } 14 | 15 | public function latestAuthentication() 16 | { 17 | return $this->morphOne(config('filament-authentication.models.AuthenticationLog'), 'authenticatable')->latestOfMany('login_at'); 18 | } 19 | 20 | public function latestSuccessfullAuthentication() 21 | { 22 | return $this->latestAuthentication()->whereLoginSuccessful(true); 23 | } 24 | 25 | public function lastLoginAt() 26 | { 27 | return $this->authentications()->first()?->login_at; 28 | } 29 | 30 | public function lastSuccessfulLoginAt() 31 | { 32 | return $this->authentications()->whereLoginSuccessful(true)->first()?->login_at; 33 | } 34 | 35 | public function lastLoginIp() 36 | { 37 | return $this->authentications()->first()?->ip_address; 38 | } 39 | 40 | public function lastSuccessfulLoginIp() 41 | { 42 | return $this->authentications()->whereLoginSuccessful(true)->first()?->ip_address; 43 | } 44 | 45 | public function previousLoginAt() 46 | { 47 | return $this->authentications()->skip(1)->first()?->login_at; 48 | } 49 | 50 | public function previousLoginIp() 51 | { 52 | return $this->authentications()->skip(1)->first()?->ip_address; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Traits/PagePolicyTrait.php: -------------------------------------------------------------------------------- 1 | viewAny(Filament::auth()->user()); 25 | } 26 | 27 | protected static function shouldRegisterNavigation(): bool 28 | { 29 | return static::canView() && static::$shouldRegisterNavigation; 30 | } 31 | 32 | protected static function getPolicy() 33 | { 34 | return Gate::getPolicyFor(static::class); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Traits/SettingsPagePolicyTrait.php: -------------------------------------------------------------------------------- 1 | viewAny(Filament::auth()->user()); 27 | } 28 | 29 | protected static function shouldRegisterNavigation(): bool 30 | { 31 | return static::canView() && static::$shouldRegisterNavigation; 32 | } 33 | 34 | protected static function getPolicy() 35 | { 36 | return Gate::getPolicyFor(static::class); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Widgets/LatestUsersWidget.php: -------------------------------------------------------------------------------- 1 | getResource()::getEloquentQuery() 20 | ->latest() 21 | ->limit($this->limit); 22 | } 23 | 24 | protected function getTableColumns(): array 25 | { 26 | return [ 27 | TextColumn::make('id') 28 | ->label('ID'), 29 | TextColumn::make('name') 30 | ->label(strval(__('filament-authentication::filament-authentication.field.user.name'))), 31 | //@phpstan-ignore method.notFound (macro in package) 32 | TextColumn::make('created_at') 33 | ->humanDate() 34 | ->label(strval(__('filament-authentication::filament-authentication.field.user.created_at'))), 35 | ]; 36 | } 37 | 38 | protected function isTablePaginationEnabled(): bool 39 | { 40 | return $this->paginate; 41 | } 42 | 43 | public static function canView(): bool 44 | { 45 | return static::getResource()::canViewAny(); 46 | } 47 | 48 | public static function getResource(): string 49 | { 50 | return FilamentAuthentication::getPlugin()->getResource('UserResource'); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /tests/DefaultTest.php: -------------------------------------------------------------------------------- 1 | assertSame(1, 1); 15 | } 16 | } 17 | --------------------------------------------------------------------------------