= e791c08ce1 Feat: (UI) Replace connected-app creation with modular OIDC service and components
- Removed the old `connected-apps.create` implementation and components, simplifying the codebase.
- Introduced modular OIDC integration using reusable components and traits, such as `WithSearchableChoices` and `WithRepeaterFields`.
- Added `StoreOIDCAppForm` for handling OpenID Connect app-specific fields and validation.
- Updated `choices.blade.php` and related components to leverage configuration-driven, dynamic multi-select functionality.
- Updated routes to support new OIDC app creation under distinct connection protocol namespaces.
- Enhanced app creation flow with support for dynamic fields like URIs (`repeater-input`) and scoped user roles.
2026-06-18 09:48:07 +00:00

197 lines
7.0 KiB
PHP

<?php
use App\Concerns\HandlesOperations;
use App\Data\ConnectedApp\{ConnectAppRequest, ConnectionStatusData};
use App\Enums\{ApplicationTypeEnum, ConnectionProtocolEnum, ConnectionStatusEnum, UserAccessTypeEnum};
use App\Livewire\Forms\StoreOIDCAppForm;
use App\Livewire\Concerns\Choices\{ChoiceField, WithSearchableChoices};
use App\Livewire\Concerns\WithRepeaterFields;
use App\Models\{ConnectionProtocol, ConnectionProvider, ConnectionStatus};
use App\Services\{ConnectedAppService,
ConnectionProtocolService,
ConnectionProviderService,
ConnectionStatusService,
RoleService
};
use Illuminate\Support\Collection;
use Livewire\Attributes\{Computed, Title, Url};
use Livewire\Component;
use Livewire\WithFileUploads;
use Mary\Traits\Toast;
use Spatie\LaravelData\DataCollection;
new #[Title("Create a service")]
class extends Component {
use HandlesOperations, WithRepeaterFields, WithSearchableChoices, WithFileUploads;
#[Url]
public string $protocol = '';
#[Url]
public ?string $type = null;
public StoreOIDCAppForm $form;
public ConnectionProtocolEnum $connectionProtocol = ConnectionProtocolEnum::OIDC;
public ?ApplicationTypeEnum $applicationType = null;
private RoleService $roleService;
public function boot(RoleService $roleService): void
{
$this->roleService = $roleService;
}
public function mount(): void
{
$this->applicationType = ApplicationTypeEnum::tryFrom($this->type);
}
public function save(bool $goBack = true): void
{
$this->form->validate();
ds($this->form->pull());
}
public function goBack(): void
{
$this->redirectRoute("apps.index", navigate: true);
}
public function title(): string
{
$title = "New ";
$title .= match ($this->applicationType) {
ApplicationTypeEnum::Web => "Web Application ",
ApplicationTypeEnum::SPA => "Single Page Application ",
ApplicationTypeEnum::Machine => "Microservice Application ",
null => ""
};
$title .= "Integration";
return $title;
}
public function searchRoles(string $q = ''): void
{
$this->searchChoice('roles', $q);
}
public function selectAllRoles(): void
{
$this->selectAllChoice('roles');
}
public function clearRoles(): void
{
$this->clearChoice('roles');
}
protected function choiceFields(): array
{
return [
'roles' => new ChoiceField(
search: fn(string $q): array => $this->roleService->searchRolesForSelect($q)->toArray(),
allIds: $this->roleService->getAllRoleIds(...),
formPath: 'form.roleIds',
),
];
}
};
?>
<div>
<x-shared.card :title="$this->title()" icon="lucide-package-plus">
<x-slot:actions>
<x-mary-button wire:click="goBack" icon="lucide.corner-up-left" class="btn-ghost">Go Back
</x-mary-button>
</x-slot:actions>
<x-mary-form wire:submit="save" class="p-4 gap-y-8">
<!-- General settings start -->
<div class="flex gap-4 flex-col w-full md:flex-row">
<article class="w-full md:w-2/5">
<h2 class="font-bold">General Settings</h2>
</article>
<div class="w-full md:w-3/5">
<x-mary-input required label="App Name" wire:model="form.name" placeholder="Keka, MS 365 etc."
hint="A basic name that matches the actual application"/>
<x-mary-file wire:model="form.logo" label="Logo" hint="Optional"
accept="application/jpg, application/png, image/jpeg, image/png"/>
</div>
</div>
<!-- General settings end -->
<!-- Sign-in uris start -->
<hr>
<div class="flex gap-4 flex-col w-full md:flex-row">
<article class="w-full md:w-2/5">
<h2 class="font-bold">Sign In Redirect URIs</h2>
<p class="text-sm text-gray-500 max-w-70 mt-4">
{{config('app.name')}} sends response and ID Token back to these URIs.
</p>
</article>
<div class="w-full md:w-3/5">
<x-shared.repeater-input model="form.signInUris" :items="$form->signInUris"/>
</div>
</div>
<!-- Sign-in uris end -->
<!-- Sign-out uris start -->
<hr>
<div class="flex gap-4 flex-col w-full md:flex-row">
<article class="w-full md:w-2/5">
<h2 class="font-bold">Sign out Redirect URIs</h2>
<p class="text-sm text-gray-500 max-w-70 mt-4">
{{config('app.name')}} sends back the user to these URIs after logout.
</p>
</article>
<div class="w-full md:w-3/5">
<x-shared.repeater-input model="form.signOutUris" :items="$form->signOutUris"/>
</div>
</div>
<!-- Sign-out uris end -->
<!-- Access start -->
<hr>
<div class="flex gap-4 flex-col w-full md:flex-row">
<article class="w-full md:w-2/5">
<h2 class="font-bold">User Access</h2>
<p class="text-sm text-gray-500 max-w-70 mt-4">
Choose whether to allow all users be able to connect this app or selected ones.
</p>
</article>
<div class="w-full md:w-3/5">
<x-mary-radio
:options="UserAccessTypeEnum::asOptions()"
option-value="value"
wire:model.change.live="form.userAccessOption"
/>
<div class=""
x-show="$wire.form.userAccessOption === '{{UserAccessTypeEnum::Role->value}}'"
x-cloak
>
<x-shared.choices
wire:model.live="form.roleIds"
:options="$choicesSearchable['roles'] ?? []"
search-function="searchRoles"
select-all-action="selectAllRoles"
clear-action="clearRoles"
label="Roles"
/>
</div>
</div>
</div>
<!-- Access end -->
<div class="flex gap-x-4 font-medium justify-end">
<x-mary-button wire:click.prevent="goBack" spinner="goBack">Cancel</x-mary-button>
<x-mary-button wire:click.prevent="save(false)">Save & Add Another</x-mary-button>
<x-mary-button type="submit"
class="btn-primary">
Save
</x-mary-button>
</div>
</x-mary-form>
</x-shared.card>
</div>