Feat: add ConnectedAppFactory, MockDataSeeder, and role-app connection logic

- Introduced `ConnectedAppFactory` for generating connected app data.
- Added `MockDataSeeder` to create sample roles, users, and apps with dynamic role-app assignments.
- Extended `RoleAndAppsPermissionEnum` with `ConnectApps` permission.
- Updated access-manager UI to handle app-connection actions with proper authorization.
- Integrated app-role connection logic in Livewire component and updated `roles-and-apps` view for permissions-based actions.
This commit is contained in:
= 2026-05-25 07:01:45 +00:00
parent cd29191c7b
commit d024308c2e
6 changed files with 144 additions and 16 deletions

View File

@ -11,4 +11,5 @@ enum RoleAndAppsPermissionEnum: string
case Read = 'roles-and-apps:read'; case Read = 'roles-and-apps:read';
case Update = 'roles-and-apps:update'; case Update = 'roles-and-apps:update';
case Delete = 'roles-and-apps:delete'; case Delete = 'roles-and-apps:delete';
case ConnectApps = 'roles-and-apps:connect-apps';
} }

View File

@ -5,6 +5,7 @@
namespace App\Models; namespace App\Models;
use Illuminate\Database\Eloquent\Attributes\Fillable; use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\{Model, Relations\BelongsTo, Relations\BelongsToMany, SoftDeletes}; use Illuminate\Database\Eloquent\{Model, Relations\BelongsTo, Relations\BelongsToMany, SoftDeletes};
use Illuminate\Database\Eloquent\Relations\HasOne; use Illuminate\Database\Eloquent\Relations\HasOne;
use Spatie\Activitylog\Models\Concerns\LogsActivity; use Spatie\Activitylog\Models\Concerns\LogsActivity;
@ -24,7 +25,7 @@
])] ])]
class ConnectedApp extends Model class ConnectedApp extends Model
{ {
use LogsActivity, SoftDeletes; use HasFactory, LogsActivity, SoftDeletes;
/** /**
* @return HasOne<ConnectionProtocol, $this> * @return HasOne<ConnectionProtocol, $this>

View File

@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace Database\Factories;
use App\Models\{ConnectedApp, ConnectionProtocol, ConnectionProvider, ConnectionStatus};
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Str;
/**
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\ConnectedApp>
*/
class ConnectedAppFactory extends Factory
{
protected $model = ConnectedApp::class;
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
public function definition(): array
{
$name = $this->faker->unique()->company().' '.$this->faker->randomElement(['SSO', 'Portal', 'OAuth', 'API', 'App']);
return [
'name' => $name,
'slug' => Str::slug($name),
'connection_protocol_id' => ConnectionProtocol::inRandomOrder()->first()?->id ?? 1,
'connection_provider_id' => ConnectionProvider::inRandomOrder()->first()?->id ?? 1,
'connection_status_id' => ConnectionStatus::inRandomOrder()->first()?->id ?? 1,
];
}
}

View File

@ -21,5 +21,6 @@ public function run(): void
$this->call(PermissionSeeder::class); $this->call(PermissionSeeder::class);
$this->call(DefaultRoleWithPermissionSeeder::class); $this->call(DefaultRoleWithPermissionSeeder::class);
$this->call(ExampleUserWithRoleSeeder::class); $this->call(ExampleUserWithRoleSeeder::class);
$this->call(MockDataSeeder::class);
} }
} }

View File

@ -0,0 +1,79 @@
<?php
declare(strict_types=1);
namespace Database\Seeders;
use App\Data\ConnectedApp\ConnectAppRequest;
use App\Data\Role\AssignAppsToRoleData;
use App\Models\{ConnectedApp, Role, User};
use App\Services\{AppRoleService, ConnectedAppService};
use Illuminate\Database\Seeder;
class MockDataSeeder extends Seeder
{
public function __construct(
private AppRoleService $appRoleService,
private ConnectedAppService $connectedAppService,
) {}
public function run(): void
{
// 1. Create 5 roles with different priorities
$roleNames = ['Manager', 'Editor', 'Auditor', 'Developer', 'Support'];
$priorities = [10, 20, 30, 40, 50];
$roles = [];
foreach ($roleNames as $index => $name) {
$role = Role::findOrCreate($name, 'web');
$role->update(['priority' => $priorities[$index]]);
$roles[] = $role;
}
// 2. Create 5 users and assign them role combinations
$users = User::factory()->count(5)->create();
// Assign combinations
$users[0]->assignRole(['Manager', 'Editor']);
$users[1]->assignRole(['Developer', 'Support']);
$users[2]->assignRole(['Auditor']);
$users[3]->assignRole(['Manager', 'Developer']);
$users[4]->assignRole(['Support']);
// 3. Create 10 apps using ConnectedAppService to automatically register permissions
$apps = [];
$factory = ConnectedApp::factory();
for ($i = 0; $i < 10; $i++) {
$data = $factory->definition();
$request = new ConnectAppRequest(
name: $data['name'],
connectionProviderId: $data['connection_provider_id'],
connectionProtocolId: $data['connection_protocol_id'],
slug: $data['slug'],
connectionStatusId: $data['connection_status_id'],
);
$this->connectedAppService->create($request);
$apps[] = ConnectedApp::where('slug', $data['slug'])->firstOrFail();
}
// 4. Connect apps to roles using AppRoleService to automatically assign permissions
$roleAppsMapping = [
'Manager' => [$apps[0]->id, $apps[1]->id, $apps[2]->id],
'Editor' => [$apps[2]->id, $apps[3]->id, $apps[4]->id],
'Auditor' => [$apps[5]->id],
'Developer' => [$apps[6]->id, $apps[7]->id, $apps[8]->id],
'Support' => [$apps[8]->id, $apps[9]->id],
];
foreach ($roleAppsMapping as $roleName => $appIds) {
$role = Role::findByName($roleName, 'web');
$this->appRoleService->assign(new AssignAppsToRoleData(
roleId: $role->id,
validUpto: now()->addYears(5)->toDateTimeString(),
appIds: $appIds
));
}
}
}

View File

@ -3,6 +3,7 @@
use App\Concerns\{Confirmation, HandlesOperations}; use App\Concerns\{Confirmation, HandlesOperations};
use App\Data\Role\{AssignAppsToRoleData, AssignUsersToRoleData, RoleAppData, UpdateRolePriorityData}; use App\Data\Role\{AssignAppsToRoleData, AssignUsersToRoleData, RoleAppData, UpdateRolePriorityData};
use App\Data\Ui\TableHeader; use App\Data\Ui\TableHeader;
use App\Enums\Permissions\RoleAndAppsPermissionEnum;
use App\Livewire\Forms\AssignAppToRoleForm; use App\Livewire\Forms\AssignAppToRoleForm;
use App\Models\Role; use App\Models\Role;
use App\Services\{AppRoleService, RoleService}; use App\Services\{AppRoleService, RoleService};
@ -165,6 +166,8 @@ public function clearApps(): void
public function assignApp(): void public function assignApp(): void
{ {
$this->authorize(RoleAndAppsPermissionEnum::ConnectApps->value);
$this->attempt( $this->attempt(
action: function (): void { action: function (): void {
$this->assignForm->validate(); $this->assignForm->validate();
@ -183,6 +186,8 @@ public function assignApp(): void
public function updateApp(): void public function updateApp(): void
{ {
$this->authorize(RoleAndAppsPermissionEnum::ConnectApps->value);
$this->attempt( $this->attempt(
action: function (): void { action: function (): void {
$this->assignForm->validate(); $this->assignForm->validate();
@ -202,6 +207,8 @@ public function updateApp(): void
public function detachApp(int $id): void public function detachApp(int $id): void
{ {
$this->authorize(RoleAndAppsPermissionEnum::ConnectApps->value);
$this->attempt( $this->attempt(
action: function () use ($id): void { action: function () use ($id): void {
$this->appRoleService->detach($id); $this->appRoleService->detach($id);
@ -319,9 +326,11 @@ class="btn-sm btn-ghost btn-circle"
<x-shared.card title="Connected Apps" icon="lucide-blocks" class="pb-2 w-full h-min"> <x-shared.card title="Connected Apps" icon="lucide-blocks" class="pb-2 w-full h-min">
<x-slot:actions> <x-slot:actions>
<x-mary-button wire:click="openAddAppModal" spinner="openAddAppModal" icon="lucide.plus" class="btn-sm"> @can(RoleAndAppsPermissionEnum::ConnectApps->value)
Add App <x-mary-button wire:click="openAddAppModal" spinner="openAddAppModal" icon="lucide.plus" class="btn-sm">
</x-mary-button> Add App
</x-mary-button>
@endcan
</x-slot:actions> </x-slot:actions>
@if ($this->apps->count()) @if ($this->apps->count())
@ -339,18 +348,20 @@ class="btn-sm btn-ghost btn-circle"
@scope('actions', $row) @scope('actions', $row)
<div class="flex gap-1"> <div class="flex gap-1">
<x-mary-button @can(RoleAndAppsPermissionEnum::ConnectApps->value)
icon="lucide.square-pen" <x-mary-button
wire:click="openEditAppModal({{ $row['id'] }})" icon="lucide.square-pen"
spinner="openEditAppModal({{ $row['id'] }})" wire:click="openEditAppModal({{ $row['id'] }})"
class="btn-sm btn-soft btn-circle" spinner="openEditAppModal({{ $row['id'] }})"
/> class="btn-sm btn-soft btn-circle"
<x-mary-button />
icon="lucide.trash" <x-mary-button
wire:click="requireConfirmation('detachApp', {{ $row['id'] }})" icon="lucide.trash"
spinner="requireConfirmation('detachApp', {{ $row['id'] }})" wire:click="requireConfirmation('detachApp', {{ $row['id'] }})"
class="btn-sm btn-error btn-soft btn-circle" spinner="requireConfirmation('detachApp', {{ $row['id'] }})"
/> class="btn-sm btn-error btn-soft btn-circle"
/>
@endcan
</div> </div>
@endscope @endscope
</x-mary-table> </x-mary-table>