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.
This commit is contained in:
= 2026-05-20 09:06:55 +00:00
parent 5a593ffdb5
commit 562b4a220d
7 changed files with 106 additions and 8 deletions

View File

@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
namespace App\Data\Permissions;
use App\Enums\ConnectedAppPermissonEnum;
use App\Helpers\AppPermission;
use Spatie\LaravelData\Data;
use Spatie\Permission\Models\Permission;
class PermissionData extends Data
{
public function __construct(
public int $id,
public string $name,
public ConnectedAppPermissonEnum $type,
) {}
public static function fromModel(Permission $permission): self
{
return new self(
id: $permission->id,
name: $permission->name,
type: AppPermission::type($permission->name),
);
}
}

View File

@ -18,4 +18,14 @@ public static function name(ConnectedAppPermissonEnum $permission, string|Connec
return "{$permission->value}-{$slug}";
}
/**
* @param string $permissionName This should be the name constructed by the {@see self::name()} method.
*/
public static function type(string $permissionName): ConnectedAppPermissonEnum
{
$permission = explode('-', $permissionName);
return ConnectedAppPermissonEnum::from($permission[0]);
}
}

View File

@ -13,8 +13,13 @@
use Spatie\LaravelData\DataCollection;
use Throwable;
final class AppRoleService
final readonly class AppRoleService
{
public function __construct(
private ConnectedAppService $connectedAppService,
private AppsPermissionService $appsPermissionService,
) {}
/**
* Assign connected apps to a role, updating the duration if already assigned.
*
@ -25,6 +30,9 @@ public function assign(AssignAppsToRoleData $data): void
if (empty($data->appIds)) {
return;
}
$role = Role::query()->findOrFail($data->roleId);
$this->assignAppPermissionsRole($role, $data->appIds);
$records = array_map(fn (int $appId) => [
'role_id' => $data->roleId,
@ -37,6 +45,21 @@ public function assign(AssignAppsToRoleData $data): void
['role_id', 'app_id'],
['duration']
);
}
/**
* @param array<int, int> $appIds Apps, of which all permissions will be assigned to the role.
*/
private function assignAppPermissionsRole(Role $role, array $appIds): void
{
array_map(function ($appId) use ($role): void {
$app = $this->connectedAppService->getConnectedApp($appId);
$permissions = $this->appsPermissionService->getAllPermissions($app);
$permissions
->toCollection()
->map(fn ($permission) => $role->givePermissionTo($permission->id));
}, $appIds);
}
/**
@ -52,10 +75,27 @@ public function detach(int $id): void
}
DB::transaction(function () use ($id): void {
AppRole::query()->findOrFail($id)->delete();
$appRole = AppRole::query()->findOrFail($id);
$role = Role::query()->findOrFail($appRole->role_id);
$this->removeAppPermissionsRole($role, $appRole->app_id);
$appRole->delete();
});
}
/**
* @param array<int,int>|int $appIds
*/
private function removeAppPermissionsRole(Role $role, ...$appIds): void
{
array_map(function ($appId) use ($role): void {
$app = $this->connectedAppService->getConnectedApp($appId);
$permissions = $this->appsPermissionService->getAllPermissions($app);
$permissions
->toCollection()
->map(fn ($permission) => $role->revokePermissionTo($permission->name));
}, $appIds);
}
/**
* Get all apps assigned to a given role.
*
@ -74,7 +114,7 @@ public function getAppsForRole(int $roleId): DataCollection
$assignments->map(fn (AppRole $row): array => [
'id' => $row->id,
'appId' => $row->app_id,
'appName' => $row->app->name,
'appName' => $row->app->name ?? '',
'duration' => $row->duration,
])->all(),
DataCollection::class,

View File

@ -5,10 +5,12 @@
namespace App\Services;
use App\Data\ConnectedApp\ConnectedAppData;
use App\Data\Permissions\PermissionData;
use App\Enums\ConnectedAppPermissonEnum;
use App\Helpers\AppPermission;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Spatie\LaravelData\DataCollection;
use Spatie\Permission\Models\Permission;
use Throwable;
@ -29,4 +31,20 @@ public function add(ConnectedAppData $connectedApp, ConnectedAppPermissonEnum $p
Permission::firstOrCreate(['name' => $permissionName]);
});
}
/**
* Returns all permissions for the connected app.
*
* @return DataCollection<int, PermissionData>
*/
public function getAllPermissions(ConnectedAppData $connectedApp): DataCollection
{
if (! $connectedApp->slug) {
$connectedApp->slug = Str::slug($connectedApp->name);
}
$permissionName = AppPermission::name(ConnectedAppPermissonEnum::Use, $connectedApp);
$permissions = Permission::where('name', 'like', $permissionName)->get();
return PermissionData::collect($permissions, DataCollection::class);
}
}

View File

@ -22,6 +22,7 @@ public function __construct(
/**
* Create a new connected app.
* By Default creates a use permission for the app.
*
* @throws Throwable when an error occurs during the creation.
*/

View File

@ -2,7 +2,7 @@
declare(strict_types=1);
use app\Models\Role;
use App\Models\Role;
use Spatie\Permission\DefaultTeamResolver;
use Spatie\Permission\Models\Permission;

View File

@ -64,7 +64,7 @@ public function mount(int $id): void
$this->roleName = $role->name;
},
onError: function (): void {
$this->redirectRoute('access-manager.roles-and-apps', navigate: true);
$this->redirectRoute('access-manager.roles-and-apps.index', navigate: true);
},
showSuccess: false,
);
@ -192,11 +192,11 @@ public function updateApp(): void
);
}
public function detachApp(int $assignmentId): void
public function detachApp(int $appId): void
{
$this->attempt(
action: function () use ($assignmentId): void {
$this->appRoleService->detach($assignmentId);
action: function () use ($appId): void {
$this->appRoleService->detach($appId);
unset($this->apps);
},
successMessage: 'App removed from role.',