= 6a400fc532 Feat: Add OIDC app management UI and features
- Introduced a detailed view for OIDC apps (`show` page) with client credentials and sign-out URI management.
- Added functionality to toggle app status, generate new client secrets, and display OAuth scopes.
- Implemented service methods for handling OIDC app data, including `getPassportClient`, `generateNewSecret`, and `toggleStatus`.
- Enhanced feature tests for role-based access and validation of client secret generation.
2026-06-22 10:06:00 +00:00

168 lines
5.6 KiB
PHP

<?php
use App\Concerns\Confirmation;
use App\Concerns\HandlesOperations;
use App\Data\Ui\ConnectedApps\ConnectedAppData;
use App\Data\Ui\TableHeader;
use App\Enums\ApplicationTypeEnum;
use App\Enums\ConnectionProtocolEnum;
use App\Livewire\Forms\CreateAppSelectionForm;
use App\Models\ConnectedApp;
use App\Services\ConnectedAppService;
use Livewire\Attributes\Computed;
use Livewire\Attributes\Title;
use Livewire\Component;
use Livewire\WithPagination;
use Spatie\LaravelData\Attributes\DataCollectionOf;
use Spatie\LaravelData\DataCollection;
use Spatie\LaravelData\PaginatedDataCollection;
new
#[Title('Connected Apps')]
class extends Component {
use WithPagination, HandlesOperations, Confirmation;
protected ConnectedAppService $service;
public bool $showAppCreationModal = false;
public CreateAppSelectionForm $appSelectionForm;
#[DataCollectionOf(TableHeader::class)]
public DataCollection $headers;
public function boot(ConnectedAppService $service): void
{
$this->service = $service;
}
public function mount(): void
{
$this->initTableHeaders();
}
private function initTableHeaders(): void
{
$this->headers = TableHeader::collect([
new TableHeader(key: 'name', label: 'Name',),
new TableHeader(key: 'protocol', label: 'Protocol'),
new TableHeader(key: 'status', label: 'Status'),
], DataCollection::class);
}
/**
* @return PaginatedDataCollection<int, ConnectedAppData>
*/
#[Computed]
public function getConnectedApps(): PaginatedDataCollection
{
return $this->service->getAll();
}
public function edit(int $appId): void
{
$this->redirectRoute('apps.edit', ['id' => $appId], navigate: true);
}
public function viewApp(int $appId): void
{
$app = ConnectedApp::with('protocol')->findOrFail($appId);
$protocol = mb_strtolower($app->protocol->name ?? '');
if ($protocol === 'oidc') {
$this->redirectRoute('apps.oidc.show', ['id' => $appId], navigate: true);
} else {
$this->redirectRoute('apps.edit', ['id' => $appId], navigate: true);
}
}
public function delete(int $appId): void
{
$this->attempt(
action: fn() => $this->service->delete($appId),
successMessage: 'The app has been deleted !'
);
}
public function createApp(): void
{
$this->appSelectionForm->validate();
$route = "apps.{$this->appSelectionForm->connectionProtocol}.create";
$this->redirectRoute($route, [
'type' => $this->appSelectionForm->applicationType
], navigate: true);
}
};
?>
<div>
<x-shared.card title="Connected Apps" class="pb-2">
<x-slot:actions>
<x-mary-button @click="$wire.showAppCreationModal = true" icon="lucide.plus">Add App
</x-mary-button>
</x-slot:actions>
<x-mary-table
:headers="$headers->toArray()"
:rows="$this->getConnectedApps->items()"
with-pagination
>
@scope('cell_protocol', $row)
{{$row->protocol->name}}
@endscope
@scope('cell_status', $row)
<x-shared.connection-status :status="$row->status"/>
@endscope
@scope('actions', $row)
<div class="flex">
<x-mary-button icon="lucide.eye" wire:click="viewApp({{$row->id}})" spinner="viewApp({{$row->id}})"
class="mr-2 btn-sm btn-circle text-blue-500" tooltip="View Details"/>
<x-mary-button icon="lucide.trash" wire:click="requireConfirmation('delete', {{$row->id}})"
spinner="requireConfirmation('delete', {{$row->id}})"
variant="danger"
class="btn-sm btn-circle btn-error btn-soft"/>
</div>
@endscope
</x-mary-table>
</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>