= 7f02209631 Feat: Add OIDC app creation feature with access URL support, Hide SAML, Microservice
- Introduced tests for OIDC app creation, including validation for access URL formatting.
- Updated enums and services to streamline application type and protocol handling.
- Added `accessUrl` support across backend, services, and UI to enhance app functionality and user accessibility.
- Refactored and commented out unused enum values and providers to improve maintainability.
2026-06-22 05:54:36 +00:00

290 lines
11 KiB
PHP

<?php
use App\Concerns\HandlesOperations;
use App\Data\Application\CreatedApplicationData;
use App\Data\Application\StoreOIDCAppData;
use App\Data\ConnectedApp\ConnectedAppData;
use App\Enums\{ApplicationTypeEnum, ConnectionProtocolEnum, Permissions\AppPermissionEnum, UserAccessTypeEnum};
use App\Livewire\Concerns\Choices\{ChoiceField, WithSearchableChoices};
use App\Livewire\Concerns\WithRepeaterFields;
use App\Livewire\Forms\StoreOIDCAppForm;
use App\Services\{Applications\OIDC\OidcConfigResolver, Applications\OIDC\OidcService, RoleService};
use Livewire\Attributes\{Computed, Url};
use Livewire\Component;
use Livewire\WithFileUploads;
new
class extends Component {
use HandlesOperations, WithRepeaterFields, WithSearchableChoices, WithFileUploads;
#[Url]
public string $protocol = '';
#[Url]
public ?string $type = null;
// For creation states
public bool $created = false;
public ?string $clientId = null;
public ?string $clientSecret = null;
public ?ConnectedAppData $app = null;
public StoreOIDCAppForm $form;
public ConnectionProtocolEnum $connectionProtocol = ConnectionProtocolEnum::OIDC;
public ApplicationTypeEnum $applicationType;
private RoleService $roleService;
private OidcService $oidcService;
private OidcConfigResolver $configResolver;
public function boot(RoleService $roleService, OidcService $oidcService, OidcConfigResolver $configResolver): void
{
$this->roleService = $roleService;
$this->oidcService = $oidcService;
$this->configResolver = $configResolver;
}
public function mount(): void
{
$this->applicationType = ApplicationTypeEnum::from($this->type);
}
public function render()
{
/** @phpstan-ignore-next-line */
return $this->view()->title($this->title());
}
public function save(bool $goBack = true): void
{
$this->authorize(AppPermissionEnum::Create->value);
$this->form->validate();
$this->attempt(
function () {
$data = StoreOIDCAppData::fromArray($this->applicationType, $this->form->pull());
$appdata = $this->oidcService->createApp($data);
$this->showCredentials($appdata);
}
);
}
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 ",
};
$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');
}
#[Computed]
public function hasClientSecret(): bool
{
return $this->configResolver->for($this->applicationType)->hasClientSecret();
}
#[Computed]
public function needSignInUris(): bool
{
return $this->configResolver->for($this->applicationType)->needSignInUris();
}
#[Computed]
public function needSignOutUris(): bool
{
return $this->configResolver->for($this->applicationType)->needSignOutUris();
}
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',
),
];
}
private function showCredentials(CreatedApplicationData $appData): void
{
$this->clientId = $appData->clientCredentials->clientId;
$this->clientSecret = $appData->clientCredentials->clientSecret;
$this->app = $appData->application;
$this->created = true;
}
};
?>
<div>
@if($created)
<x-shared.card
title="Application Created"
icon="lucide-check-circle"
>
<div class="flex w-full justify-center items-center pt-6">
<article class="mx-auto flex items-center gap-4">
<x-mary-avatar :image="$app?->logo ? asset($app->logo) : asset('images/app-icon-placeholder.png')"
alt="App logo"
class="w-14! rounded-lg! p-2"/>
<h1 class="text-2xl font-medium">{{$app->name}}</h1>
</article>
</div>
<div class="space-y-6 p-6">
<x-mary-input
label="Client ID"
:value="$clientId"
readonly
/>
@if($this->hasClientSecret)
<x-mary-alert
title="Save these credentials"
description="The client secret will only be displayed once."
class="alert-warning alert-soft text-yellow-700"
/>
<x-mary-password label="Client Secret" :value="$clientSecret" readonly right/>
@endif
<div class="flex justify-end gap-4">
<x-mary-button
wire:click="goBack"
class="btn-primary"
>
Done
</x-mary-button>
</div>
</div>
</x-shared.card>
@else
<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-input required label="Access Url" wire:model="form.accessUrl"
placeholder="https://app.example.com"
hint="The URL by which user will access this application. We will show this url in user dashboard."/>
<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 -->
@if($this->needSignInUris)
<!-- 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">
Allowed URIs which this app can ask to get the response back.
</p>
</article>
<div class="w-full md:w-3/5">
<p class="text-xs text-gray-500 mb-2">You can add this later.</p>
<x-shared.repeater-input model="form.signInUris" :items="$form->signInUris"/>
</div>
</div>
<!-- Sign-in uris end -->
@endif
@if($this->needSignOutUris)
<!-- 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">
Allowed URIs which this app can redirect the user back to after logout.
</p>
</article>
<div class="w-full md:w-3/5">
<p class="text-xs text-gray-500 mb-2">You can add this later.</p>
<x-shared.repeater-input model="form.signOutUris" :items="$form->signOutUris"/>
</div>
</div>
<!-- Sign-out uris end -->
@endif
<!-- 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>
@endif
</div>