Feat: Add modular application types, connection protocols, and dynamic app creation modal

- Introduced `ApplicationTypeEnum` and updated `ConnectionProtocolEnum` with labeled options, hints, and implementations for enhanced flexibility.
- Modularized the `apps` routes into a separate file for improved structure and maintainability.
- Added `CreateAppSelectionForm` Livewire component to handle app creation flows with dynamic protocol and type selection.
- Enhanced `connected-apps` UI with a modal-based app creation mechanism.
This commit is contained in:
= 2026-06-17 12:46:25 +00:00
parent 0921535ae2
commit a04bb5d7d1
7 changed files with 186 additions and 27 deletions

2
.ddev/php/xdebug.ini Normal file
View File

@ -0,0 +1,2 @@
[xdebug]
xdebug.client_port = 9009

View File

@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
namespace App\Enums;
use App\Contracts\EnumMorphsToOptionsContract;
enum ApplicationTypeEnum: string implements EnumMorphsToOptionsContract
{
case Web = 'web';
case SPA = 'spa';
case Machine = 'machine';
public static function asOptions(): array
{
return array_map(fn (self $enum) => [
'name' => $enum->toLabel(),
'value' => $enum->value,
'hint' => $enum->toHint(),
], self::cases());
}
public function toLabel(): string
{
return match ($this) {
self::Web => 'Web Applications',
self::SPA => 'Single Page Applications',
self::Machine => 'Microservices',
};
}
public function toHint(): string
{
return match ($this) {
self::Web => 'Server-side rendered web application, where authentication is handled by the server.',
self::SPA => 'Single Page Application where client side framework e.g. React, Vue, Angular is used, and gets the authentication token from the server.',
self::Machine => 'Application that runs on a machine, e.g. a serverless function, or a container.',
};
}
}

View File

@ -5,13 +5,40 @@
namespace App\Enums; namespace App\Enums;
use App\Concerns\EnumValuesAsArray; use App\Concerns\EnumValuesAsArray;
use App\Contracts\EnumMorphsToOptionsContract;
enum ConnectionProtocolEnum: string enum ConnectionProtocolEnum: string implements EnumMorphsToOptionsContract
{ {
use EnumValuesAsArray; use EnumValuesAsArray;
case OIDC = 'oidc'; case OIDC = 'oidc';
case SAML = 'saml'; case SAML = 'saml';
case OAUTH2 = 'oauth2';
case URL = 'url'; case URL = 'url';
public static function asOptions(): array
{
return array_map(fn (self $enum) => [
'name' => $enum->toLabel(),
'value' => mb_strtolower($enum->name),
'hint' => $enum->toHint(),
], self::cases());
}
public function toLabel(): string
{
return match ($this) {
self::OIDC => 'OIDC - OpenID Connect',
self::SAML => 'SAML 2.0',
self::URL => 'Redirect URL',
};
}
public function toHint(): string
{
return match ($this) {
self::OIDC => 'Modern authentication protocol built on OAuth 2.0 for secure user sign-in across web, mobile, and API-based applications.',
self::SAML => 'XML-based Single Sign-On (SSO) protocol commonly used by enterprise applications and identity providers',
self::URL => 'Simple URL that will redirect the user to the authentication page.',
};
}
} }

View File

@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace App\Livewire\Forms;
use App\Enums\{ApplicationTypeEnum, ConnectionProtocolEnum};
use Illuminate\Validation\Rule;
use Livewire\Form;
class CreateAppSelectionForm extends Form
{
public string $connectionProtocol = ConnectionProtocolEnum::OIDC->value;
public string $applicationType = ApplicationTypeEnum::Web->value;
protected function rules(): array
{
return [
'connectionProtocol' => [
'required',
Rule::enum(ConnectionProtocolEnum::class),
],
'applicationType' => [
'required',
Rule::enum(ApplicationTypeEnum::class),
],
];
}
}

View File

@ -4,6 +4,9 @@
use App\Concerns\HandlesOperations; use App\Concerns\HandlesOperations;
use App\Data\Ui\ConnectedApps\ConnectedAppData; use App\Data\Ui\ConnectedApps\ConnectedAppData;
use App\Data\Ui\TableHeader; use App\Data\Ui\TableHeader;
use App\Enums\ApplicationTypeEnum;
use App\Enums\ConnectionProtocolEnum;
use App\Livewire\Forms\CreateAppSelectionForm;
use App\Services\ConnectedAppService; use App\Services\ConnectedAppService;
use Livewire\Attributes\Computed; use Livewire\Attributes\Computed;
use Livewire\Attributes\Title; use Livewire\Attributes\Title;
@ -19,6 +22,8 @@ class extends Component {
use WithPagination, HandlesOperations, Confirmation; use WithPagination, HandlesOperations, Confirmation;
protected ConnectedAppService $service; protected ConnectedAppService $service;
public bool $showAppCreationModal = false;
public CreateAppSelectionForm $appSelectionForm;
#[DataCollectionOf(TableHeader::class)] #[DataCollectionOf(TableHeader::class)]
public DataCollection $headers; public DataCollection $headers;
@ -64,13 +69,22 @@ public function delete(int $appId): void
successMessage: 'The app has been deleted !' successMessage: 'The app has been deleted !'
); );
} }
public function createApp(): void
{
$this->appSelectionForm->validate();
$this->redirectRoute('apps.create', [
'protocol' => $this->appSelectionForm->connectionProtocol,
'type' => $this->appSelectionForm->applicationType
], navigate: true);
}
}; };
?> ?>
<div> <div>
<x-shared.card title="Connected Apps" class="pb-2"> <x-shared.card title="Connected Apps" class="pb-2">
<x-slot:actions> <x-slot:actions>
<x-mary-button :link="route('apps.create')" wire:navigate icon="lucide.plus">Add App <x-mary-button @click="$wire.showAppCreationModal = true" icon="lucide.plus">Add App
</x-mary-button> </x-mary-button>
</x-slot:actions> </x-slot:actions>
<x-mary-table <x-mary-table
@ -85,6 +99,7 @@ public function delete(int $appId): void
@scope('cell_status', $row) @scope('cell_status', $row)
<x-shared.connection-status :status="$row->status"/> <x-shared.connection-status :status="$row->status"/>
@endscope @endscope
@scope('actions', $row) @scope('actions', $row)
<div class="flex"> <div class="flex">
<x-mary-button icon="lucide.edit" wire:click="edit({{$row->id}})" spinner="edit({{$row->id}})" <x-mary-button icon="lucide.edit" wire:click="edit({{$row->id}})" spinner="edit({{$row->id}})"
@ -97,4 +112,44 @@ class="btn-sm btn-circle btn-error btn-soft"/>
@endscope @endscope
</x-mary-table> </x-mary-table>
</x-shared.card> </x-shared.card>
<x-mary-modal title='Create a new app' wire:model="showAppCreationModal" box-class="w-11/12 max-w-4xl">
<div class="flex flex-col md:flex-row gap-6 mb-6">
<article class="md:w-1/3 shrink-0">
<h2 class="font-bold">Protocol</h2>
</article>
<div class="flex-1">
<x-mary-radio
wire:model.change.live="appSelectionForm.connectionProtocol"
:options="ConnectionProtocolEnum::asOptions()"
option-value="value"
/>
</div>
</div>
<div class="flex flex-col md:flex-row gap-6 mt-6 border-t border-gray-200 pt-6"
x-show="$wire.appSelectionForm.connectionProtocol === '{{ ConnectionProtocolEnum::OIDC->value }}'"
x-cloak>
<article class="md:w-1/3 shrink-0">
<h2 class="font-bold">Application Type</h2>
<p class="text-sm mt-1 text-gray-500">
Choose what type of application you are trying to connect. This helps to properly generate
responses.
</p>
</article>
<div class="flex-1">
<x-mary-radio wire:model="appSelectionForm.applicationType" :options="ApplicationTypeEnum::asOptions()"
option-value="value"/>
</div>
</div>
<x-slot:actions>
<x-mary-button @click="$wire.showAppCreationModal = false">Cancel</x-mary-button>
<x-mary-button class="btn-primary" wire:click="createApp" spinner="createApp">Create</x-mary-button>
</x-slot:actions>
</x-mary-modal>
</div> </div>

26
routes/apps.php Normal file
View File

@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
use App\Enums\Permissions\{AppPermissionEnum, ConnectionProviderPermissionEnum};
use Spatie\Permission\Middleware\PermissionMiddleware;
Route::prefix('apps')->name('apps.')->group(function (): void {
Route::livewire('microsoft-federation', 'pages::connecton-providers.microsoft-federation')
->middleware(PermissionMiddleware::using(ConnectionProviderPermissionEnum::Access))
->name('microsoft-federation');
Route::middleware(PermissionMiddleware::using(AppPermissionEnum::Access))->group(function (): void {
Route::livewire('create', 'pages::connected-apps.create')->name('create');
Route::livewire('{id}/edit', 'pages::connected-apps.edit')->name('edit');
Route::livewire('/', 'pages::connected-apps.index')->name('index');
});
Route::prefix('connection-providers')->name('connectionProviders.')
->middleware(PermissionMiddleware::using(ConnectionProviderPermissionEnum::Access))
->group(function (): void {
Route::livewire('create', 'pages::connecton-providers.create')->name('create');
Route::livewire('{slug}/edit', 'pages::connecton-providers.edit')->name('edit');
Route::livewire('index', 'pages::connecton-providers.index')->name('index');
});
});

View File

@ -3,11 +3,7 @@
declare(strict_types=1); declare(strict_types=1);
use App\Enums\DefaultUserRole; use App\Enums\DefaultUserRole;
use App\Enums\Permissions\{AppPermissionEnum, use App\Enums\Permissions\{PermissionPermissionEnum, RoleAndAppsPermissionEnum, UserPermissionEnum};
ConnectionProviderPermissionEnum,
PermissionPermissionEnum,
RoleAndAppsPermissionEnum,
UserPermissionEnum};
use App\Http\Controllers\{EntraController, KekaController}; use App\Http\Controllers\{EntraController, KekaController};
use App\Http\Controllers\KekaOutlookController; use App\Http\Controllers\KekaOutlookController;
use App\Http\Controllers\{ResolveDashboardRouteController, SamlIdpController}; use App\Http\Controllers\{ResolveDashboardRouteController, SamlIdpController};
@ -62,25 +58,7 @@
}); });
}); });
Route::prefix('apps')->name('apps.')->group(function (): void { require __DIR__.'/apps.php';
Route::livewire('microsoft-federation', 'pages::connecton-providers.microsoft-federation')
->middleware(PermissionMiddleware::using(ConnectionProviderPermissionEnum::Access))
->name('microsoft-federation');
Route::middleware(PermissionMiddleware::using(AppPermissionEnum::Access))->group(function (): void {
Route::livewire('create', 'pages::connected-apps.create')->name('create');
Route::livewire('{id}/edit', 'pages::connected-apps.edit')->name('edit');
Route::livewire('/', 'pages::connected-apps.index')->name('index');
});
Route::prefix('connection-providers')->name('connectionProviders.')
->middleware(PermissionMiddleware::using(ConnectionProviderPermissionEnum::Access))
->group(function (): void {
Route::livewire('create', 'pages::connecton-providers.create')->name('create');
Route::livewire('{slug}/edit', 'pages::connecton-providers.edit')->name('edit');
Route::livewire('index', 'pages::connecton-providers.index')->name('index');
});
});
}); });
Route::middleware(['auth', 'verified'])->prefix('entra')->name('entra.')->group(function (): void { Route::middleware(['auth', 'verified'])->prefix('entra')->name('entra.')->group(function (): void {