diff --git a/database/migrations/2024_01_01_000004_create_library_item_role_permissions_table.php b/database/migrations/2024_01_01_000004_create_library_item_role_permissions_table.php new file mode 100644 index 0000000..9c300cd --- /dev/null +++ b/database/migrations/2024_01_01_000004_create_library_item_role_permissions_table.php @@ -0,0 +1,31 @@ +id(); + $table->foreignId('library_item_id')->constrained()->cascadeOnDelete(); + $table->string('role_name'); + $table->timestamps(); + + $table->unique(['library_item_id', 'role_name'], 'library_item_role_perms_unique'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('library_item_role_permissions'); + } +}; diff --git a/src/FilamentLibraryPlugin.php b/src/FilamentLibraryPlugin.php index 7420fb4..c731928 100644 --- a/src/FilamentLibraryPlugin.php +++ b/src/FilamentLibraryPlugin.php @@ -10,6 +10,10 @@ class FilamentLibraryPlugin implements Plugin { protected static $libraryAdminCallback = null; + protected static $roleCheckerCallback = null; + + protected static $availableRolesCallback = null; + public function getId(): string { return 'filament-library'; @@ -56,6 +60,77 @@ public static function isLibraryAdmin($user): bool return false; } + /** + * Set a custom callback to check if a user has a specific role. + * + * @param callable $callback Function that receives a user and role name, returns bool + */ + public static function setRoleCheckerCallback(callable $callback): void + { + static::$roleCheckerCallback = $callback; + } + + /** + * Check if a user has a specific role. + * + * @param \Illuminate\Contracts\Auth\Authenticatable|null $user + */ + public static function hasRole($user, string $roleName): bool + { + if (! $user) { + return false; + } + + // Use custom callback if set + if (static::$roleCheckerCallback) { + return call_user_func(static::$roleCheckerCallback, $user, $roleName); + } + + // Check for config-based callback + $configCallback = config('filament-library.role_checker_callback'); + if ($configCallback && is_callable($configCallback)) { + return call_user_func($configCallback, $user, $roleName); + } + + // Default implementation - check if user has hasRole method + if (method_exists($user, 'hasRole')) { + return $user->hasRole($roleName); + } + + return false; + } + + /** + * Set a custom callback to get available roles for selection. + * + * @param callable $callback Function that returns an array of role names => labels + */ + public static function setAvailableRolesCallback(callable $callback): void + { + static::$availableRolesCallback = $callback; + } + + /** + * Get available roles for selection. + * + * @return array Array of role names => labels + */ + public static function getAvailableRoles(): array + { + // Use custom callback if set + if (static::$availableRolesCallback) { + return call_user_func(static::$availableRolesCallback); + } + + // Check for config-based callback + $configCallback = config('filament-library.available_roles_callback'); + if ($configCallback && is_callable($configCallback)) { + return call_user_func($configCallback); + } + + return []; + } + public function register(Panel $panel): void { $panel diff --git a/src/FilamentLibraryServiceProvider.php b/src/FilamentLibraryServiceProvider.php index 4d576c3..abfbd45 100644 --- a/src/FilamentLibraryServiceProvider.php +++ b/src/FilamentLibraryServiceProvider.php @@ -174,6 +174,7 @@ protected function getMigrations(): array '2024_01_01_000001_create_library_item_permissions_table', '2024_01_01_000002_create_library_item_tags_table', '2024_01_01_000003_create_library_item_favorites_table', + '2024_01_01_000004_create_library_item_role_permissions_table', ]; } } diff --git a/src/Models/LibraryItem.php b/src/Models/LibraryItem.php index ac86aaf..b161778 100644 --- a/src/Models/LibraryItem.php +++ b/src/Models/LibraryItem.php @@ -137,6 +137,14 @@ public function permissions(): HasMany return $this->hasMany(LibraryItemPermission::class); } + /** + * Get the role-based permissions for this item. + */ + public function rolePermissions(): HasMany + { + return $this->hasMany(LibraryItemRolePermission::class); + } + /** * Scope to get only folders. */ @@ -174,6 +182,66 @@ public function scopeForUser($query, $user) }); } + /** + * Check if this item (or its parent folders) has role-based restrictions. + */ + public function hasRoleRestrictions(): bool + { + // Check if this item has role restrictions + if ($this->rolePermissions()->exists()) { + return true; + } + + // Check parent folders recursively + if ($this->parent_id) { + return $this->parent->hasRoleRestrictions(); + } + + return false; + } + + /** + * Get all required role names for this item (including inherited from parents). + */ + public function getRequiredRoleNames(): array + { + $roleNames = $this->rolePermissions()->pluck('role_name')->toArray(); + + // Check parent folders recursively + if ($this->parent_id) { + $parentRoles = $this->parent->getRequiredRoleNames(); + $roleNames = array_merge($roleNames, $parentRoles); + } + + return array_unique($roleNames); + } + + /** + * Check if a user has access based on role restrictions. + */ + public function hasRoleAccess($user): bool + { + if (! $user) { + return false; + } + + $requiredRoles = $this->getRequiredRoleNames(); + + // If no role restrictions, allow access (will be checked by normal permissions) + if (empty($requiredRoles)) { + return true; + } + + // User must have at least one of the required roles + foreach ($requiredRoles as $roleName) { + if (\Tapp\FilamentLibrary\FilamentLibraryPlugin::hasRole($user, $roleName)) { + return true; + } + } + + return false; + } + /** * Get the effective role for a user on this item. */ @@ -183,6 +251,11 @@ public function getEffectiveRole($user): ?string return null; } + // Check role-based restrictions first + if ($this->hasRoleRestrictions() && ! $this->hasRoleAccess($user)) { + return null; // Denied by role restrictions + } + // Check if user is the creator (always has access) if ($this->created_by === $user->id) { // Creator always has access, but check if they're also the owner @@ -392,6 +465,11 @@ public function hasPermission($user, string $permission): bool return true; } + // Check role-based restrictions first + if ($this->hasRoleRestrictions() && ! $this->hasRoleAccess($user)) { + return false; // Denied by role restrictions + } + $effectiveRole = $this->getEffectiveRole($user); if (! $effectiveRole) { diff --git a/src/Models/LibraryItemRolePermission.php b/src/Models/LibraryItemRolePermission.php new file mode 100644 index 0000000..5b71af1 --- /dev/null +++ b/src/Models/LibraryItemRolePermission.php @@ -0,0 +1,32 @@ + 'datetime', + 'updated_at' => 'datetime', + ]; + + /** + * Get the library item this role permission belongs to. + */ + public function libraryItem(): BelongsTo + { + return $this->belongsTo(LibraryItem::class); + } +} diff --git a/src/Resources/Pages/CreateFolder.php b/src/Resources/Pages/CreateFolder.php index 1772ecc..1d80d3c 100644 --- a/src/Resources/Pages/CreateFolder.php +++ b/src/Resources/Pages/CreateFolder.php @@ -2,7 +2,9 @@ namespace Tapp\FilamentLibrary\Resources\Pages; +use Filament\Forms\Components\Select; use Filament\Resources\Pages\CreateRecord; +use Tapp\FilamentLibrary\FilamentLibraryPlugin; use Tapp\FilamentLibrary\Resources\LibraryItemResource; use Tapp\FilamentLibrary\Traits\HasParentFolder; @@ -12,6 +14,24 @@ class CreateFolder extends CreateRecord protected static string $resource = LibraryItemResource::class; + public function form(\Filament\Schemas\Schema $schema): \Filament\Schemas\Schema + { + return $schema + ->schema([ + ...LibraryItemResource::folderForm($schema)->getComponents(), + Select::make('role_permissions') + ->label('Required Roles') + ->helperText('Only users with at least one of these roles can access this folder. Leave empty to allow all users (subject to other permissions).') + ->options(function () { + return FilamentLibraryPlugin::getAvailableRoles(); + }) + ->multiple() + ->searchable() + ->dehydrated(false) // Don't save to model directly, we handle it in afterCreate + ->visible(fn () => auth()->user() && \Tapp\FilamentLibrary\FilamentLibraryPlugin::isLibraryAdmin(auth()->user())), + ]); + } + protected function mutateFormDataBeforeCreate(array $data): array { $data['type'] = 'folder'; @@ -20,4 +40,21 @@ protected function mutateFormDataBeforeCreate(array $data): array return $data; } + + protected function afterCreate(): void + { + parent::afterCreate(); + + $record = $this->getRecord(); + $rolePermissions = $this->form->getState()['role_permissions'] ?? []; + + // Sync role permissions + if (is_array($rolePermissions) && ! empty($rolePermissions)) { + foreach ($rolePermissions as $roleName) { + $record->rolePermissions()->create([ + 'role_name' => $roleName, + ]); + } + } + } } diff --git a/src/Resources/Pages/EditFolder.php b/src/Resources/Pages/EditFolder.php index ce8a6cf..fa254cf 100644 --- a/src/Resources/Pages/EditFolder.php +++ b/src/Resources/Pages/EditFolder.php @@ -3,6 +3,7 @@ namespace Tapp\FilamentLibrary\Resources\Pages; use Filament\Forms\Components\Select; +use Tapp\FilamentLibrary\FilamentLibraryPlugin; use Tapp\FilamentLibrary\Resources\LibraryItemResource; class EditFolder extends EditLibraryItemPage @@ -58,8 +59,49 @@ public function form(\Filament\Schemas\Schema $schema): \Filament\Schemas\Schema }) ->visible(fn () => $this->getRecord()->hasPermission(auth()->user(), 'share')), + Select::make('role_permissions') + ->label('Required Roles') + ->helperText('Only users with at least one of these roles can access this folder. Leave empty to allow all users (subject to other permissions).') + ->options(function () { + return FilamentLibraryPlugin::getAvailableRoles(); + }) + ->multiple() + ->searchable() + ->default(function () { + return $this->getRecord()->rolePermissions()->pluck('role_name')->toArray(); + }) + ->dehydrated(false) // Don't save to model directly, we handle it in mutateFormDataBeforeSave + ->visible(fn () => $this->getRecord()->hasPermission(auth()->user(), 'share')), + // Creator select field $this->getCreatorSelectField(), ]); } + + protected function mutateFormDataBeforeSave(array $data): array + { + // Handle role permissions separately + $rolePermissions = $data['role_permissions'] ?? []; + unset($data['role_permissions']); + + return $data; + } + + protected function afterSave(): void + { + parent::afterSave(); + + $record = $this->getRecord(); + $rolePermissions = $this->form->getState()['role_permissions'] ?? []; + + // Sync role permissions + $record->rolePermissions()->delete(); + if (is_array($rolePermissions) && ! empty($rolePermissions)) { + foreach ($rolePermissions as $roleName) { + $record->rolePermissions()->create([ + 'role_name' => $roleName, + ]); + } + } + } }