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:
parent
d024308c2e
commit
2a19940635
@ -15,15 +15,19 @@ public function __construct(
|
||||
public int $id,
|
||||
public string $name,
|
||||
public ?ConnectedAppPermissonEnum $type,
|
||||
public string $resource = 'general',
|
||||
) {}
|
||||
|
||||
public static function fromModel(Permission $permission): self
|
||||
{
|
||||
$parts = explode(':', $permission->name);
|
||||
$resource = count($parts) > 1 ? $parts[0] : 'general';
|
||||
|
||||
return new self(
|
||||
id: $permission->id,
|
||||
name: $permission->name,
|
||||
type: AppPermission::type($permission->name),
|
||||
resource: $resource,
|
||||
);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
14
app/Enums/Permissions/PermissionPermissionEnum.php
Normal file
14
app/Enums/Permissions/PermissionPermissionEnum.php
Normal 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';
|
||||
}
|
||||
@ -5,15 +5,18 @@
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Spatie\Activitylog\Models\Concerns\LogsActivity;
|
||||
use Spatie\Activitylog\Support\LogOptions;
|
||||
use Spatie\Permission\Models\Role as SpatieRole;
|
||||
use Spatie\Permission\Models\{Permission, Role as SpatieRole};
|
||||
|
||||
/**
|
||||
* @property int $priority
|
||||
* @property AppRole $pivot
|
||||
* @property ConnectedApp $apps
|
||||
*
|
||||
* @method static Builder<Role> visible()
|
||||
*/
|
||||
#[Fillable(['priority'])]
|
||||
class Role extends SpatieRole
|
||||
@ -39,4 +42,28 @@ public function getActivitylogOptions(): LogOptions
|
||||
->logOnlyDirty()
|
||||
->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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -142,11 +142,19 @@ public function getAppsForRole(int $roleId): DataCollection
|
||||
#[NoDiscard]
|
||||
public function searchAppsForSelect(string $search = ''): Collection
|
||||
{
|
||||
return ConnectedApp::query()
|
||||
->select(['id', 'name'])
|
||||
->when($search, function ($query, $search): void {
|
||||
$query->where('name', 'like', '%'.$search.'%');
|
||||
})
|
||||
$query = ConnectedApp::query()
|
||||
->select(['id', 'name']);
|
||||
|
||||
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
|
||||
->orderBy('name')
|
||||
->get(['id', 'name']);
|
||||
@ -160,7 +168,16 @@ public function searchAppsForSelect(string $search = ''): Collection
|
||||
#[NoDiscard]
|
||||
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]
|
||||
public function searchUsersForSelect(string $search = ''): DataCollection
|
||||
{
|
||||
$users = User::query()
|
||||
->select(['id', 'name'])
|
||||
->when($search, function ($query, $search): void {
|
||||
$query->where('name', 'like', '%'.$search.'%');
|
||||
})
|
||||
$query = User::query()
|
||||
->select(['id', 'name']);
|
||||
|
||||
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)
|
||||
->orderBy('name')
|
||||
->get();
|
||||
|
||||
64
app/Services/PermissionManagerService.php
Normal file
64
app/Services/PermissionManagerService.php
Normal 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;
|
||||
}
|
||||
}
|
||||
@ -56,6 +56,7 @@ public function delete(int $id): void
|
||||
public function getAll(): PaginatedDataCollection
|
||||
{
|
||||
$roles = Role::query()
|
||||
->visible()
|
||||
->with(['apps:id,name', 'users:id,name'])
|
||||
->orderBy('id')
|
||||
->paginate();
|
||||
@ -66,6 +67,7 @@ public function getAll(): PaginatedDataCollection
|
||||
public function searchRolesForSelect(string $search = ''): Collection
|
||||
{
|
||||
return Role::query()
|
||||
->visible()
|
||||
->when('' !== $search, fn ($query) => $query->where('name', 'like', "%{$search}%"))
|
||||
->select('id', 'name')
|
||||
->get();
|
||||
@ -73,7 +75,7 @@ public function searchRolesForSelect(string $search = ''): Collection
|
||||
|
||||
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()
|
||||
->visible()
|
||||
->with('permissions:id,name')
|
||||
->whereIn('id', $roleIds)
|
||||
->get(['id', 'name', 'priority']);
|
||||
|
||||
@ -25,7 +25,7 @@ public function getAll(): PaginatedDataCollection
|
||||
{
|
||||
$users = User::query()
|
||||
->with([
|
||||
'roles:id,name',
|
||||
'roles:id,name,priority',
|
||||
'roles.apps:id,name',
|
||||
'roles.permissions:id,name',
|
||||
'restrictedPermissions',
|
||||
@ -64,7 +64,19 @@ public function assignRoles(int $userId, array $roleIds): void
|
||||
|
||||
DB::transaction(function () use ($userId, $roleIds): void {
|
||||
$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 {
|
||||
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();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -15,8 +15,10 @@ class DefaultRoleWithPermissionSeeder extends Seeder
|
||||
public function run(): void
|
||||
{
|
||||
$role = Role::findOrCreate(DefaultUserRole::Admin->value);
|
||||
$role->update(['priority' => 0]);
|
||||
$role->givePermissionTo(Permission::all());
|
||||
$role = Role::findOrCreate(DefaultUserRole::User->value);
|
||||
$role->update(['priority' => 100]);
|
||||
$this->syncUserPermissions($role);
|
||||
}
|
||||
|
||||
|
||||
@ -16,7 +16,7 @@ public function run(): void
|
||||
$allEnums = $enumExtractor->extract();
|
||||
foreach ($allEnums as $enumCases) {
|
||||
foreach ($enumCases as $case) {
|
||||
Permission::create(['name' => $case->value]);
|
||||
Permission::findOrCreate($case->value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
use App\Data\Ui\Sidebar\NavItemData;
|
||||
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 Spatie\LaravelData\DataCollection;
|
||||
|
||||
@ -65,6 +65,12 @@ public function navItems(): DataCollection
|
||||
active_pattern: 'access-manager.roles-and-apps.*',
|
||||
permission: RoleAndAppsPermissionEnum::Access
|
||||
),
|
||||
new NavSubItemData(
|
||||
label: 'Permission Manager',
|
||||
route: 'access-manager.permissions.index',
|
||||
active_pattern: 'access-manager.permissions.*',
|
||||
permission: PermissionPermissionEnum::Access
|
||||
),
|
||||
new NavSubItemData(
|
||||
label: 'Users',
|
||||
route: 'access-manager.users.index',
|
||||
|
||||
@ -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>
|
||||
@ -83,7 +83,15 @@ public function openCreateModal(): void
|
||||
|
||||
public function save(): void
|
||||
{
|
||||
$userPriority = auth()->user()?->roles()->min('priority') ?? 999999;
|
||||
|
||||
$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(
|
||||
action: function (): void {
|
||||
$role = $this->roleService->create(new CreateRoleData(
|
||||
|
||||
@ -64,9 +64,14 @@ public function boot(AppRoleService $appRoleService, RoleService $roleService):
|
||||
|
||||
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(
|
||||
action: function () use ($id): void {
|
||||
$role = Role::query()->findOrFail($id);
|
||||
action: function () use ($role): void {
|
||||
$this->roleId = $role->id;
|
||||
$this->roleName = $role->name;
|
||||
$this->rolePriority = $role->priority;
|
||||
@ -277,8 +282,12 @@ public function openEditPriorityModal(): void
|
||||
|
||||
public function savePriority(): void
|
||||
{
|
||||
$userPriority = auth()->user()?->roles()->min('priority') ?? 999999;
|
||||
|
||||
$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(
|
||||
|
||||
@ -68,10 +68,18 @@ public function openAssignRolesModal(int $userId): void
|
||||
{
|
||||
$this->attempt(
|
||||
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->form->reset();
|
||||
$this->searchRoles();
|
||||
$this->form->roleIds = User::findOrFail($userId)->roles()->pluck('id')->toArray();
|
||||
$this->form->roleIds = $user->roles()->pluck('id')->toArray();
|
||||
$this->hydratePermissionInForm();
|
||||
$this->showAssignRolesModal = true;
|
||||
},
|
||||
@ -213,8 +221,13 @@ public function saveRoles(): void
|
||||
|
||||
@scope('cell_roles', $row)
|
||||
<div class="flex flex-wrap gap-1">
|
||||
@php
|
||||
$userPriority = auth()->user()?->roles()->min('priority') ?? 999999;
|
||||
@endphp
|
||||
@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
|
||||
</div>
|
||||
@endscope
|
||||
@ -223,9 +236,10 @@ public function saveRoles(): void
|
||||
<div class="flex flex-wrap gap-1">
|
||||
@php
|
||||
$displayedPermissions = [];
|
||||
$userPriority = auth()->user()?->roles()->min('priority') ?? 999999;
|
||||
@endphp
|
||||
@foreach($row->roles as $role)
|
||||
@if($role->permissions)
|
||||
@if($role->priority > $userPriority && $role->permissions)
|
||||
@foreach($role->permissions as $permission)
|
||||
@if(!in_array($permission->id, $displayedPermissions))
|
||||
@php
|
||||
@ -247,8 +261,11 @@ public function saveRoles(): void
|
||||
|
||||
@scope('cell_apps', $row)
|
||||
<div class="flex flex-wrap gap-1">
|
||||
@php
|
||||
$userPriority = auth()->user()?->roles()->min('priority') ?? 999999;
|
||||
@endphp
|
||||
@foreach($row->roles as $role)
|
||||
@if($role->apps)
|
||||
@if($role->priority > $userPriority && $role->apps)
|
||||
@foreach ($role->apps as $app)
|
||||
<x-mary-badge class="badge-soft badge-secondary" :value="$app->appName"/>
|
||||
@endforeach
|
||||
@ -258,23 +275,30 @@ public function saveRoles(): void
|
||||
@endscope
|
||||
|
||||
@scope('actions', $row)
|
||||
<div class="flex gap-1">
|
||||
<x-mary-button
|
||||
icon="lucide.shield-plus"
|
||||
wire:click="openAssignRolesModal({{ $row->id }})"
|
||||
tooltip="Manage roles"
|
||||
class="btn-sm btn-circle btn-soft"
|
||||
spinner="openAssignRolesModal({{ $row->id }})"
|
||||
/>
|
||||
<x-mary-button
|
||||
icon="lucide.trash"
|
||||
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>
|
||||
@php
|
||||
$userPriority = auth()->user()?->roles()->min('priority') ?? 999999;
|
||||
$rowMinPriority = collect($row->roles)->min('priority') ?? 999999;
|
||||
$isManageable = $rowMinPriority > $userPriority;
|
||||
@endphp
|
||||
@if($isManageable)
|
||||
<div class="flex gap-1">
|
||||
<x-mary-button
|
||||
icon="lucide.shield-plus"
|
||||
wire:click="openAssignRolesModal({{ $row->id }})"
|
||||
tooltip="Manage roles"
|
||||
class="btn-sm btn-circle btn-soft"
|
||||
spinner="openAssignRolesModal({{ $row->id }})"
|
||||
/>
|
||||
<x-mary-button
|
||||
icon="lucide.trash"
|
||||
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
|
||||
</x-mary-table>
|
||||
</x-shared.card>
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
declare(strict_types=1);
|
||||
|
||||
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 Illuminate\Support\Facades\Route;
|
||||
use Spatie\Permission\Middleware\{PermissionMiddleware, RoleMiddleware};
|
||||
@ -12,6 +12,7 @@
|
||||
|
||||
Route::group(['middleware' => ['auth', 'verified']], function (): void {
|
||||
Route::get('dashboard', ResolveDashboardRouteController::class)->name('dashboard');
|
||||
|
||||
Route::prefix('dashboard')->name('dashboard.')->group(function (): void {
|
||||
Route::livewire('/user', 'pages::dashboards.user')
|
||||
->middleware(RoleMiddleware::using(DefaultUserRole::User))
|
||||
@ -34,6 +35,12 @@
|
||||
->group(function (): void {
|
||||
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 {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user