Refactor: consolidate and replace permission enums for streamlined access management

- Replaced `AccessManager`, `Apps`, and `Settings` enums with modular enums like `RoleAndAppsPermissionEnum`, `AppPermissionEnum`, etc.
- Updated route, view, Livewire components, and tests to use new modular enums.
- Enhanced `DefaultRoleWithPermissionSeeder` and `PermissionSeeder` with updated enums.
- Added README in the Enums/Permissions directory to explain usage and naming conventions.
- Refactored dashboard sidebar and route definitions for better enum-based permission handling.
- Improved test coverage to validate new permission enums functionality.
This commit is contained in:
= 2026-05-25 06:10:07 +00:00
parent 4fe1765525
commit d7c2491fed
20 changed files with 218 additions and 63 deletions

View File

@ -1,11 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Enums\Permissions;
enum AccessManager: string
{
case RoleAndApps = 'role-and-apps';
case Users = 'users';
}

View File

@ -0,0 +1,14 @@
<?php
declare(strict_types=1);
namespace App\Enums\Permissions;
enum AppPermissionEnum: string
{
case Access = 'app:access';
case Create = 'app:create';
case Read = 'app:read';
case Update = 'app:update';
case Delete = 'app:delete';
}

View File

@ -1,11 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Enums\Permissions;
enum Apps: string
{
case AllApps = 'all-apps';
case Providers = 'providers';
}

View File

@ -0,0 +1,14 @@
<?php
declare(strict_types=1);
namespace App\Enums\Permissions;
enum ConnectionProviderPermissionEnum: string
{
case Access = 'connection-provider:access';
case Create = 'connection-provider:create';
case Read = 'connection-provider:read';
case Update = 'connection-provider:update';
case Delete = 'connection-provider:delete';
}

View File

@ -0,0 +1,14 @@
<?php
declare(strict_types=1);
namespace App\Enums\Permissions;
enum ProfilePermissionEnum: string
{
case Access = 'profile:access';
case Create = 'profile:create';
case Read = 'profile:read';
case Update = 'profile:update';
case Delete = 'profile:delete';
}

View File

@ -0,0 +1,34 @@
# Permissions Enums
This directory contains all the permissions enums.
Spatie/laravel-permission uses these enums to check permissions.
Any Permission enum added here will be automatically added to the database, when the
[PermissionSeeder](../../../database/seeders/PermissionSeeder.php) is run, no need to add it manually.
This is done via Reflection API.
`Only string backed are supported.`
## Value format of Enum
We are following the convention of `scope:resource:action` .
**File name** of the enum should be same as the **scope name**, and Enum *value* should be in the format of
*resource:action*.
Example:
UserPermissionEnum.php
```php
namespace App\Enums\Permissions;
enum UserPermissionEnum:string
{
case Create = 'user:create';
case Read = 'user:read';
case Update = 'user:update';
case Delete = 'user:delete';
}
// using the enum makes straight forward permission check
$user->can(UserPermissionEnum::Create);
@can(UserPermissionEnum::Create);
```

View File

@ -0,0 +1,14 @@
<?php
declare(strict_types=1);
namespace App\Enums\Permissions;
enum RoleAndAppsPermissionEnum: string
{
case Access = 'roles-and-apps:access';
case Create = 'roles-and-apps:create';
case Read = 'roles-and-apps:read';
case Update = 'roles-and-apps:update';
case Delete = 'roles-and-apps:delete';
}

View File

@ -0,0 +1,14 @@
<?php
declare(strict_types=1);
namespace App\Enums\Permissions;
enum SecurityPermissionEnum: string
{
case Access = 'security:access';
case Create = 'security:create';
case Read = 'security:read';
case Update = 'security:update';
case Delete = 'security:delete';
}

View File

@ -1,11 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Enums\Permissions;
enum Settings: string
{
case Profile = 'profile';
case Security = 'security';
}

View File

@ -0,0 +1,14 @@
<?php
declare(strict_types=1);
namespace App\Enums\Permissions;
enum UserPermissionEnum: string
{
case Access = 'user:access';
case Create = 'user:create';
case Read = 'user:read';
case Update = 'user:update';
case Delete = 'user:delete';
}

View File

@ -10,22 +10,59 @@
class AppPermission
{
/**
* Generates the standard permission string.
* The standard scope name for connected app permissions.
*/
public const SCOPE = 'app';
/**
* Generates the standard permission string following the scope:resource:action convention.
* Format: app:{slug}:{action}
*
* @param ConnectedAppPermissonEnum $permission The action to perform (e.g., 'use')
* @param string|ConnectedAppData $app The connected app object or its slug
*
* @return string The formatted permission name
*/
public static function name(ConnectedAppPermissonEnum $permission, string|ConnectedAppData $app): string
{
$slug = is_string($app) ? $app : $app->slug;
return "{$permission->value}-{$slug}";
return sprintf('%s:%s:%s', self::SCOPE, $slug, $permission->value);
}
/**
* @param string $permissionName This should be the name constructed by the {@see self::name()} method.
* Parses the permission string to extract the ConnectedAppPermissonEnum type (action).
*
* @param string $permissionName The constructed permission name
*
* @return ?ConnectedAppPermissonEnum The matching enum or null
*/
public static function type(string $permissionName): ?ConnectedAppPermissonEnum
{
$permission = explode('-', $permissionName);
$parts = explode(':', $permissionName);
return ConnectedAppPermissonEnum::tryFrom($permission[0]);
if (3 === count($parts) && self::SCOPE === $parts[0]) {
return ConnectedAppPermissonEnum::tryFrom($parts[2]);
}
return null;
}
/**
* Parses the permission string to extract the connected app slug (resource).
*
* @param string $permissionName The constructed permission name
*
* @return ?string The app slug or null
*/
public static function slug(string $permissionName): ?string
{
$parts = explode(':', $permissionName);
if (3 === count($parts) && self::SCOPE === $parts[0]) {
return $parts[1];
}
return null;
}
}

View File

@ -5,10 +5,14 @@
namespace App\Models;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Spatie\Activitylog\Models\Concerns\LogsActivity;
use Spatie\Activitylog\Support\LogOptions;
use Spatie\Permission\Models\Role as SpatieRole;
class Role extends SpatieRole
{
use LogsActivity;
public function apps(): BelongsToMany
{
return $this->belongsToMany(
@ -20,4 +24,12 @@ public function apps(): BelongsToMany
->using(AppRole::class)
->withPivot('id', 'duration');
}
public function getActivitylogOptions(): LogOptions
{
return LogOptions::defaults()
->logOnly(['name', 'priority'])
->logOnlyDirty()
->useLogName('role_management');
}
}

View File

@ -19,6 +19,7 @@ public function run(): void
$this->call(ConnectionProtocolSeeder::class);
$this->call(ConnectionProviderSeeder::class);
$this->call(PermissionSeeder::class);
$this->call(DefaultRoleWithPermissionSeeder::class);
$this->call(ExampleUserWithRoleSeeder::class);
}
}

View File

@ -5,7 +5,7 @@
namespace Database\Seeders;
use App\Enums\DefaultUserRole;
use App\Enums\Permissions\Settings;
use App\Enums\Permissions\{ProfilePermissionEnum, SecurityPermissionEnum};
use App\Models\Role;
use Illuminate\Database\Seeder;
use Spatie\Permission\Models\Permission;
@ -22,6 +22,6 @@ public function run(): void
private function syncUserPermissions(Role $role): void
{
$role->givePermissionTo(Settings::Profile->value, Settings::Security->value);
$role->givePermissionTo(ProfilePermissionEnum::Access->value, SecurityPermissionEnum::Access->value);
}
}

View File

@ -2,9 +2,7 @@
use App\Data\Ui\Sidebar\NavItemData;
use App\Data\Ui\Sidebar\NavSubItemData;
use App\Enums\Permissions\AccessManager;
use App\Enums\Permissions\Apps;
use App\Enums\Permissions\Settings;
use App\Enums\Permissions\{AppPermissionEnum, ConnectionProviderPermissionEnum, ProfilePermissionEnum, RoleAndAppsPermissionEnum, SecurityPermissionEnum, UserPermissionEnum};
use Livewire\Component;
use Spatie\LaravelData\DataCollection;
@ -45,13 +43,13 @@ public function navItems(): DataCollection
label: 'All Apps',
route: 'apps.index',
active_pattern: 'apps.index',
permission: Apps::AllApps
permission: AppPermissionEnum::Access
),
new NavSubItemData(
label: 'Providers',
route: 'apps.connectionProviders.index',
active_pattern: 'apps.connectionProviders.*',
permission: Apps::Providers
permission: ConnectionProviderPermissionEnum::Access
)
], DataCollection::class)
),
@ -65,13 +63,13 @@ public function navItems(): DataCollection
label: 'Roles and Apps',
route: 'access-manager.roles-and-apps.index',
active_pattern: 'access-manager.roles-and-apps.*',
permission: AccessManager::RoleAndApps
permission: RoleAndAppsPermissionEnum::Access
),
new NavSubItemData(
label: 'Users',
route: 'access-manager.users.index',
active_pattern: 'access-manager.users.*',
permission: AccessManager::Users
permission: UserPermissionEnum::Access
),
], DataCollection::class)
),
@ -85,13 +83,13 @@ public function navItems(): DataCollection
label: 'Profile',
route: 'profile.edit',
active_pattern: 'profile.edit',
permission: Settings::Profile
permission: ProfilePermissionEnum::Access
),
new NavSubItemData(
label: 'Security',
route: 'security.edit',
active_pattern: 'security.edit',
permission: Settings::Security
permission: SecurityPermissionEnum::Access
),
], DataCollection::class)
),

View File

@ -347,7 +347,7 @@ class="toggle-primary"
Cancel
</x-mary-button>
<x-mary-button type="submit" spinner="saveRoles" class="btn-primary">
Save Roles
Save
</x-mary-button>
</x-slot:actions>
</x-mary-form>

View File

@ -2,9 +2,11 @@
declare(strict_types=1);
use App\Enums\Permissions\{ProfilePermissionEnum, SecurityPermissionEnum};
use App\Livewire\Settings\{Appearance, Profile, Security};
use Illuminate\Support\Facades\Route;
use Laravel\Fortify\Features;
use Spatie\Permission\Middleware\PermissionMiddleware;
// Grouping by the "Settings" sidebar category
Route::middleware(['auth'])->prefix('settings')->group(function (): void {
@ -13,7 +15,9 @@
Route::redirect('/', '/settings/profile')->name('settings');
// Profile Settings
Route::livewire('profile', Profile::class)->name('profile.edit');
Route::livewire('profile', Profile::class)
->middleware(PermissionMiddleware::using(ProfilePermissionEnum::Access))
->name('profile.edit');
// Settings that require email verification
Route::middleware(['verified'])->group(function (): void {
@ -24,12 +28,15 @@
// Security Settings
Route::livewire('security', Security::class)
->middleware(
when(
Features::canManageTwoFactorAuthentication()
&& Features::optionEnabled(Features::twoFactorAuthentication(), 'confirmPassword'),
['password.confirm'],
[],
),
array_merge(
[PermissionMiddleware::using(SecurityPermissionEnum::Access)],
when(
Features::canManageTwoFactorAuthentication()
&& Features::optionEnabled(Features::twoFactorAuthentication(), 'confirmPassword'),
['password.confirm'],
[],
),
)
)
->name('security.edit');
});

View File

@ -3,7 +3,7 @@
declare(strict_types=1);
use App\Enums\DefaultUserRole;
use App\Enums\Permissions\{AccessManager, Apps};
use App\Enums\Permissions\{AppPermissionEnum, ConnectionProviderPermissionEnum, RoleAndAppsPermissionEnum, UserPermissionEnum};
use App\Http\Controllers\ResolveDashboardRouteController;
use Illuminate\Support\Facades\Route;
use Spatie\Permission\Middleware\{PermissionMiddleware, RoleMiddleware};
@ -23,28 +23,28 @@
Route::prefix('access-manager')->name('access-manager.')->group(function (): void {
Route::prefix('roles-and-apps')->name('roles-and-apps.')
->middleware(PermissionMiddleware::using(AccessManager::RoleAndApps))
->middleware(PermissionMiddleware::using(RoleAndAppsPermissionEnum::Access))
->group(function (): void {
Route::livewire('roles-and-apps', 'pages::access-manager.roles-and-apps.index')->name('index');
Route::livewire('roles-and-apps/{id}', 'pages::access-manager.roles-and-apps.show')->name('show');
});
Route::prefix('users')->name('users.')
->middleware(PermissionMiddleware::using(AccessManager::Users))
->middleware(PermissionMiddleware::using(UserPermissionEnum::Access))
->group(function (): void {
Route::livewire('users', 'pages::access-manager.users.index')->name('index');
});
});
Route::prefix('apps')->name('apps.')->group(function (): void {
Route::middleware(PermissionMiddleware::using(Apps::AllApps))->group(function (): void {
Route::middleware(PermissionMiddleware::using(AppPermissionEnum::Access))->group(function (): void {
Route::livewire('create', 'pages::connected-apps.create')->name('create');
Route::livewire('{id}/edit', 'pages::connected-apps.edit')->name('edit');
Route::livewire('/', 'pages::connected-apps.index')->name('index');
});
Route::prefix('connection-providers')->name('connectionProviders.')
->middleware(PermissionMiddleware::using(Apps::Providers))
->middleware(PermissionMiddleware::using(ConnectionProviderPermissionEnum::Access))
->group(function (): void {
Route::livewire('create', 'pages::connecton-providers.create')->name('create');
Route::livewire('{slug}/edit', 'pages::connecton-providers.edit')->name('edit');

View File

@ -2,12 +2,20 @@
declare(strict_types=1);
use App\Enums\Permissions\ProfilePermissionEnum;
use App\Livewire\Settings\Profile;
use App\Models\User;
use Livewire\Livewire;
use Spatie\Permission\Models\Permission;
beforeEach(function (): void {
Permission::findOrCreate(ProfilePermissionEnum::Access->value);
});
test('profile page is displayed', function (): void {
$this->actingAs($user = User::factory()->create());
$user = User::factory()->create();
$user->givePermissionTo(ProfilePermissionEnum::Access->value);
$this->actingAs($user);
$this->get('/settings/profile')->assertOk();
});

View File

@ -2,11 +2,13 @@
declare(strict_types=1);
use App\Enums\Permissions\SecurityPermissionEnum;
use App\Livewire\Settings\Security;
use App\Models\User;
use Illuminate\Support\Facades\Hash;
use Laravel\Fortify\Features;
use Livewire\Livewire;
use Spatie\Permission\Models\Permission;
beforeEach(function (): void {
$this->skipUnlessFortifyHas(Features::twoFactorAuthentication());
@ -15,10 +17,13 @@
'confirm' => true,
'confirmPassword' => true,
]);
Permission::findOrCreate(SecurityPermissionEnum::Access->value);
});
test('security settings page can be rendered', function (): void {
$user = User::factory()->create();
$user->givePermissionTo(SecurityPermissionEnum::Access->value);
$this->actingAs($user)
->withSession(['auth.password_confirmed_at' => time()])
@ -30,6 +35,7 @@
test('security settings page requires password confirmation when enabled', function (): void {
$user = User::factory()->create();
$user->givePermissionTo(SecurityPermissionEnum::Access->value);
$response = $this->actingAs($user)
->get(route('security.edit'));
@ -41,6 +47,7 @@
config(['fortify.features' => []]);
$user = User::factory()->create();
$user->givePermissionTo(SecurityPermissionEnum::Access->value);
$this->actingAs($user)
->withSession(['auth.password_confirmed_at' => time()])