Feat: implement role-based priority restrictions and permission manager

- Added `priority`-based access logic to restrict role and user management actions.
- Introduced `Permission Manager` UI and routes for permission access control.
- Updated `RoleService` and `AppRoleService` to include `visible` scope for priority filtering.
- Enhanced seeding to assign priorities to default roles and ensure proper hierarchy.
- Updated sidebar, routes, and data structure to handle new permission enums and resource segmentation.
- Improved UI components to dynamically restrict visibility and actions based on role priority.
This commit is contained in:
= 2026-05-25 10:12:42 +00:00
parent d024308c2e
commit 2a19940635
15 changed files with 472 additions and 44 deletions

View File

@ -15,15 +15,19 @@ public function __construct(
public int $id, public int $id,
public string $name, public string $name,
public ?ConnectedAppPermissonEnum $type, public ?ConnectedAppPermissonEnum $type,
public string $resource = 'general',
) {} ) {}
public static function fromModel(Permission $permission): self public static function fromModel(Permission $permission): self
{ {
$parts = explode(':', $permission->name);
$resource = count($parts) > 1 ? $parts[0] : 'general';
return new self( return new self(
id: $permission->id, id: $permission->id,
name: $permission->name, name: $permission->name,
type: AppPermission::type($permission->name), type: AppPermission::type($permission->name),
resource: $resource,
); );
} }
} }

View File

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

View File

@ -5,15 +5,18 @@
namespace App\Models; namespace App\Models;
use Illuminate\Database\Eloquent\Attributes\Fillable; use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Spatie\Activitylog\Models\Concerns\LogsActivity; use Spatie\Activitylog\Models\Concerns\LogsActivity;
use Spatie\Activitylog\Support\LogOptions; use Spatie\Activitylog\Support\LogOptions;
use Spatie\Permission\Models\Role as SpatieRole; use Spatie\Permission\Models\{Permission, Role as SpatieRole};
/** /**
* @property int $priority * @property int $priority
* @property AppRole $pivot * @property AppRole $pivot
* @property ConnectedApp $apps * @property ConnectedApp $apps
*
* @method static Builder<Role> visible()
*/ */
#[Fillable(['priority'])] #[Fillable(['priority'])]
class Role extends SpatieRole class Role extends SpatieRole
@ -39,4 +42,28 @@ public function getActivitylogOptions(): LogOptions
->logOnlyDirty() ->logOnlyDirty()
->useLogName('role_management'); ->useLogName('role_management');
} }
/**
* Override the Spatie permissions relationship to provide precise type-hinting for Larastan.
*
* @return BelongsToMany<Permission, Role>
*/
public function permissions(): BelongsToMany
{
return parent::permissions();
}
/**
* Scope a query to only include roles with priority strictly greater than the logged in user's priority (lower privilege).
*/
public function scopeVisible(Builder $query): Builder
{
if (! auth()->check()) {
return $query;
}
$userPriority = auth()->user()?->roles()->min('priority') ?? 999999;
return $query->where('priority', '>', $userPriority);
}
} }

View File

@ -142,11 +142,19 @@ public function getAppsForRole(int $roleId): DataCollection
#[NoDiscard] #[NoDiscard]
public function searchAppsForSelect(string $search = ''): Collection public function searchAppsForSelect(string $search = ''): Collection
{ {
return ConnectedApp::query() $query = ConnectedApp::query()
->select(['id', 'name']) ->select(['id', 'name']);
->when($search, function ($query, $search): void {
$query->where('name', 'like', '%'.$search.'%'); if (auth()->check()) {
}) $userPriority = auth()->user()?->roles()->min('priority') ?? 999999;
$query->whereHas('roles', function ($q) use ($userPriority): void {
$q->where('priority', '>', $userPriority);
});
}
return $query->when($search, function ($q, $search): void {
$q->where('name', 'like', '%'.$search.'%');
})
->limit(50) // Limit the results so the dropdown doesn't lag ->limit(50) // Limit the results so the dropdown doesn't lag
->orderBy('name') ->orderBy('name')
->get(['id', 'name']); ->get(['id', 'name']);
@ -160,7 +168,16 @@ public function searchAppsForSelect(string $search = ''): Collection
#[NoDiscard] #[NoDiscard]
public function getAllAppIds(): array public function getAllAppIds(): array
{ {
return ConnectedApp::pluck('id')->toArray(); $query = ConnectedApp::query();
if (auth()->check()) {
$userPriority = auth()->user()?->roles()->min('priority') ?? 999999;
$query->whereHas('roles', function ($q) use ($userPriority): void {
$q->where('priority', '>', $userPriority);
});
}
return $query->pluck('id')->toArray();
} }
/** /**
@ -241,11 +258,19 @@ public function getUsersForRole(int $roleId): DataCollection
#[NoDiscard] #[NoDiscard]
public function searchUsersForSelect(string $search = ''): DataCollection public function searchUsersForSelect(string $search = ''): DataCollection
{ {
$users = User::query() $query = User::query()
->select(['id', 'name']) ->select(['id', 'name']);
->when($search, function ($query, $search): void {
$query->where('name', 'like', '%'.$search.'%'); if (auth()->check()) {
}) $userPriority = auth()->user()?->roles()->min('priority') ?? 999999;
$query->whereDoesntHave('roles', function ($q) use ($userPriority): void {
$q->where('priority', '<=', $userPriority);
});
}
$users = $query->when($search, function ($q, $search): void {
$q->where('name', 'like', '%'.$search.'%');
})
->limit(50) ->limit(50)
->orderBy('name') ->orderBy('name')
->get(); ->get();

View File

@ -0,0 +1,64 @@
<?php
declare(strict_types=1);
namespace App\Services;
use App\Data\Permissions\PermissionData;
use App\Data\Role\RoleData;
use App\Helpers\AppPermission;
use App\Models\Role;
use NoDiscard;
use Spatie\LaravelData\{DataCollection, PaginatedDataCollection};
use Spatie\Permission\Models\Permission;
final class PermissionManagerService
{
/**
* Returns a Paginated Collection of roles with their permissions and users DTO.
*
* @return PaginatedDataCollection<int, RoleData>
*/
#[NoDiscard]
public function getRolesWithPermissionsAndUsers(): PaginatedDataCollection
{
$roles = Role::query()
->visible()
->with(['permissions:id,name', 'users:id,name'])
->orderBy('priority', 'asc')
->paginate();
return RoleData::collect($roles, PaginatedDataCollection::class);
}
/**
* Get all non-app system permissions mapped to the PermissionData DTO.
*
* @return DataCollection<int, PermissionData>
*/
#[NoDiscard]
public function getNonAppPermissions(): DataCollection
{
$permissions = Permission::all()
->filter(fn ($p) => null === AppPermission::type($p->name));
return PermissionData::collect($permissions, DataCollection::class);
}
/**
* Get a role by its ID, ensuring it has a priority strictly greater than the logged in user's priority.
*
* @throws \Symfony\Component\HttpKernel\Exception\HttpException
*/
public function getRoleForEditing(int $roleId): Role
{
$role = Role::query()->findOrFail($roleId);
$userPriority = auth()->user()?->roles()->min('priority') ?? 999999;
if ($role->priority <= $userPriority) {
abort(403, 'Unauthorized to manage permissions for this role.');
}
return $role;
}
}

View File

@ -56,6 +56,7 @@ public function delete(int $id): void
public function getAll(): PaginatedDataCollection public function getAll(): PaginatedDataCollection
{ {
$roles = Role::query() $roles = Role::query()
->visible()
->with(['apps:id,name', 'users:id,name']) ->with(['apps:id,name', 'users:id,name'])
->orderBy('id') ->orderBy('id')
->paginate(); ->paginate();
@ -66,6 +67,7 @@ public function getAll(): PaginatedDataCollection
public function searchRolesForSelect(string $search = ''): Collection public function searchRolesForSelect(string $search = ''): Collection
{ {
return Role::query() return Role::query()
->visible()
->when('' !== $search, fn ($query) => $query->where('name', 'like', "%{$search}%")) ->when('' !== $search, fn ($query) => $query->where('name', 'like', "%{$search}%"))
->select('id', 'name') ->select('id', 'name')
->get(); ->get();
@ -73,7 +75,7 @@ public function searchRolesForSelect(string $search = ''): Collection
public function getAllRoleIds(): array public function getAllRoleIds(): array
{ {
return Role::query()->pluck('id')->toArray(); return Role::query()->visible()->pluck('id')->toArray();
} }
/** /**
@ -92,6 +94,7 @@ public function getPermissionsGroupedByRole(array $roleIds): ?DataCollection
} }
$roles = Role::query() $roles = Role::query()
->visible()
->with('permissions:id,name') ->with('permissions:id,name')
->whereIn('id', $roleIds) ->whereIn('id', $roleIds)
->get(['id', 'name', 'priority']); ->get(['id', 'name', 'priority']);

View File

@ -25,7 +25,7 @@ public function getAll(): PaginatedDataCollection
{ {
$users = User::query() $users = User::query()
->with([ ->with([
'roles:id,name', 'roles:id,name,priority',
'roles.apps:id,name', 'roles.apps:id,name',
'roles.permissions:id,name', 'roles.permissions:id,name',
'restrictedPermissions', 'restrictedPermissions',
@ -64,7 +64,19 @@ public function assignRoles(int $userId, array $roleIds): void
DB::transaction(function () use ($userId, $roleIds): void { DB::transaction(function () use ($userId, $roleIds): void {
$user = User::query()->findOrFail($userId); $user = User::query()->findOrFail($userId);
$user->syncRoles($roleIds);
$userPriority = auth()->user()?->roles()->min('priority') ?? 999999;
// Get user's existing roles that have priority <= $userPriority (which are forbidden to the logged in user)
$forbiddenRoleIds = $user->roles()
->where('priority', '<=', $userPriority)
->pluck('id')
->toArray();
// Merge them with the newly assigned roles so they are preserved
$mergedRoleIds = array_values(array_unique(array_merge($forbiddenRoleIds, $roleIds)));
$user->syncRoles($mergedRoleIds);
}); });
} }
@ -111,7 +123,16 @@ public function delete(int $id): void
} }
DB::transaction(function () use ($id): void { DB::transaction(function () use ($id): void {
User::query()->findOrFail($id)->delete(); $user = User::query()->findOrFail($id);
$userPriority = auth()->user()?->roles()->min('priority') ?? 999999;
$targetUserMinPriority = $user->roles()->min('priority') ?? 999999;
if ($targetUserMinPriority <= $userPriority) {
abort(403, 'Unauthorized to delete this user due to role priority hierarchy.');
}
$user->delete();
}); });
} }
} }

View File

@ -15,8 +15,10 @@ class DefaultRoleWithPermissionSeeder extends Seeder
public function run(): void public function run(): void
{ {
$role = Role::findOrCreate(DefaultUserRole::Admin->value); $role = Role::findOrCreate(DefaultUserRole::Admin->value);
$role->update(['priority' => 0]);
$role->givePermissionTo(Permission::all()); $role->givePermissionTo(Permission::all());
$role = Role::findOrCreate(DefaultUserRole::User->value); $role = Role::findOrCreate(DefaultUserRole::User->value);
$role->update(['priority' => 100]);
$this->syncUserPermissions($role); $this->syncUserPermissions($role);
} }

View File

@ -16,7 +16,7 @@ public function run(): void
$allEnums = $enumExtractor->extract(); $allEnums = $enumExtractor->extract();
foreach ($allEnums as $enumCases) { foreach ($allEnums as $enumCases) {
foreach ($enumCases as $case) { foreach ($enumCases as $case) {
Permission::create(['name' => $case->value]); Permission::findOrCreate($case->value);
} }
} }
} }

View File

@ -2,7 +2,7 @@
use App\Data\Ui\Sidebar\NavItemData; use App\Data\Ui\Sidebar\NavItemData;
use App\Data\Ui\Sidebar\NavSubItemData; use App\Data\Ui\Sidebar\NavSubItemData;
use App\Enums\Permissions\{AppPermissionEnum, ConnectionProviderPermissionEnum, ProfilePermissionEnum, RoleAndAppsPermissionEnum, SecurityPermissionEnum, UserPermissionEnum}; use App\Enums\Permissions\{AppPermissionEnum, ConnectionProviderPermissionEnum, ProfilePermissionEnum, RoleAndAppsPermissionEnum, SecurityPermissionEnum, UserPermissionEnum, PermissionPermissionEnum};
use Livewire\Component; use Livewire\Component;
use Spatie\LaravelData\DataCollection; use Spatie\LaravelData\DataCollection;
@ -65,6 +65,12 @@ public function navItems(): DataCollection
active_pattern: 'access-manager.roles-and-apps.*', active_pattern: 'access-manager.roles-and-apps.*',
permission: RoleAndAppsPermissionEnum::Access permission: RoleAndAppsPermissionEnum::Access
), ),
new NavSubItemData(
label: 'Permission Manager',
route: 'access-manager.permissions.index',
active_pattern: 'access-manager.permissions.*',
permission: PermissionPermissionEnum::Access
),
new NavSubItemData( new NavSubItemData(
label: 'Users', label: 'Users',
route: 'access-manager.users.index', route: 'access-manager.users.index',

View File

@ -0,0 +1,214 @@
<?php
use App\Concerns\Confirmation;
use App\Concerns\HandlesOperations;
use App\Data\Role\RoleData;
use App\Services\PermissionManagerService;
use App\Data\Ui\TableHeader;
use Livewire\Attributes\{Computed, Title};
use Livewire\Component;
use Spatie\LaravelData\Attributes\DataCollectionOf;
use Spatie\LaravelData\DataCollection;
use Spatie\LaravelData\PaginatedDataCollection;
use App\Models\Role;
use Spatie\Permission\Models\Permission;
use App\Helpers\AppPermission;
use App\Enums\Permissions\PermissionPermissionEnum;
new
#[Title('Permission Manager')]
class extends Component {
use HandlesOperations, Confirmation;
protected PermissionManagerService $permissionManagerService;
#[DataCollectionOf(TableHeader::class)]
public DataCollection $headers;
public bool $showEditModal = false;
public int $roleId = 0;
public string $roleName = '';
public array $selectedPermissionIds = [];
public array $nonAppPermissions = [];
public function boot(PermissionManagerService $permissionManagerService): void
{
$this->permissionManagerService = $permissionManagerService;
}
public function mount(): void
{
$this->headers = TableHeader::collect([
new TableHeader(key: 'name', label: 'Role'),
new TableHeader(key: 'priority', label: 'Priority'),
new TableHeader(key: 'permissions', label: 'Permissions'),
new TableHeader(key: 'users', label: 'Added Users'),
], DataCollection::class);
}
/**
* @return PaginatedDataCollection<int, RoleData>
*/
#[Computed]
public function getRoles(): PaginatedDataCollection
{
return $this->permissionManagerService->getRolesWithPermissionsAndUsers();
}
public function openEditModal(int $roleId): void
{
$this->authorize(PermissionPermissionEnum::Update->value);
$this->attempt(
action: function () use ($roleId): void {
$role = $this->permissionManagerService->getRoleForEditing($roleId);
$this->roleId = $role->id;
$this->roleName = $role->name;
$this->nonAppPermissions = $this->permissionManagerService->getNonAppPermissions()->toCollection()
->groupBy('resource')
->map(fn($group) => $group->toArray())
->toArray();
$this->selectedPermissionIds = $role->permissions()
->pluck('id')
->map(fn($id) => (int) $id)
->toArray();
$this->showEditModal = true;
},
showSuccess: false,
);
}
public function savePermissions(): void
{
$this->authorize(PermissionPermissionEnum::Update->value);
$this->attempt(
action: function (): void {
$role = $this->permissionManagerService->getRoleForEditing($this->roleId);
$existingAppPermissions = $role->permissions()
->get()
->filter(fn($p) => AppPermission::type($p->name) !== null)
->pluck('name')
->toArray();
$selectedNames = Permission::query()
->whereIn('id', $this->selectedPermissionIds)
->pluck('name')
->toArray();
$finalNames = array_values(array_unique(array_merge($existingAppPermissions, $selectedNames)));
$role->syncPermissions($finalNames);
$this->showEditModal = false;
unset($this->getRoles);
},
successMessage: 'Permissions updated successfully!',
);
}
};
?>
<div>
<x-shared.card title="Permission Manager" icon="lucide-shield-alert" class="pb-2">
<x-mary-table
:headers="$headers->toArray()"
:rows="$this->getRoles->items()"
>
@scope('cell_name', $row)
<span class="font-semibold text-gray-900">{{ $row->name }}</span>
@endscope
@scope('cell_priority', $row)
<span class="text-gray-500 font-medium">{{ $row->priority }}</span>
@endscope
@scope('cell_permissions', $row)
<div class="flex flex-wrap gap-1 max-w-xl">
@if($row->permissions && $row->permissions->count())
@foreach($row->permissions as $permission)
@if(AppPermission::type($permission->name) === null)
<x-mary-badge class="badge-soft badge-primary text-[11px]" :value="$permission->name"/>
@else
<x-mary-badge class="badge-soft badge-secondary text-[11px]" :value="$permission->name"/>
@endif
@endforeach
@else
<span class="text-xs text-gray-400">No permissions</span>
@endif
</div>
@endscope
@scope('cell_users', $row)
<div class="flex -space-x-2 overflow-hidden">
@if($row->users && $row->users->count())
@foreach($row->users as $user)
<x-mary-avatar :placeholder="$user->initials"
class="w-8! h-8! bg-blue-100 text-blue-600 border-2 border-white ring-0"
tooltip="{{ $user->name }}"/>
@endforeach
@else
<span class="text-xs text-gray-400">No users</span>
@endif
</div>
@endscope
@scope('actions', $row)
@php
$userPriority = auth()->user()?->roles()->min('priority') ?? 999999;
$isManageable = $row->priority > $userPriority;
@endphp
@if($isManageable)
<div class="flex gap-1">
@can(PermissionPermissionEnum::Update->value)
<x-mary-button
icon="lucide.pencil"
wire:click="openEditModal({{ $row->id }})"
spinner="openEditModal({{ $row->id }})"
tooltip="Edit permissions"
class="btn-sm btn-circle btn-soft"
/>
@endcan
</div>
@endif
@endscope
</x-mary-table>
</x-shared.card>
<x-mary-modal wire:model="showEditModal" title="Manage Permissions for {{ $roleName }}">
<x-mary-form wire:submit="savePermissions">
<div class="space-y-4 max-h-[60vh] overflow-y-auto pr-2">
@foreach ($nonAppPermissions as $resource => $permissions)
<div class="rounded-xl border border-base-300 bg-base-200/50 p-4">
<h3 class="text-sm font-semibold text-gray-800 mb-3 capitalize">
{{ ucwords(str_replace('-', ' ', $resource)) }}
</h3>
<div class="grid grid-cols-1 gap-3 sm:grid-cols-2">
@foreach ($permissions as $permission)
<x-mary-toggle
class="toggle-primary"
:label="explode(':', $permission['name'])[1] ?? $permission['name']"
wire:model="selectedPermissionIds"
:value="$permission['id']"
/>
@endforeach
</div>
</div>
@endforeach
</div>
<x-slot:actions>
<x-mary-button @click="$wire.showEditModal = false" class="btn-ghost">
Cancel
</x-mary-button>
<x-mary-button type="submit" class="btn-primary" icon="lucide.save" spinner="savePermissions">
Save
</x-mary-button>
</x-slot:actions>
</x-mary-form>
</x-mary-modal>
</div>

View File

@ -83,7 +83,15 @@ public function openCreateModal(): void
public function save(): void public function save(): void
{ {
$userPriority = auth()->user()?->roles()->min('priority') ?? 999999;
$this->form->validate(); $this->form->validate();
if ((int) $this->form->priority <= $userPriority) {
$this->addError('form.priority', 'The priority must be greater than your own highest role priority (' . $userPriority . ').');
return;
}
$this->attempt( $this->attempt(
action: function (): void { action: function (): void {
$role = $this->roleService->create(new CreateRoleData( $role = $this->roleService->create(new CreateRoleData(

View File

@ -64,9 +64,14 @@ public function boot(AppRoleService $appRoleService, RoleService $roleService):
public function mount(int $id): void public function mount(int $id): void
{ {
$role = Role::query()->findOrFail($id);
$userPriority = auth()->user()?->roles()->min('priority') ?? 999999;
if ($role->priority <= $userPriority) {
abort(403, 'Unauthorized access to this role.');
}
$this->attempt( $this->attempt(
action: function () use ($id): void { action: function () use ($role): void {
$role = Role::query()->findOrFail($id);
$this->roleId = $role->id; $this->roleId = $role->id;
$this->roleName = $role->name; $this->roleName = $role->name;
$this->rolePriority = $role->priority; $this->rolePriority = $role->priority;
@ -277,8 +282,12 @@ public function openEditPriorityModal(): void
public function savePriority(): void public function savePriority(): void
{ {
$userPriority = auth()->user()?->roles()->min('priority') ?? 999999;
$this->validate([ $this->validate([
'newPriority' => 'required|integer|min:0', 'newPriority' => 'required|integer|min:' . ($userPriority + 1),
], [
'newPriority.min' => 'The priority must be greater than your own highest role priority (' . $userPriority . ').',
]); ]);
$this->attempt( $this->attempt(

View File

@ -68,10 +68,18 @@ public function openAssignRolesModal(int $userId): void
{ {
$this->attempt( $this->attempt(
action: function () use ($userId) { action: function () use ($userId) {
$user = User::findOrFail($userId);
$userPriority = auth()->user()?->roles()->min('priority') ?? 999999;
$targetUserMinPriority = $user->roles()->min('priority') ?? 999999;
if ($targetUserMinPriority <= $userPriority) {
abort(403, 'Unauthorized to manage this user.');
}
$this->currentUserId = $userId; $this->currentUserId = $userId;
$this->form->reset(); $this->form->reset();
$this->searchRoles(); $this->searchRoles();
$this->form->roleIds = User::findOrFail($userId)->roles()->pluck('id')->toArray(); $this->form->roleIds = $user->roles()->pluck('id')->toArray();
$this->hydratePermissionInForm(); $this->hydratePermissionInForm();
$this->showAssignRolesModal = true; $this->showAssignRolesModal = true;
}, },
@ -213,8 +221,13 @@ public function saveRoles(): void
@scope('cell_roles', $row) @scope('cell_roles', $row)
<div class="flex flex-wrap gap-1"> <div class="flex flex-wrap gap-1">
@php
$userPriority = auth()->user()?->roles()->min('priority') ?? 999999;
@endphp
@foreach($row->roles as $role) @foreach($row->roles as $role)
<x-mary-badge class="badge-soft badge-neutral" :value="ucfirst($role->name)"/> @if($role->priority > $userPriority)
<x-mary-badge class="badge-soft badge-neutral" :value="ucfirst($role->name)"/>
@endif
@endforeach @endforeach
</div> </div>
@endscope @endscope
@ -223,9 +236,10 @@ public function saveRoles(): void
<div class="flex flex-wrap gap-1"> <div class="flex flex-wrap gap-1">
@php @php
$displayedPermissions = []; $displayedPermissions = [];
$userPriority = auth()->user()?->roles()->min('priority') ?? 999999;
@endphp @endphp
@foreach($row->roles as $role) @foreach($row->roles as $role)
@if($role->permissions) @if($role->priority > $userPriority && $role->permissions)
@foreach($role->permissions as $permission) @foreach($role->permissions as $permission)
@if(!in_array($permission->id, $displayedPermissions)) @if(!in_array($permission->id, $displayedPermissions))
@php @php
@ -247,8 +261,11 @@ public function saveRoles(): void
@scope('cell_apps', $row) @scope('cell_apps', $row)
<div class="flex flex-wrap gap-1"> <div class="flex flex-wrap gap-1">
@php
$userPriority = auth()->user()?->roles()->min('priority') ?? 999999;
@endphp
@foreach($row->roles as $role) @foreach($row->roles as $role)
@if($role->apps) @if($role->priority > $userPriority && $role->apps)
@foreach ($role->apps as $app) @foreach ($role->apps as $app)
<x-mary-badge class="badge-soft badge-secondary" :value="$app->appName"/> <x-mary-badge class="badge-soft badge-secondary" :value="$app->appName"/>
@endforeach @endforeach
@ -258,23 +275,30 @@ public function saveRoles(): void
@endscope @endscope
@scope('actions', $row) @scope('actions', $row)
<div class="flex gap-1"> @php
<x-mary-button $userPriority = auth()->user()?->roles()->min('priority') ?? 999999;
icon="lucide.shield-plus" $rowMinPriority = collect($row->roles)->min('priority') ?? 999999;
wire:click="openAssignRolesModal({{ $row->id }})" $isManageable = $rowMinPriority > $userPriority;
tooltip="Manage roles" @endphp
class="btn-sm btn-circle btn-soft" @if($isManageable)
spinner="openAssignRolesModal({{ $row->id }})" <div class="flex gap-1">
/> <x-mary-button
<x-mary-button icon="lucide.shield-plus"
icon="lucide.trash" wire:click="openAssignRolesModal({{ $row->id }})"
wire:click="requireConfirmation('delete', {{ $row->id }});" tooltip="Manage roles"
spinner="requireConfirmation('delete', {{ $row->id }});" class="btn-sm btn-circle btn-soft"
variant="danger" spinner="openAssignRolesModal({{ $row->id }})"
:tooltip="__('Delete user')" />
class="btn-sm btn-circle btn-error btn-soft" <x-mary-button
/> icon="lucide.trash"
</div> wire:click="requireConfirmation('delete', {{ $row->id }});"
spinner="requireConfirmation('delete', {{ $row->id }});"
variant="danger"
:tooltip="__('Delete user')"
class="btn-sm btn-circle btn-error btn-soft"
/>
</div>
@endif
@endscope @endscope
</x-mary-table> </x-mary-table>
</x-shared.card> </x-shared.card>

View File

@ -3,7 +3,7 @@
declare(strict_types=1); declare(strict_types=1);
use App\Enums\DefaultUserRole; use App\Enums\DefaultUserRole;
use App\Enums\Permissions\{AppPermissionEnum, ConnectionProviderPermissionEnum, RoleAndAppsPermissionEnum, UserPermissionEnum}; use App\Enums\Permissions\{AppPermissionEnum, ConnectionProviderPermissionEnum, PermissionPermissionEnum, RoleAndAppsPermissionEnum, UserPermissionEnum};
use App\Http\Controllers\ResolveDashboardRouteController; use App\Http\Controllers\ResolveDashboardRouteController;
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
use Spatie\Permission\Middleware\{PermissionMiddleware, RoleMiddleware}; use Spatie\Permission\Middleware\{PermissionMiddleware, RoleMiddleware};
@ -12,6 +12,7 @@
Route::group(['middleware' => ['auth', 'verified']], function (): void { Route::group(['middleware' => ['auth', 'verified']], function (): void {
Route::get('dashboard', ResolveDashboardRouteController::class)->name('dashboard'); Route::get('dashboard', ResolveDashboardRouteController::class)->name('dashboard');
Route::prefix('dashboard')->name('dashboard.')->group(function (): void { Route::prefix('dashboard')->name('dashboard.')->group(function (): void {
Route::livewire('/user', 'pages::dashboards.user') Route::livewire('/user', 'pages::dashboards.user')
->middleware(RoleMiddleware::using(DefaultUserRole::User)) ->middleware(RoleMiddleware::using(DefaultUserRole::User))
@ -34,6 +35,12 @@
->group(function (): void { ->group(function (): void {
Route::livewire('users', 'pages::access-manager.users.index')->name('index'); Route::livewire('users', 'pages::access-manager.users.index')->name('index');
}); });
Route::prefix('permissions')->name('permissions.')
->middleware(PermissionMiddleware::using(PermissionPermissionEnum::Access))
->group(function (): void {
Route::livewire('/', 'pages::access-manager.permissions.index')->name('index');
});
}); });
Route::prefix('apps')->name('apps.')->group(function (): void { Route::prefix('apps')->name('apps.')->group(function (): void {