= 2a19940635 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.
2026-05-25 10:14:28 +00:00

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>