diff --git a/app/Models/Role.php b/app/Models/Role.php index d32d7e83..f7cba1d9 100644 --- a/app/Models/Role.php +++ b/app/Models/Role.php @@ -56,6 +56,16 @@ class Role extends SpatieRole 'disable_activity_checks', ]; + /** + * The name of the role that bypasses every permission check. + * + * Replaces the removed filament-shield `Utils::getSuperAdminName()`. + */ + public static function superAdminName(): string + { + return config('roles.super_admin', 'super_admin'); + } + public function getActivitylogOptions(): LogOptions { return LogOptions::defaults() diff --git a/app/Models/User.php b/app/Models/User.php index 2646e0c7..5d7980e4 100755 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -5,8 +5,8 @@ namespace App\Models; use App\Enums\JournalType; use App\Enums\UserState; use App\Observers\UserObserver; +use App\Services\PermissionRegistry; use App\Traits\JournalTrait; -use BezhanSalleh\FilamentShield\Support\Utils; use Database\Factories\UserFactory; use Filament\Models\Contracts\FilamentUser; use Filament\Models\Contracts\HasAvatar; @@ -33,7 +33,6 @@ use Spatie\Activitylog\LogOptions; use Spatie\Activitylog\Models\Activity; use Spatie\Activitylog\Traits\LogsActivity; use Spatie\Permission\Models\Permission; -use Spatie\Permission\Models\Role; use Spatie\Permission\Traits\HasRoles; use Staudenmeir\EloquentHasManyDeep\HasRelationships; @@ -487,16 +486,24 @@ class User extends Authenticatable implements FilamentUser, HasAvatar, MustVerif } // For modules panels - if ($this->hasRole(Utils::getSuperAdminName())) { + if ($this->hasRole(Role::superAdminName())) { return true; } + // Each module panel is gated by its own `access:` permission, + // falling back to the generic `view:modules` for unscoped panels. + $moduleKey = app(PermissionRegistry::class)->moduleKeyForPanel($panel); + + if ($moduleKey !== null) { + return $this->can('access:'.$moduleKey); + } + return $this->can('view:modules'); } public function hasAdminAccess(): bool { - if ($this->hasRole(Utils::getSuperAdminName().'|admin')) { + if ($this->hasRole(Role::superAdminName().'|admin')) { return true; } diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index c691c628..4f254d97 100755 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -11,10 +11,12 @@ use App\Enums\PirepStatus; use App\Enums\UserState; use App\Http\Composers\PageLinksComposer; use App\Http\Composers\VersionComposer; +use App\Models\Role; use App\Models\User; use App\Notifications\Channels\Discord\DiscordWebhook; use App\Policies\Filament\ActivityPolicy; use App\Services\ModuleService; +use App\Services\PermissionRegistry; use App\Services\RouteForge\Contracts\LintRule; use App\Services\RouteForge\LintRunner; use App\Services\RouteForge\Rules\ExistingDuplicates; @@ -105,7 +107,7 @@ class AppServiceProvider extends ServiceProvider * Str::nanoid() generates an ID via the hidehalo/nanoid client using * the project's alphabet/length; Str::isNanoid() validates one. */ - Str::macro('nanoid', fn (int $length = BaseModel::ID_MAX_LENGTH): string => (new NanoidClient($length))->formattedId(BaseModel::ID_ALPHABET, $length)); + Str::macro('nanoid', fn (int $length = BaseModel::ID_MAX_LENGTH): string => new NanoidClient($length)->formattedId(BaseModel::ID_ALPHABET, $length)); Str::macro('isNanoid', fn (mixed $value): bool => is_string($value) && preg_match('/^['.BaseModel::ID_ALPHABET.']{'.BaseModel::ID_MAX_LENGTH.'}$/', $value) === 1); @@ -114,6 +116,11 @@ class AppServiceProvider extends ServiceProvider /** * Gates (i.e. Authentication) definition */ + // Super-admins bypass every permission/policy check. Replaces the + // removed filament-shield super_admin gate. Return null (not false) so + // non-super-admins fall through to the normal checks. + Gate::before(static fn (?User $user): ?bool => $user?->hasRole(Role::superAdminName()) ? true : null); + Gate::define('access_admin', static fn (?User $user): Response => $user?->hasAdminAccess() ? Response::allow() : Response::deny('You do not have permission to access this page.')); @@ -206,6 +213,10 @@ class AppServiceProvider extends ServiceProvider // nav views) must see the same instance, so it has to be a singleton. $this->app->singleton(ModuleService::class); + // Permission registry: modules register custom permissions into the + // same instance during boot(), so it must be a singleton. + $this->app->singleton(PermissionRegistry::class); + // RouteForge lint catalog: tag every concrete rule class so adding a // rule means appending one entry here, not editing LintRunner. The // bind below materializes the tagged generator into the runner's diff --git a/config/permission.php b/config/permission.php index 6a2d8352..83ab4f6f 100644 --- a/config/permission.php +++ b/config/permission.php @@ -2,9 +2,9 @@ declare(strict_types=1); +use App\Models\Permission; +use App\Models\Role; use Spatie\Permission\DefaultTeamResolver; -use Spatie\Permission\Models\Permission; -use Spatie\Permission\Models\Role; return [