Feat: add role seeding, dashboard routing
- Added `DefaultUserRole` and permissions enums (`AccessManager`, `Apps`, `Settings`) for centralizing role and permission constants. - Implemented `DashboardRoute` helper to resolve routes dynamically based on user roles. - Introduced seeders for default roles (`DefaultRoleWithPermissionSeeder`, `PermissionSeeder`, `ExampleUserWithRoleSeeder`) with role-permission assignments. - Created `EnumExtractor` utility for parsing enums and generating permissions. - Customized dashboard routes with role-based redirection logic. - Updated sidebar navigation to display role-based menu items with permission checks.
This commit is contained in:
parent
fd57ac57e1
commit
f02caef80c
@ -4,6 +4,7 @@
|
||||
|
||||
namespace App\Data\Ui\Sidebar;
|
||||
|
||||
use BackedEnum;
|
||||
use Spatie\LaravelData\Data;
|
||||
|
||||
final class NavSubItemData extends Data
|
||||
@ -12,5 +13,6 @@ public function __construct(
|
||||
public string $label,
|
||||
public string $route,
|
||||
public string $active_pattern,
|
||||
public ?BackedEnum $permission = null
|
||||
) {}
|
||||
}
|
||||
|
||||
14
app/Enums/DefaultUserRole.php
Normal file
14
app/Enums/DefaultUserRole.php
Normal file
@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
use App\Concerns\EnumValuesAsArray;
|
||||
|
||||
enum DefaultUserRole: string
|
||||
{
|
||||
use EnumValuesAsArray;
|
||||
case Admin = 'admin';
|
||||
case User = 'user';
|
||||
}
|
||||
11
app/Enums/Permissions/AccessManager.php
Normal file
11
app/Enums/Permissions/AccessManager.php
Normal file
@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Enums\Permissions;
|
||||
|
||||
enum AccessManager: string
|
||||
{
|
||||
case RoleAndApps = 'role-and-apps';
|
||||
case Users = 'users';
|
||||
}
|
||||
11
app/Enums/Permissions/Apps.php
Normal file
11
app/Enums/Permissions/Apps.php
Normal file
@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Enums\Permissions;
|
||||
|
||||
enum Apps: string
|
||||
{
|
||||
case AllApps = 'all-apps';
|
||||
case Providers = 'providers';
|
||||
}
|
||||
11
app/Enums/Permissions/Settings.php
Normal file
11
app/Enums/Permissions/Settings.php
Normal file
@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Enums\Permissions;
|
||||
|
||||
enum Settings: string
|
||||
{
|
||||
case Profile = 'profile';
|
||||
case Security = 'security';
|
||||
}
|
||||
26
app/Helpers/DashboardRoute.php
Normal file
26
app/Helpers/DashboardRoute.php
Normal file
@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Helpers;
|
||||
|
||||
use App\Enums\DefaultUserRole;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class DashboardRoute
|
||||
{
|
||||
/**
|
||||
* This function will resolve the dashboard route based on the user role.
|
||||
*/
|
||||
public static function resolve(Collection $roles): string
|
||||
{
|
||||
if ($roles->contains(DefaultUserRole::Admin->value)) {
|
||||
return 'dashboard.admin';
|
||||
}
|
||||
if ($roles->contains(DefaultUserRole::User->value)) {
|
||||
return 'dashboard.user';
|
||||
}
|
||||
|
||||
return 'home';
|
||||
}
|
||||
}
|
||||
90
app/Helpers/EnumExtractor.php
Normal file
90
app/Helpers/EnumExtractor.php
Normal file
@ -0,0 +1,90 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Helpers;
|
||||
|
||||
use FilesystemIterator;
|
||||
use Generator;
|
||||
use RecursiveDirectoryIterator;
|
||||
use RecursiveIteratorIterator;
|
||||
use SplFileInfo;
|
||||
|
||||
class EnumExtractor
|
||||
{
|
||||
private string $namespace;
|
||||
|
||||
private string $directory;
|
||||
|
||||
public function __construct(string $namespace, string $directory)
|
||||
{
|
||||
$this->namespace = mb_trim($namespace, '\\').'\\';
|
||||
$this->directory = mb_rtrim($directory, DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR;
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes the extraction process and returns an array of actual Enum objects.
|
||||
*/
|
||||
public function extract(): array
|
||||
{
|
||||
$enumObjects = [];
|
||||
|
||||
foreach ($this->getPhpFiles() as $file) {
|
||||
$className = $this->resolveClassName($file->getPathname());
|
||||
|
||||
$this->loadClassIfNeeded($className, $file->getPathname());
|
||||
|
||||
if (enum_exists($className)) {
|
||||
// $className::cases() returns an array of the actual Enum objects
|
||||
// e.g., [App\Enums\Status::Active, App\Enums\Status::Inactive]
|
||||
$enumObjects[$className] = $className::cases();
|
||||
}
|
||||
}
|
||||
|
||||
return $enumObjects;
|
||||
}
|
||||
|
||||
/**
|
||||
* Yields only PHP files from the target directory.
|
||||
*/
|
||||
private function getPhpFiles(): Generator
|
||||
{
|
||||
$iterator = new RecursiveIteratorIterator(
|
||||
new RecursiveDirectoryIterator($this->directory, FilesystemIterator::SKIP_DOTS)
|
||||
);
|
||||
|
||||
/** @var SplFileInfo $file */
|
||||
foreach ($iterator as $file) {
|
||||
if ('php' === $file->getExtension()) {
|
||||
yield $file;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a physical file path to a fully qualified PSR-4 class name.
|
||||
*/
|
||||
private function resolveClassName(string $filePath): string
|
||||
{
|
||||
// Strip the base directory from the path
|
||||
$relativePath = str_replace($this->directory, '', $filePath);
|
||||
|
||||
// Normalize slashes and remove the .php extension
|
||||
$normalizedPath = str_replace([DIRECTORY_SEPARATOR, '/', '.php'], ['\\', '\\', ''], $relativePath);
|
||||
|
||||
return $this->namespace.mb_ltrim($normalizedPath, '\\');
|
||||
}
|
||||
|
||||
/**
|
||||
* Requires the file into memory if it hasn't been autoloaded yet.
|
||||
*/
|
||||
private function loadClassIfNeeded(string $className, string $filePath): void
|
||||
{
|
||||
if (! class_exists($className, false)
|
||||
&& ! enum_exists($className, false)
|
||||
&& ! interface_exists($className, false)
|
||||
) {
|
||||
require_once $filePath;
|
||||
}
|
||||
}
|
||||
}
|
||||
19
app/Http/Controllers/ResolveDashboardRouteController.php
Normal file
19
app/Http/Controllers/ResolveDashboardRouteController.php
Normal file
@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Helpers\DashboardRoute;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class ResolveDashboardRouteController extends Controller
|
||||
{
|
||||
public function __invoke(Request $request)
|
||||
{
|
||||
$user = $request->user();
|
||||
$route = DashboardRoute::resolve($user->getRoleNames());
|
||||
|
||||
return to_route($route);
|
||||
}
|
||||
}
|
||||
28
app/Http/Responses/LoginResponse.php
Normal file
28
app/Http/Responses/LoginResponse.php
Normal file
@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Responses;
|
||||
|
||||
use Laravel\Fortify\Contracts\LoginResponse as LoginResponseContract;
|
||||
|
||||
/**
|
||||
* This class is used overide the fortify LoginResponse class, so that
|
||||
* we can redirect the user to the intended page after login.
|
||||
*/
|
||||
class LoginResponse implements LoginResponseContract
|
||||
{
|
||||
public function toResponse($request)
|
||||
{
|
||||
$user = $request->user();
|
||||
|
||||
activity()
|
||||
->causedBy($user)
|
||||
->withProperties([
|
||||
'roles' => $user->getRoleNames(),
|
||||
])
|
||||
->log('User logged in}');
|
||||
|
||||
return to_route('dashboard');
|
||||
}
|
||||
}
|
||||
@ -5,10 +5,12 @@
|
||||
namespace App\Providers;
|
||||
|
||||
use App\Actions\Fortify\{CreateNewUser, ResetUserPassword};
|
||||
use App\Http\Responses\LoginResponse;
|
||||
use Illuminate\Cache\RateLimiting\Limit;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\RateLimiter;
|
||||
use Illuminate\Support\{ServiceProvider, Str};
|
||||
use Laravel\Fortify\Contracts\LoginResponse as LoginResponseContract;
|
||||
use Laravel\Fortify\Fortify;
|
||||
|
||||
final class FortifyServiceProvider extends ServiceProvider
|
||||
@ -16,7 +18,13 @@ final class FortifyServiceProvider extends ServiceProvider
|
||||
/**
|
||||
* Register any application services.
|
||||
*/
|
||||
public function register(): void {}
|
||||
public function register(): void
|
||||
{
|
||||
$this->app->singleton(
|
||||
LoginResponseContract::class,
|
||||
LoginResponse::class
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Bootstrap any application services.
|
||||
|
||||
@ -4,7 +4,6 @@
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Seeder;
|
||||
|
||||
// use Illuminate\Database\Console\Seeds\WithoutModelEvents;
|
||||
@ -16,15 +15,10 @@ final class DatabaseSeeder extends Seeder
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
// User::factory(10)->create();
|
||||
|
||||
User::factory()->create([
|
||||
'name' => 'Test User',
|
||||
'email' => 'test@example.com',
|
||||
]);
|
||||
|
||||
$this->call(ConnectionStatusSeeder::class);
|
||||
$this->call(ConnectionProtocolSeeder::class);
|
||||
$this->call(ConnectionProviderSeeder::class);
|
||||
$this->call(PermissionSeeder::class);
|
||||
$this->call(ExampleUserWithRoleSeeder::class);
|
||||
}
|
||||
}
|
||||
|
||||
27
database/seeders/DefaultRoleWithPermissionSeeder.php
Normal file
27
database/seeders/DefaultRoleWithPermissionSeeder.php
Normal file
@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Enums\DefaultUserRole;
|
||||
use App\Enums\Permissions\Settings;
|
||||
use App\Models\Role;
|
||||
use Illuminate\Database\Seeder;
|
||||
use Spatie\Permission\Models\Permission;
|
||||
|
||||
class DefaultRoleWithPermissionSeeder extends Seeder
|
||||
{
|
||||
public function run(): void
|
||||
{
|
||||
$role = Role::findOrCreate(DefaultUserRole::Admin->value);
|
||||
$role->givePermissionTo(Permission::all());
|
||||
$role = Role::findOrCreate(DefaultUserRole::User->value);
|
||||
$this->syncUserPermissions($role);
|
||||
}
|
||||
|
||||
private function syncUserPermissions(Role $role): void
|
||||
{
|
||||
$role->givePermissionTo(Settings::Profile->value, Settings::Security->value);
|
||||
}
|
||||
}
|
||||
49
database/seeders/ExampleUserWithRoleSeeder.php
Normal file
49
database/seeders/ExampleUserWithRoleSeeder.php
Normal file
@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Enums\DefaultUserRole;
|
||||
use App\Models\{Role, User};
|
||||
use Illuminate\Database\Seeder;
|
||||
|
||||
class ExampleUserWithRoleSeeder extends Seeder
|
||||
{
|
||||
public function run(): void
|
||||
{
|
||||
/**
|
||||
* Create a user for each role;
|
||||
*/
|
||||
$this->syncAdminRole();
|
||||
$this->syncUserRole();
|
||||
}
|
||||
|
||||
private function syncAdminRole(): void
|
||||
{
|
||||
$role = Role::findOrCreate(DefaultUserRole::Admin->value);
|
||||
// Check if user exists by email, otherwise create via factory
|
||||
$user = User::firstWhere('email', 'admin@example.com')
|
||||
?? User::factory()->create([
|
||||
'name' => 'Admin',
|
||||
'email' => 'admin@example.com',
|
||||
'password' => 'password',
|
||||
]);
|
||||
|
||||
$user->assignRole($role);
|
||||
}
|
||||
|
||||
private function syncUserRole(): void
|
||||
{
|
||||
$role = Role::findOrCreate(DefaultUserRole::User->value);
|
||||
|
||||
$user = User::firstWhere('email', 'test@example.com')
|
||||
?? User::factory()->create([
|
||||
'name' => 'Test User',
|
||||
'email' => 'test@example.com',
|
||||
'password' => 'password',
|
||||
]);
|
||||
$user->assignRole($role);
|
||||
|
||||
}
|
||||
}
|
||||
23
database/seeders/PermissionSeeder.php
Normal file
23
database/seeders/PermissionSeeder.php
Normal file
@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Helpers\EnumExtractor;
|
||||
use Illuminate\Database\Seeder;
|
||||
use Spatie\Permission\Models\Permission;
|
||||
|
||||
class PermissionSeeder extends Seeder
|
||||
{
|
||||
public function run(): void
|
||||
{
|
||||
$enumExtractor = new EnumExtractor('App\\Enums\\Permissions', __DIR__.'/../../app/Enums/Permissions');
|
||||
$allEnums = $enumExtractor->extract();
|
||||
foreach ($allEnums as $enumCases) {
|
||||
foreach ($enumCases as $case) {
|
||||
Permission::create(['name' => $case->value]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -2,6 +2,9 @@
|
||||
|
||||
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 Livewire\Component;
|
||||
use Spatie\LaravelData\DataCollection;
|
||||
|
||||
@ -25,7 +28,7 @@ public function toggle(): void
|
||||
*/
|
||||
public function navItems(): DataCollection
|
||||
{
|
||||
return NavItemData::collect([
|
||||
$items = NavItemData::collect([
|
||||
new NavItemData(
|
||||
label: 'Dashboard',
|
||||
icon: 'lucide.house',
|
||||
@ -41,12 +44,14 @@ public function navItems(): DataCollection
|
||||
new NavSubItemData(
|
||||
label: 'All Apps',
|
||||
route: 'apps.index',
|
||||
active_pattern: 'apps.index'
|
||||
active_pattern: 'apps.index',
|
||||
permission: Apps::AllApps
|
||||
),
|
||||
new NavSubItemData(
|
||||
label: 'Providers',
|
||||
route: 'apps.connectionProviders.index',
|
||||
active_pattern: 'apps.connectionProviders.*'
|
||||
active_pattern: 'apps.connectionProviders.*',
|
||||
permission: Apps::Providers
|
||||
)
|
||||
], DataCollection::class)
|
||||
),
|
||||
@ -60,11 +65,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
|
||||
),
|
||||
new NavSubItemData(
|
||||
label: 'Users',
|
||||
route: 'access-manager.users.index',
|
||||
active_pattern: 'access-manager.users.*',
|
||||
permission: AccessManager::Users
|
||||
),
|
||||
], DataCollection::class)
|
||||
),
|
||||
@ -78,15 +85,52 @@ public function navItems(): DataCollection
|
||||
label: 'Profile',
|
||||
route: 'profile.edit',
|
||||
active_pattern: 'profile.edit',
|
||||
permission: Settings::Profile
|
||||
),
|
||||
new NavSubItemData(
|
||||
label: 'Security',
|
||||
route: 'security.edit',
|
||||
active_pattern: 'security.edit',
|
||||
permission: Settings::Security
|
||||
),
|
||||
], DataCollection::class)
|
||||
),
|
||||
], DataCollection::class);
|
||||
|
||||
// Filter out items the user shouldn't see
|
||||
return $this->filterNavMenu($items);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter out menus that the current user does not have permission.
|
||||
* @param DataCollection<NavItemData> $items
|
||||
* @returns DataCollection<NavItemData>
|
||||
*/
|
||||
private function filterNavMenu(DataCollection $items): DataCollection
|
||||
{
|
||||
$filteredItems = $items->toCollection()->filter(function (NavItemData $item) {
|
||||
if ($item->sub_menu) {
|
||||
$allowedSubItems = $item->sub_menu->toCollection()->filter(function (NavSubItemData $subItem) {
|
||||
$permission = $subItem->permission instanceof UnitEnum
|
||||
? $subItem->permission->value
|
||||
: $subItem->permission;
|
||||
|
||||
return auth()->user()->can($permission);
|
||||
});
|
||||
|
||||
// If the user has NO access to any submenus, discard the parent menu item
|
||||
if ($allowedSubItems->isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Update the DataCollection with authorized items
|
||||
$item->sub_menu = NavSubItemData::collect($allowedSubItems, DataCollection::class);
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
return NavItemData::collect($filteredItems->values()->all(), DataCollection::class);
|
||||
}
|
||||
};
|
||||
?>
|
||||
@ -193,17 +237,19 @@ class="absolute right-full top-1/2 -translate-y-1/2 border-[5px] border-transpar
|
||||
<div class="mt-1 space-y-1 pl-9 pr-2">
|
||||
@foreach ($item->sub_menu as $subItem)
|
||||
@php $isSubActive = request()->routeIs($subItem->active_pattern); @endphp
|
||||
<a
|
||||
href="{{ route($subItem->route) }}"
|
||||
wire:navigate
|
||||
@class([
|
||||
'block px-3 py-2 rounded-md text-sm font-medium transition-colors',
|
||||
'bg-gray-100 text-blue-700' => $isSubActive,
|
||||
'text-gray-500 hover:bg-gray-50 hover:text-gray-900' => ! $isSubActive,
|
||||
])
|
||||
>
|
||||
{{ $subItem->label }}
|
||||
</a>
|
||||
@can($subItem->permission)
|
||||
<a
|
||||
href="{{ route($subItem->route) }}"
|
||||
wire:navigate
|
||||
@class([
|
||||
'block px-3 py-2 rounded-md text-sm font-medium transition-colors',
|
||||
'bg-gray-100 text-blue-700' => $isSubActive,
|
||||
'text-gray-500 hover:bg-gray-50 hover:text-gray-900' => ! $isSubActive,
|
||||
])
|
||||
>
|
||||
{{ $subItem->label }}
|
||||
</a>
|
||||
@endcan
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
|
||||
13
resources/views/pages/dashboards/⚡user.blade.php
Normal file
13
resources/views/pages/dashboards/⚡user.blade.php
Normal file
@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
use Livewire\Component;
|
||||
|
||||
new class extends Component
|
||||
{
|
||||
//
|
||||
};
|
||||
?>
|
||||
|
||||
<div>
|
||||
{{-- People find pleasure in different ways. I find it in keeping my mind clear. - Marcus Aurelius --}}
|
||||
</div>
|
||||
@ -2,12 +2,17 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Http\Controllers\ResolveDashboardRouteController;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
Route::view('/', 'welcome')->name('home');
|
||||
|
||||
Route::group(['middleware' => ['auth', 'verified']], function (): void {
|
||||
Route::livewire('dashboard', 'pages::dashboard')->name('dashboard');
|
||||
Route::get('dashboard', ResolveDashboardRouteController::class)->name('dashboard');
|
||||
Route::prefix('dashboard')->name('dashboard.')->group(function (): void {
|
||||
Route::livewire('/user', 'pages::dashboards.user')->name('user');
|
||||
Route::livewire('admin', 'pages::dashboards.admin')->name('admin');
|
||||
});
|
||||
|
||||
Route::prefix('access-manager')->name('access-manager.')->group(function (): void {
|
||||
Route::prefix('roles-and-apps')->name('roles-and-apps.')->group(function (): void {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user