diff --git a/README.md b/README.md index ad8de67..305e25a 100644 --- a/README.md +++ b/README.md @@ -39,9 +39,9 @@ the [EditRecord](https://filamentphp.com/docs/2.x/admin/resources/editing-record a [simple modal resource.](https://filamentphp.com/docs/2.x/admin/resources/getting-started#simple-modal-resources) Follow the steps below to add locks to your resources. -### Add Locks to your modal +### Add Locks to your model -The first step is to add the HasLocks trait to the modal of your resource. The HasLocks trait enables the locking +The first step is to add the HasLocks trait to the model of your resource. The HasLocks trait enables the locking functionality on your model. ```php @@ -95,13 +95,21 @@ class ListExamples extends ManageRecords And that's it! Your resource is now able to be locked. Refer to the documentation below for more information on how to configure the locking functionality. +## Resource Lock manager + +filament-shield-art + +The package also provides a simple way to manage and view all your active and expired locks within your app. And it also +provides a way to quickly unlock all resources or specific locks. + ## Configuration ### Access filament-shield-art -You can restrict the access to the **Unlock** button by adjusting the access variable. Enabling the "limited" key and +You can restrict the access to the **Unlock** button or to the resource manager by adjusting the access variable. +Enabling the "limited" key and setting it to true allows you to specify either a Laravel Gate class or a permission name from the [Spatie Permissions package](https://github.com/spatie/laravel-permission). @@ -119,11 +127,24 @@ the [Spatie Permissions package](https://github.com/spatie/laravel-permission). */ 'unlocker' => [ - 'limited_access' => false, + 'limited_access' => true, 'gate' => 'unlock-resource' ], ``` +Example + +```php + +// Example using gates +Gate::define('unlock-resource', function (User $user, Post $post) { + return $user->email === 'admin@mail.com'; +}); + +// Example using spatie permission package +Permission::create(['name' => 'unlock-resource']); +``` + ### Using custom models Sometimes, you may have a customized implementation for the User model in your application, or you may want to use a @@ -199,6 +220,12 @@ Optionally, you can publish the views using php artisan vendor:publish --tag="resource-lock-views" ``` +## Coming soon + +- Locked status indicator for table rows +- Polling +- Displaying which users has locked a resource + ## Changelog Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently. diff --git a/composer.json b/composer.json index 647d439..e1123b5 100644 --- a/composer.json +++ b/composer.json @@ -1,6 +1,6 @@ { "name": "kenepa/resource-lock", - "description": "This is my package resource lock", + "description": "Filament Resource Lock is a Filament plugin that adds resource locking functionality to your site.", "keywords": [ "Kenepa", "laravel", diff --git a/config/resource-lock.php b/config/resource-lock.php index e38ca8d..15f7d9d 100644 --- a/config/resource-lock.php +++ b/config/resource-lock.php @@ -20,12 +20,13 @@ /* |-------------------------------------------------------------------------- - | Resource Unlocker + | Resource Unlocker Button |-------------------------------------------------------------------------- | | The unlocker configuration specifies whether limited access is enabled for - | the resource lock feature. If limited access is enabled, only specific - | users or roles will be able to unlock locked resources. + | the resource unlock button. If limited access is enabled, only specific + | users or roles will be able to unlock locked resources directly from + | the modal. | */ @@ -34,6 +35,25 @@ // 'gate' => '' ], + /* + |-------------------------------------------------------------------------- + | Resource Lock Manager + |-------------------------------------------------------------------------- + | + | The resource lock manager provides a simple way to view all resource locks + | of your application. It provides several ways to quickly unlock all or + | specific resources within your app. + | + */ + + 'manager' => [ + 'navigation_label' => 'Resource Lock Manager', + 'navigation_group' => 'Ticket', + 'navigation_sort' => 1, + 'limited_access' => true, + 'gate' => 'manager' + ], + /* |-------------------------------------------------------------------------- | Lock timeout (in minutes) @@ -49,15 +69,16 @@ /* |-------------------------------------------------------------------------- - | Throw Forbidden Exception + | Check Locks before saving |-------------------------------------------------------------------------- | - | The throw_forbidden_exception configuration specifies whether a 403 forbidden - | exception should be thrown if a tech-savvy user is able to bypass the locked - | resource modal and attempt to save the resource. + | The check_locks_before_saving configuration specifies whether a lock of a resource will be checked + | before saving a resource if a tech-savvy user is able to bypass the locked + | resource modal and attempt to save the resource. In some cases you may want to turns this off. + | It's recommended to keep this on. | */ - 'throw_forbidden_exception' => true, + 'check_locks_before_saving' => true, ]; diff --git a/resources/lang/de/manager.php b/resources/lang/de/manager.php new file mode 100644 index 0000000..1c45b53 --- /dev/null +++ b/resources/lang/de/manager.php @@ -0,0 +1,10 @@ + 'Aktiv', + 'expired' => 'Abgelaufen', + 'unlock' => 'Entsperren', + 'unlocked' => 'Entsperrter Ressourcen', + 'unlocked_selected' => 'Ausgewählte Ressourcen entsperrt', + 'unlock_all' => 'Alle Ressourcen entsperren' +]; \ No newline at end of file diff --git a/resources/lang/en/manager.php b/resources/lang/en/manager.php new file mode 100644 index 0000000..c7179c1 --- /dev/null +++ b/resources/lang/en/manager.php @@ -0,0 +1,10 @@ + 'Active', + 'expired' => 'Expired', + 'unlock' => 'Unlock', + 'unlocked' => 'Unlocked resource', + 'unlocked_selected' => 'Unlocked selected resources', + 'unlock_all' => 'Unlock all resources' +]; \ No newline at end of file diff --git a/resources/lang/es/manager.php b/resources/lang/es/manager.php new file mode 100644 index 0000000..5513241 --- /dev/null +++ b/resources/lang/es/manager.php @@ -0,0 +1,10 @@ + 'Activo', + 'expired' => 'Expirado', + 'unlock' => 'Desbloquear', + 'unlocked' => 'Recurso desbloqueado', + 'unlocked_selected' => 'Recursos seleccionados desbloqueados', + 'unlock_all' => 'Desbloquear todos los recursos' +]; \ No newline at end of file diff --git a/resources/lang/fr/manager.php b/resources/lang/fr/manager.php new file mode 100644 index 0000000..d783755 --- /dev/null +++ b/resources/lang/fr/manager.php @@ -0,0 +1,10 @@ + 'Actif', + 'expired' => 'Expiré', + 'unlock' => 'Déverrouiller', + 'unlocked' => 'Ressource déverrouillée', + 'unlocked_selected' => 'Ressources sélectionnées déverrouillées', + 'unlock_all' => 'Déverrouiller toutes les ressources' +]; \ No newline at end of file diff --git a/resources/lang/nl/manager.php b/resources/lang/nl/manager.php new file mode 100644 index 0000000..d7aa602 --- /dev/null +++ b/resources/lang/nl/manager.php @@ -0,0 +1,10 @@ + 'Actief', + 'expired' => 'Verlopen', + 'unlock' => 'Ontgrendelen', + 'unlocked' => 'Ontgrendelde resource', + 'unlocked_selected' => 'Geselecteerde resource ontgrendeld', + 'unlock_all' => 'Alle resources ontgrendelen' +]; \ No newline at end of file diff --git a/src/Models/ResourceLock.php b/src/Models/ResourceLock.php index 1e84aaf..9173fab 100644 --- a/src/Models/ResourceLock.php +++ b/src/Models/ResourceLock.php @@ -2,6 +2,7 @@ namespace Kenepa\ResourceLock\Models; +use Carbon\Carbon; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; @@ -20,4 +21,11 @@ public function lockable(): MorphTo { return $this->morphTo(); } + + public function isExpired(): bool + { + $expiredDate = (new Carbon($this->updated_at))->addMinutes(config('resource-lock.lock_timeout')); + + return Carbon::now()->greaterThan($expiredDate); + } } diff --git a/src/ResourceLockServiceProvider.php b/src/ResourceLockServiceProvider.php index f2c95e9..08f3d7e 100644 --- a/src/ResourceLockServiceProvider.php +++ b/src/ResourceLockServiceProvider.php @@ -5,6 +5,7 @@ use Filament\Facades\Filament; use Filament\PluginServiceProvider; use Illuminate\Support\Facades\Blade; +use Kenepa\ResourceLock\Resources\ResourceLockResource; use Livewire\Livewire; use Spatie\LaravelPackageTools\Commands\InstallCommand; use Spatie\LaravelPackageTools\Package; @@ -14,7 +15,7 @@ class ResourceLockServiceProvider extends PluginServiceProvider public static string $name = 'resource-lock'; protected array $resources = [ - // CustomResource::class, + ResourceLockResource::class, ]; public function configurePackage(Package $package): void diff --git a/src/Resources/Pages/Concerns/UsesResourceLock.php b/src/Resources/Pages/Concerns/UsesResourceLock.php index 39d86c3..cd0ca40 100644 --- a/src/Resources/Pages/Concerns/UsesResourceLock.php +++ b/src/Resources/Pages/Concerns/UsesResourceLock.php @@ -67,8 +67,13 @@ public function resourceLockObserverUnlock() */ public function save(bool $shouldRedirect = true): void { - if (config('resource-lock.throw_forbidden_exception', true)) { - abort_unless($this->record->isLocked() && $this->record->isLockedByCurrentUser(), 403); + if (config('resource-lock.check_locks_before_saving', true)) { + $this->record->refresh(); + if ($this->record->isLocked() && !$this->record->isLockedByCurrentUser()) { + $this->checkIfResourceLockHasExpired($this->record); + $this->lockResource($this->record); + return; + } } parent::save($shouldRedirect); diff --git a/src/Resources/Pages/Concerns/UsesSimpleResourceLock.php b/src/Resources/Pages/Concerns/UsesSimpleResourceLock.php index 91eee54..b81fd67 100644 --- a/src/Resources/Pages/Concerns/UsesSimpleResourceLock.php +++ b/src/Resources/Pages/Concerns/UsesSimpleResourceLock.php @@ -33,6 +33,18 @@ public function mountTableAction(string $name, ?string $record = null) $this->lockResource($this->resourceRecord); } + public function callMountedTableAction(?string $arguments = null) { + if (config('resource-lock.check_locks_before_saving', true)) { + $this->resourceRecord->refresh(); + if ($this->resourceRecord->isLocked() && !$this->resourceRecord->isLockedByCurrentUser()) { + $this->checkIfResourceLockHasExpired($this->resourceRecord); + $this->lockResource($this->resourceRecord); + return; + } + } + parent::callMountedTableAction($arguments); + } + public function resourceLockObserverUnload() { $this->resourceRecord->unlock(); diff --git a/src/Resources/ResourceLockResource.php b/src/Resources/ResourceLockResource.php new file mode 100644 index 0000000..81ff7d2 --- /dev/null +++ b/src/Resources/ResourceLockResource.php @@ -0,0 +1,124 @@ +schema([ + // + ]); + } + + public static function table(Table $table): Table + { + return $table + ->columns([ + TextColumn::make('id')->label('Lock ID'), + TextColumn::make('user.id')->label('User ID'), + TextColumn::make('lockable.id')->label('Lockable ID'), + TextColumn::make('lockable_type'), + TextColumn::make('created_at'), + TextColumn::make('updated_at'), + BadgeColumn::make('updated_at')->label('Expired') + ->color(static function ($record): string { + if ($record->isExpired()) { + return 'warning'; + } + return 'success'; + }) + ->icon(static function ($record): string { + if ($record->isExpired()) { + return 'heroicon-o-lock-open'; + } + + return 'heroicon-o-lock-closed'; + })->formatStateUsing(static function ($record) { + if ($record->isExpired()) { + return __('resource-lock::manager.expired'); + } + + return __('resource-lock::manager.active'); + }) + ]) + ->filters([ + // + ]) + ->actions([ + Tables\Actions\DeleteAction::make() + ->icon('heroicon-o-lock-open') + ->successNotificationTitle(__('resource-lock::manager.unlocked')) + ->label(__('resource-lock::manager.unlock')), + ]) + ->bulkActions([ + Tables\Actions\DeleteBulkAction::make() + ->deselectRecordsAfterCompletion() + ->requiresConfirmation() + ->icon('heroicon-o-lock-open') + ->successNotificationTitle(__('resource-lock::manager.unlocked_selected')) + ->label(__('resource-lock::manager.unlock')), + ]); + } + + public static function getPages(): array + { + return [ + 'index' => ManageResourceLocks::route('/'), + ]; + } + + public static function canViewAny(): bool + { + if (config('resource-lock.manager.limited_access')) { + return Gate::allows(config('resource-lock.manager.gate')); + } + + return true; + } + + public static function canDeleteAny(): bool + { + if (config('resource-lock.manager.limited_access')) { + return Gate::allows(config('resource-lock.manager.gate')); + } + + return true; + } + + public static function getNavigationLabel(): string + { + return config('resource-lock.manager.navigation_label','Resource Lock Manager'); + } + + public static function getNavigationGroup(): ?string + { + return config('resource-lock.manager.navigation_group'); + } + + protected static function getNavigationSort(): ?int + { + return config('resource-lock.manager.navigation_sort'); + } +} diff --git a/src/Resources/ResourceLockResource/ManageResourceLocks.php b/src/Resources/ResourceLockResource/ManageResourceLocks.php new file mode 100644 index 0000000..c73e07d --- /dev/null +++ b/src/Resources/ResourceLockResource/ManageResourceLocks.php @@ -0,0 +1,26 @@ +icon('heroicon-o-lock-open') + ->action(fn () => ResourceLock::truncate()) + ->requiresConfirmation() + ]; + } +}