feat(permissions): add super-admin gate and model wiring

- Register a `Gate::before` hook so the super-admin role bypasses every
  permission check, replacing the removed filament-shield gate
- Add Role::superAdminName() as the single source for the bypass role
- Gate module panel access behind per-module `access:<module>`
  permissions via the PermissionRegistry, falling back to `view:modules`
- Bind PermissionRegistry as a singleton
- Point config/permission.php at the app's Role and Permission models
This commit is contained in:
Arthur Pariente 2026-06-14 11:13:50 +02:00
parent d64c4ac577
commit 8bb6914f6c
No known key found for this signature in database
4 changed files with 35 additions and 7 deletions

View File

@ -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()

View File

@ -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:<module>` 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;
}

View File

@ -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

View File

@ -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 [