- 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.
215 lines
8.0 KiB
PHP
215 lines
8.0 KiB
PHP
<?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>
|