= 562b4a220d Fix: Associate App Permission with Roles
- 'Use' permission of apps now synced with Role
- Added `PermissionData` DTO for standardized permission transformation.
- Expanded `AppsPermissionService` with `getAllPermissions` method for retrieving connected app permissions.
- Updated `AppRoleService` with methods for assigning and revoking app permissions to roles (`assignAppPermissionsRole`, `removeAppPermissionsRole`).
- Introduced `AppPermission::type` method for extracting permission type from names.
- Refined role and app interaction logic, including proper slug generation and app-role association.
2026-05-20 09:06:55 +00:00

442 lines
14 KiB
PHP

<?php
use App\Concerns\HandlesOperations;
use App\Data\Role\{AssignAppsToRoleData, AssignUsersToRoleData, RoleAppData};
use App\Data\Ui\TableHeader;
use App\Livewire\Forms\AssignAppToRoleForm;
use App\Models\Role;
use App\Services\AppRoleService;
use Livewire\Attributes\{Computed, Layout, Title};
use Livewire\Component;
use Spatie\LaravelData\Attributes\DataCollectionOf;
use Spatie\LaravelData\DataCollection;
/**
* @property-read DataCollection $apps
*/
new
#[Layout('layouts.app.sidebar')]
#[Title('Role Details')]
class extends Component {
use HandlesOperations;
public int $roleId = 0;
public string $roleName = '';
public bool $showAddAppModal = false;
// UI State for Editing
public bool $isEditingApp = false;
public ?int $editingAssignmentId = null;
public string $editingAppName = '';
public AssignAppToRoleForm $assignForm;
public array $appsSearchable = [];
public bool $showAddUserModal = false;
/** @var int[] */
public array $selectedUserIds = [];
public array $usersSearchable = [];
protected AppRoleService $appRoleService;
#[DataCollectionOf(TableHeader::class)]
public DataCollection $appHeaders;
#[DataCollectionOf(TableHeader::class)]
public DataCollection $userHeaders;
public function boot(AppRoleService $appRoleService): void
{
$this->appRoleService = $appRoleService;
}
public function mount(int $id): void
{
$this->attempt(
action: function () use ($id): void {
$role = Role::query()->findOrFail($id);
$this->roleId = $role->id;
$this->roleName = $role->name;
},
onError: function (): void {
$this->redirectRoute('access-manager.roles-and-apps.index', navigate: true);
},
showSuccess: false,
);
$this->appHeaders = TableHeader::collect([
new TableHeader(key: 'app_name', label: 'App'),
new TableHeader(key: 'duration', label: 'Duration (days)'),
], DataCollection::class);
$this->userHeaders = TableHeader::collect([
new TableHeader(key: 'name', label: 'Name'),
], DataCollection::class);
}
#[Computed]
public function apps(): DataCollection
{
return $this->appRoleService->getAppsForRole($this->roleId);
}
/**
* Get users assigned to this role via spatie/laravel-permission.
*/
#[Computed]
public function getRoleUsers(): DataCollection
{
return $this->appRoleService->getUsersForRole($this->roleId);
}
public function openAddAppModal(): void
{
$this->isEditingApp = false;
$this->editingAssignmentId = null;
$this->editingAppName = '';
$this->assignForm->reset();
$this->resetErrorBag();
$this->searchApps('');
$this->showAddAppModal = true;
}
public function openEditAppModal(int $assignmentId): void
{
$this->isEditingApp = true;
$this->editingAssignmentId = $assignmentId;
$this->resetErrorBag();
$assignment = collect($this->apps())->firstWhere('id', $assignmentId);
if ($assignment) {
$this->assignForm->appIds = [$assignment['appId'] ?? $assignment['id']];
$this->assignForm->duration = $assignment['duration'];
$this->editingAppName = $assignment['appName'];
}
$this->showAddAppModal = true;
}
public function saveApp(): void
{
$this->assignForm->validate();
if ($this->isEditingApp) {
$this->updateApp();
} else {
$this->assignApp();
}
}
public function searchApps(string $value = ''): void
{
$this->appsSearchable = $this
->appRoleService
->searchAppsForSelect($value)
->toArray();
}
public function selectAllApps(): void
{
$this->assignForm->appIds = $this->appRoleService->getAllAppIds();
$this->appsSearchable = $this->appRoleService->searchAppsForSelect('')->toArray();
}
public function clearApps(): void
{
$this->assignForm->appIds = [];
}
public function assignApp(): void
{
$this->attempt(
action: function (): void {
$this->appRoleService->assign(new AssignAppsToRoleData(
roleId: $this->roleId,
duration: $this->assignForm->duration,
appIds: $this->assignForm->appIds,
));
$this->assignForm->reset();
$this->showAddAppModal = false;
unset($this->apps);
},
successMessage: 'Apps assigned successfully!',
);
}
public function updateApp(): void
{
$this->attempt(
action: function (): void {
$this->appRoleService->assign(new AssignAppsToRoleData(
roleId: $this->roleId,
duration: $this->assignForm->duration,
appIds: $this->assignForm->appIds,
));
$this->assignForm->reset();
$this->showAddAppModal = false;
unset($this->apps);
},
successMessage: 'Apps updated successfully!',
);
}
public function detachApp(int $appId): void
{
$this->attempt(
action: function () use ($appId): void {
$this->appRoleService->detach($appId);
unset($this->apps);
},
successMessage: 'App removed from role.',
);
}
public function openAddUserModal(): void
{
$this->reset('selectedUserIds');
$this->resetErrorBag();
$this->searchUsers('');
$this->showAddUserModal = true;
}
public function searchUsers(string $value = ''): void
{
$this->usersSearchable = $this
->appRoleService
->searchUsersForSelect($value)
->toArray();
}
public function assignUser(): void
{
$this->validate([
'selectedUserIds' => 'required|array|min:1',
'selectedUserIds.*' => 'integer|exists:users,id',
]);
$this->attempt(
action: function (): void {
$this->appRoleService->assignUsersToRole(new AssignUsersToRoleData(
roleId: $this->roleId,
userIds: $this->selectedUserIds,
));
$this->reset('selectedUserIds');
$this->showAddUserModal = false;
unset($this->getRoleUsers);
},
successMessage: 'Users assigned successfully!',
);
}
public function detachUser(int $userId): void
{
$this->attempt(
action: function () use ($userId): void {
$this->appRoleService->detachUserFromRole($this->roleId, $userId);
unset($this->getRoleUsers);
},
successMessage: 'User removed from role.',
);
}
};
?>
<div>
<div class="mb-6 flex items-center justify-between">
<div>
<div class="flex items-center gap-3">
<x-mary-button
:link="route('access-manager.roles-and-apps.index')"
wire:navigate
icon="lucide.arrow-left"
class="btn-sm btn-ghost"
tooltip-right="Back To Roles & Apps"
/>
<h1 class="text-2xl font-semibold text-gray-900">{{ $roleName }}</h1>
</div>
</div>
</div>
<div class="flex flex-col md:flex-row gap-6 w-full">
<x-shared.card title="Connected Apps" icon="lucide-blocks" class="pb-2 w-full h-min">
<x-slot:actions>
<x-mary-button wire:click="openAddAppModal" icon="lucide.plus" class="btn-sm">
Add App
</x-mary-button>
</x-slot:actions>
@if ($this->apps->count())
<x-mary-table
:headers="$appHeaders->toArray()"
:rows="$this->apps->toArray()"
>
@scope('cell_app_name', $row)
<span class="font-medium text-gray-900">{{ $row['appName'] }}</span>
@endscope
@scope('cell_duration', $row)
{{ $row['duration'] }} days
@endscope
@scope('actions', $row)
<div class="flex gap-1">
<x-mary-button
icon="lucide.square-pen"
wire:click="openEditAppModal({{ $row['id'] }})"
spinner="openEditAppModal({{ $row['id'] }})"
class="btn-sm btn-soft btn-circle"
/>
<x-mary-button
icon="lucide.trash"
wire:click="detachApp({{ $row['id'] }})"
spinner="detachApp({{ $row['id'] }})"
class="btn-sm btn-error btn-soft btn-circle"
/>
</div>
@endscope
</x-mary-table>
@else
<p class="p-6 text-center text-sm text-gray-400">
No apps assigned to this role yet.
</p>
@endif
</x-shared.card>
<x-shared.card title="Users" icon="lucide-users-round" class="pb-2 w-full h-min">
<x-slot:actions>
<x-mary-button wire:click="openAddUserModal" icon="lucide.plus" class="btn-sm">
Add User
</x-mary-button>
</x-slot:actions>
@if ($this->getRoleUsers->count())
<x-mary-table
:headers="$userHeaders->toArray()"
:rows="$this->getRoleUsers->toArray()"
>
@scope('cell_name', $row)
<span class="font-medium text-gray-900">{{ $row['name'] }}</span>
@endscope
@scope('actions', $row)
<x-mary-button
icon="lucide.trash"
wire:click="detachUser({{ $row['id'] }})"
spinner="detachUser({{ $row['id'] }})"
variant="danger"
class="btn-sm btn-error btn-soft btn-circle"
/>
@endscope
</x-mary-table>
@else
<p class="p-6 text-center text-sm text-gray-400">
No users assigned to this role yet.
</p>
@endif
</x-shared.card>
</div>
<x-mary-modal wire:model="showAddAppModal"
title="{{ $isEditingApp ? 'Edit App Duration' : 'Add Apps to ' . $roleName }}">
<x-mary-form wire:submit="saveApp">
<div class="space-y-4">
@if(!$isEditingApp)
<div>
<div class="mb-1 flex items-end justify-between">
<label class="text-sm font-semibold text-gray-700">Connected Apps</label>
<div class="flex gap-3 text-xs">
<button type="button" wire:click="selectAllApps" class="text-primary hover:underline">
Select All
</button>
<button type="button" wire:click="clearApps" class="text-error hover:underline">
Clear
</button>
</div>
</div>
<x-mary-choices
wire:model="assignForm.appIds"
:options="$appsSearchable"
search-function="searchApps"
option-label="name"
option-value="id"
no-result-text="No apps found."
placeholder="Search apps..."
searchable
/>
</div>
@else
<x-mary-input
label="Connected App"
wire:model="editingAppName"
readonly
disabled
class="bg-gray-50 text-gray-500 cursor-not-allowed"
/>
@endif
<x-mary-input
label="Duration (days)"
wire:model="assignForm.duration"
id="assign-duration-input"
type="number"
min="1"
max="3650"
placeholder="30"
/>
</div>
<x-slot:actions>
<x-mary-button @click="$wire.showAddAppModal = false" class="btn-ghost">
Cancel
</x-mary-button>
<x-mary-button type="submit" icon="{{ $isEditingApp ? 'lucide.save' : 'lucide.link' }}"
spinner="saveApp">
{{ $isEditingApp ? 'Update Duration' : 'Assign Apps' }}
</x-mary-button>
</x-slot:actions>
</x-mary-form>
</x-mary-modal>
<x-mary-modal wire:model="showAddUserModal" title="Add User to {{ $roleName }}">
<x-mary-form wire:submit="assignUser">
<x-mary-choices
wire:model="selectedUserIds"
:options="$usersSearchable"
search-function="searchUsers"
option-label="name"
option-value="id"
no-result-text="No users found."
placeholder="Search users..."
searchable
label="Users"
/>
<x-slot:actions>
<x-mary-button @click="$wire.showAddUserModal = false" class="btn-ghost">
Cancel
</x-mary-button>
<x-mary-button type="submit" icon="lucide.user-plus" spinner="assignUser">
Assign User
</x-mary-button>
</x-slot:actions>
</x-mary-form>
</x-mary-modal>
</div>