- '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.
442 lines
14 KiB
PHP
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>
|