Feat: Update App details from view page
- Implemented the `show` page for managing URL apps with functionalities like updating name, logo, access URL, and connection status. - Added `UrlAppViewTest` for feature testing, ensuring role-based access, and validation of edit actions. - Updated navigation to route URL apps to their respective management views. - Enhanced `OidcService` to handle updated app settings and redirect URIs.
This commit is contained in:
parent
6a400fc532
commit
9c9e3ca43c
@ -15,6 +15,7 @@
|
|||||||
* @property int $id
|
* @property int $id
|
||||||
* @property string $name
|
* @property string $name
|
||||||
* @property AppRole $pivot
|
* @property AppRole $pivot
|
||||||
|
* @property array $settings
|
||||||
*/
|
*/
|
||||||
#[Fillable([
|
#[Fillable([
|
||||||
'name',
|
'name',
|
||||||
@ -23,23 +24,12 @@
|
|||||||
'connection_provider_id',
|
'connection_provider_id',
|
||||||
'connection_status_id',
|
'connection_status_id',
|
||||||
'settings',
|
'settings',
|
||||||
|
'logo',
|
||||||
])]
|
])]
|
||||||
class ConnectedApp extends Model
|
class ConnectedApp extends Model
|
||||||
{
|
{
|
||||||
use HasFactory, LogsActivity, SoftDeletes;
|
use HasFactory, LogsActivity, SoftDeletes;
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the attributes that should be cast.
|
|
||||||
*
|
|
||||||
* @return array<string, string>
|
|
||||||
*/
|
|
||||||
protected function casts(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'settings' => 'array',
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return HasOne<ConnectionProtocol, $this>
|
* @return HasOne<ConnectionProtocol, $this>
|
||||||
*/
|
*/
|
||||||
@ -87,4 +77,16 @@ public function roles(): BelongsToMany
|
|||||||
->using(AppRole::class)
|
->using(AppRole::class)
|
||||||
->withPivot('duration');
|
->withPivot('duration');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the attributes that should be cast.
|
||||||
|
*
|
||||||
|
* @return array<string, string>
|
||||||
|
*/
|
||||||
|
protected function casts(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'settings' => 'array',
|
||||||
|
];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,6 +5,7 @@
|
|||||||
namespace App\Services\Applications\OIDC;
|
namespace App\Services\Applications\OIDC;
|
||||||
|
|
||||||
use App\Enums\ApplicationTypeEnum;
|
use App\Enums\ApplicationTypeEnum;
|
||||||
|
use Laravel\Passport\Client;
|
||||||
|
|
||||||
class OidcConfigResolver
|
class OidcConfigResolver
|
||||||
{
|
{
|
||||||
@ -34,12 +35,12 @@ public function needSignOutUris(): bool
|
|||||||
// return ApplicationTypeEnum::Machine !== $this->applicationType;
|
// return ApplicationTypeEnum::Machine !== $this->applicationType;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function isConfidential(\Laravel\Passport\Client $client): bool
|
public function isConfidential(Client $client): bool
|
||||||
{
|
{
|
||||||
return $client->confidential();
|
return $client->confidential();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function requiresPkce(\Laravel\Passport\Client $client): bool
|
public function requiresPkce(Client $client): bool
|
||||||
{
|
{
|
||||||
return ! $client->confidential();
|
return ! $client->confidential();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -90,19 +90,20 @@ public function generateNewSecret(Client $client): string
|
|||||||
return $plainSecret;
|
return $plainSecret;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function toggleStatus(ConnectedApp $app): void
|
|
||||||
{
|
|
||||||
$statusEnum = $app->status->name === ConnectionStatusEnum::Connected->value
|
|
||||||
? ConnectionStatusEnum::Disconnected
|
|
||||||
: ConnectionStatusEnum::Connected;
|
|
||||||
|
|
||||||
$newStatus = ConnectionStatus::where('name', $statusEnum->value)->firstOrFail();
|
|
||||||
$app->connection_status_id = $newStatus->id;
|
|
||||||
$app->save();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getSignOutUris(ConnectedApp $app): array
|
public function getSignOutUris(ConnectedApp $app): array
|
||||||
{
|
{
|
||||||
return data_get($app, 'settings.sign_out_uris', []);
|
return data_get($app->settings, 'signOutUris', []);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function updateRedirectUris(ConnectedApp $app, Client $client, array $signInUris, array $signOutUris): void
|
||||||
|
{
|
||||||
|
$mergedUris = array_merge($signInUris, $signOutUris);
|
||||||
|
/** @phpstan-ignore-next-line */
|
||||||
|
$client->redirect_uris = $mergedUris;
|
||||||
|
$client->save();
|
||||||
|
$settings = $app->settings ?? [];
|
||||||
|
$settings['signOutUris'] = $signOutUris;
|
||||||
|
$app->settings = $settings;
|
||||||
|
$app->save();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,15 +2,21 @@
|
|||||||
|
|
||||||
use App\Concerns\HandlesOperations;
|
use App\Concerns\HandlesOperations;
|
||||||
use App\Enums\Permissions\AppPermissionEnum;
|
use App\Enums\Permissions\AppPermissionEnum;
|
||||||
|
use App\Enums\ConnectionStatusEnum;
|
||||||
use App\Models\{ConnectedApp, ConnectionStatus};
|
use App\Models\{ConnectedApp, ConnectionStatus};
|
||||||
use Livewire\Component;
|
use Livewire\Component;
|
||||||
use Livewire\Attributes\Title;
|
use Livewire\Attributes\Title;
|
||||||
|
use Livewire\WithFileUploads;
|
||||||
use Laravel\Passport\Client;
|
use Laravel\Passport\Client;
|
||||||
use App\Services\Applications\OIDC\{OidcService, OidcConfigResolver};
|
use App\Services\Applications\OIDC\{OidcService, OidcConfigResolver};
|
||||||
|
use App\Services\ConnectedAppService;
|
||||||
|
use App\Services\ApplicationLogoUploader;
|
||||||
|
use App\Data\ConnectedApp\ConnectAppRequest;
|
||||||
|
use App\Livewire\Concerns\WithRepeaterFields;
|
||||||
|
|
||||||
new #[Title('OIDC Application Details')]
|
new #[Title('OIDC Application Details')]
|
||||||
class extends Component {
|
class extends Component {
|
||||||
use HandlesOperations;
|
use HandlesOperations, WithFileUploads, WithRepeaterFields;
|
||||||
|
|
||||||
public int $appId;
|
public int $appId;
|
||||||
public ConnectedApp $app;
|
public ConnectedApp $app;
|
||||||
@ -19,13 +25,35 @@ class extends Component {
|
|||||||
public string $selectedTab = 'general-tab';
|
public string $selectedTab = 'general-tab';
|
||||||
public ?string $newSecretPlain = null;
|
public ?string $newSecretPlain = null;
|
||||||
|
|
||||||
|
// Edit fields
|
||||||
|
public bool $editGeneralModal = false;
|
||||||
|
public string $editName = '';
|
||||||
|
public $editLogo = null;
|
||||||
|
|
||||||
|
public bool $editRedirectsMode = false;
|
||||||
|
public array $editSignInUris = [];
|
||||||
|
public array $editSignOutUris = [];
|
||||||
|
|
||||||
private OidcService $oidcService;
|
private OidcService $oidcService;
|
||||||
private OidcConfigResolver $configResolver;
|
private OidcConfigResolver $configResolver;
|
||||||
|
private ConnectedAppService $connectedAppService;
|
||||||
|
private ApplicationLogoUploader $logoUploader;
|
||||||
|
|
||||||
public function boot(OidcService $oidcService, OidcConfigResolver $configResolver): void
|
public function boot(
|
||||||
{
|
OidcService $oidcService,
|
||||||
|
OidcConfigResolver $configResolver,
|
||||||
|
ConnectedAppService $connectedAppService,
|
||||||
|
ApplicationLogoUploader $logoUploader
|
||||||
|
): void {
|
||||||
$this->oidcService = $oidcService;
|
$this->oidcService = $oidcService;
|
||||||
$this->configResolver = $configResolver;
|
$this->configResolver = $configResolver;
|
||||||
|
$this->connectedAppService = $connectedAppService;
|
||||||
|
$this->logoUploader = $logoUploader;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function requiresPkce(Client $client): bool
|
||||||
|
{
|
||||||
|
return $this->configResolver->requiresPkce($client);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function mount(int $id): void
|
public function mount(int $id): void
|
||||||
@ -63,13 +91,23 @@ public function generateNewSecret(): void
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function toggleStatus(): void
|
public function changeStatus(string $statusName): void
|
||||||
{
|
{
|
||||||
$this->authorize(AppPermissionEnum::Update->value);
|
$this->authorize(AppPermissionEnum::Update->value);
|
||||||
|
|
||||||
$this->attempt(
|
$this->attempt(
|
||||||
action: function () {
|
action: function () use ($statusName) {
|
||||||
$this->oidcService->toggleStatus($this->app);
|
$statusId = ConnectionStatus::where('name', $statusName)->firstOrFail()->id;
|
||||||
|
$request = new ConnectAppRequest(
|
||||||
|
name: $this->app->name,
|
||||||
|
connectionProtocolId: $this->app->connection_protocol_id,
|
||||||
|
connectionProviderId: $this->app->connection_provider_id,
|
||||||
|
slug: $this->app->slug,
|
||||||
|
connectionStatusId: $statusId,
|
||||||
|
settings: $this->app->settings,
|
||||||
|
logo: $this->app->logo,
|
||||||
|
);
|
||||||
|
$this->connectedAppService->update($this->app->id, $request);
|
||||||
$this->loadAppData();
|
$this->loadAppData();
|
||||||
},
|
},
|
||||||
successMessage: 'Application status updated successfully!'
|
successMessage: 'Application status updated successfully!'
|
||||||
@ -81,14 +119,91 @@ public function goBack(): void
|
|||||||
$this->redirectRoute('apps.index', navigate: true);
|
$this->redirectRoute('apps.index', navigate: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function editApp(): void
|
public function openEditGeneralModal(): void
|
||||||
{
|
{
|
||||||
$this->redirectRoute('apps.edit', ['id' => $this->appId], navigate: true);
|
$this->editName = $this->app->name;
|
||||||
|
$this->editLogo = null;
|
||||||
|
$this->editGeneralModal = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function requiresPkce(Client $client)
|
public function saveGeneralInfo(): void
|
||||||
{
|
{
|
||||||
return $this->configResolver->requiresPkce($client);
|
$this->authorize(AppPermissionEnum::Update->value);
|
||||||
|
|
||||||
|
$this->validate([
|
||||||
|
'editName' => 'required|string|max:255',
|
||||||
|
'editLogo' => 'nullable|image|max:2048',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->attempt(
|
||||||
|
action: function () {
|
||||||
|
$logoPath = $this->editLogo
|
||||||
|
? $this->logoUploader->store($this->editLogo)
|
||||||
|
: $this->app->logo;
|
||||||
|
|
||||||
|
$request = new ConnectAppRequest(
|
||||||
|
name: $this->editName,
|
||||||
|
connectionProtocolId: $this->app->connection_protocol_id,
|
||||||
|
connectionProviderId: $this->app->connection_provider_id,
|
||||||
|
slug: \Illuminate\Support\Str::slug($this->editName),
|
||||||
|
connectionStatusId: $this->app->connection_status_id,
|
||||||
|
settings: $this->app->settings,
|
||||||
|
logo: $logoPath,
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->connectedAppService->update($this->app->id, $request);
|
||||||
|
|
||||||
|
if ($this->passportClient && $this->passportClient->name !== $this->editName) {
|
||||||
|
$this->passportClient->name = $this->editName;
|
||||||
|
$this->passportClient->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->editGeneralModal = false;
|
||||||
|
$this->success('Application updated successfully!');
|
||||||
|
$this->loadAppData();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function startEditRedirects(): void
|
||||||
|
{
|
||||||
|
$clientUris = $this->passportClient->redirect_uris ?? [];
|
||||||
|
$this->editSignOutUris = $this->signOutUris;
|
||||||
|
|
||||||
|
$this->editSignInUris = array_values(array_filter(
|
||||||
|
$clientUris,
|
||||||
|
fn($uri) => !in_array($uri, $this->editSignOutUris, true)
|
||||||
|
));
|
||||||
|
|
||||||
|
$this->editRedirectsMode = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function cancelEditRedirects(): void
|
||||||
|
{
|
||||||
|
$this->editRedirectsMode = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public function saveRedirectUris(): void
|
||||||
|
{
|
||||||
|
$this->authorize(AppPermissionEnum::Update->value);
|
||||||
|
|
||||||
|
if (!$this->passportClient) {
|
||||||
|
$this->error('No corresponding Passport Client found to update.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$signInUris = array_values(array_filter(array_map('trim', $this->editSignInUris)));
|
||||||
|
$signOutUris = array_values(array_filter(array_map('trim', $this->editSignOutUris)));
|
||||||
|
|
||||||
|
$this->attempt(
|
||||||
|
action: function () use ($signInUris, $signOutUris) {
|
||||||
|
$this->oidcService->updateRedirectUris($this->app, $this->passportClient, $signInUris, $signOutUris);
|
||||||
|
$this->editRedirectsMode = false;
|
||||||
|
$this->success('Redirect URIs updated successfully!');
|
||||||
|
$this->loadAppData();
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
?>
|
?>
|
||||||
@ -104,16 +219,11 @@ public function requiresPkce(Client $client)
|
|||||||
:placeholder="$app->logo ? null : 'lucide.settings'"
|
:placeholder="$app->logo ? null : 'lucide.settings'"
|
||||||
class="w-16 h-16 rounded-xl border border-gray-200 p-2 shadow-xs bg-gray-50 group-hover:opacity-85 transition-opacity"
|
class="w-16 h-16 rounded-xl border border-gray-200 p-2 shadow-xs bg-gray-50 group-hover:opacity-85 transition-opacity"
|
||||||
/>
|
/>
|
||||||
<button wire:click="editApp"
|
|
||||||
class="absolute -bottom-1 -right-1 bg-white p-1.5 rounded-full border border-gray-200 shadow-sm hover:scale-105 transition-transform"
|
|
||||||
tooltip="Edit Logo">
|
|
||||||
<x-lucide-pencil class="w-3.5 h-3.5 text-gray-500"/>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<h1 class="text-2xl font-bold text-gray-900 ">{{ $app->name }}</h1>
|
<h1 class="text-2xl font-bold text-gray-900 ">{{ $app->name }}</h1>
|
||||||
<button wire:click="editApp" class="text-gray-400 hover:text-gray-600 ">
|
<button wire:click="openEditGeneralModal" class="text-gray-400 hover:text-gray-600 ">
|
||||||
<x-lucide-pencil class="w-4 h-4"/>
|
<x-lucide-pencil class="w-4 h-4"/>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@ -127,30 +237,27 @@ class="font-semibold text-gray-700 ">{{ strtoupper($app->protocol->name) }}</spa
|
|||||||
<!-- Status Dropdown/Trigger -->
|
<!-- Status Dropdown/Trigger -->
|
||||||
<div class="dropdown dropdown-end">
|
<div class="dropdown dropdown-end">
|
||||||
<label tabindex="0"
|
<label tabindex="0"
|
||||||
class="btn btn-sm btn-outline gap-1.5 font-semibold {{ $app->status->name === 'connected' ? 'border-emerald-200 text-emerald-700 bg-emerald-50/30' : 'border-rose-200 text-rose-700 bg-rose-50/30' }}">
|
class="btn btn-sm btn-outline gap-1.5 font-semibold {{ $app->status->name === 'connected' ? 'border-emerald-200 text-emerald-700 bg-emerald-50/30' : ($app->status->name === 'disconnected' ? 'border-rose-200 text-rose-700 bg-rose-50/30' : 'border-gray-200 text-gray-700 bg-gray-50/30') }}">
|
||||||
<span
|
<span
|
||||||
class="w-2 h-2 rounded-full {{ $app->status->name === 'connected' ? 'bg-emerald-500' : 'bg-rose-500' }}"></span>
|
class="w-2 h-2 rounded-full {{ $app->status->name === 'connected' ? 'bg-emerald-500' : ($app->status->name === 'disconnected' ? 'bg-rose-500' : 'bg-gray-400') }}"></span>
|
||||||
{{ ucfirst($app->status->name) }}
|
{{ ucfirst($app->status->name) }}
|
||||||
<x-lucide-chevron-down class="w-3.5 h-3.5 opacity-60"/>
|
<x-lucide-chevron-down class="w-3.5 h-3.5 opacity-60"/>
|
||||||
</label>
|
</label>
|
||||||
<ul tabindex="0"
|
<ul tabindex="0"
|
||||||
class="dropdown-content menu p-1 shadow-md bg-base-100 rounded-box w-44 border border-gray-100 mt-1 z-30">
|
class="dropdown-content menu p-1 shadow-md bg-base-100 rounded-box w-44 border border-gray-100 mt-1 z-30">
|
||||||
|
@foreach(App\Enums\ConnectionStatusEnum::cases() as $statusCase)
|
||||||
<li>
|
<li>
|
||||||
<button wire:click="toggleStatus" class="font-medium text-xs py-2">
|
<button wire:click="changeStatus('{{ $statusCase->value }}')"
|
||||||
<x-lucide-refresh-cw class="w-3.5 h-3.5 opacity-60"/>
|
class="font-medium text-xs py-2">
|
||||||
Toggle Status
|
<span
|
||||||
|
class="w-2 h-2 rounded-full {{ $statusCase->value === 'connected' ? 'bg-emerald-500' : ($statusCase->value === 'disconnected' ? 'bg-rose-500' : 'bg-gray-400') }}"></span>
|
||||||
|
{{ ucfirst($statusCase->value) }}
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
|
@endforeach
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<x-mary-button
|
|
||||||
icon="lucide.history"
|
|
||||||
class="btn-sm btn-outline border-gray-200"
|
|
||||||
label="View Logs"
|
|
||||||
tooltip="Browse audit trail"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<x-mary-button
|
<x-mary-button
|
||||||
icon="lucide.arrow-left"
|
icon="lucide.arrow-left"
|
||||||
class="btn-sm btn-ghost text-gray-500"
|
class="btn-sm btn-ghost text-gray-500"
|
||||||
@ -206,10 +313,7 @@ class="alert-error alert-soft"
|
|||||||
@endif
|
@endif
|
||||||
|
|
||||||
<!-- Client Credentials Section -->
|
<!-- Client Credentials Section -->
|
||||||
<div class="border border-gray-200 rounded-xl bg-white overflow-hidden shadow-2xs">
|
<x-shared.card title="Client Credentials">
|
||||||
<div class="px-6 py-4 border-b border-gray-200 flex justify-between items-center bg-gray-50/50">
|
|
||||||
<h3 class="font-bold text-sm text-gray-800 ">Client Credentials</h3>
|
|
||||||
</div>
|
|
||||||
<div class="p-6 space-y-6">
|
<div class="p-6 space-y-6">
|
||||||
<!-- Client ID -->
|
<!-- Client ID -->
|
||||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-2 md:items-center">
|
<div class="grid grid-cols-1 md:grid-cols-4 gap-2 md:items-center">
|
||||||
@ -262,7 +366,7 @@ class="text-xs font-medium text-gray-500">Proof Key for Code Exchange (PKCE)</sp
|
|||||||
<label class="flex items-center gap-2">
|
<label class="flex items-center gap-2">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
readonly
|
@click.prevent
|
||||||
@checked($passportClient && $this->requiresPkce($passportClient))
|
@checked($passportClient && $this->requiresPkce($passportClient))
|
||||||
class="checkbox checkbox-xs"
|
class="checkbox checkbox-xs"
|
||||||
/>
|
/>
|
||||||
@ -272,13 +376,12 @@ class="text-xs text-gray-600">Require PKCE as additional verification</span>
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</x-shared.card>
|
||||||
|
|
||||||
<!-- Client Secrets Section -->
|
<!-- Client Secrets Section -->
|
||||||
<div class="border border-gray-200 rounded-xl bg-white overflow-hidden shadow-2xs">
|
<x-shared.card title="Client Secrets">
|
||||||
<div class="px-6 py-4 border-b border-gray-200 flex justify-between items-center bg-gray-50/50">
|
|
||||||
<h3 class="font-bold text-xs uppercase tracking-wider text-gray-500">Client Secrets</h3>
|
|
||||||
@if($passportClient)
|
@if($passportClient)
|
||||||
|
<x-slot:actions>
|
||||||
<x-mary-button
|
<x-mary-button
|
||||||
wire:click="generateNewSecret"
|
wire:click="generateNewSecret"
|
||||||
class="btn-outline border-blue-200 text-blue-600 hover:bg-blue-50"
|
class="btn-outline border-blue-200 text-blue-600 hover:bg-blue-50"
|
||||||
@ -286,8 +389,8 @@ class=" btn-outline border-blue-200 text-blue-600 hover:bg-blue-50"
|
|||||||
label="Generate new secret"
|
label="Generate new secret"
|
||||||
spinner="generateNewSecret"
|
spinner="generateNewSecret"
|
||||||
/>
|
/>
|
||||||
|
</x-slot:actions>
|
||||||
@endif
|
@endif
|
||||||
</div>
|
|
||||||
<div class="overflow-x-auto">
|
<div class="overflow-x-auto">
|
||||||
<table class="table w-full text-xs">
|
<table class="table w-full text-xs">
|
||||||
<thead>
|
<thead>
|
||||||
@ -323,20 +426,57 @@ class="badge {{ !$passportClient->revoked ? 'badge-soft badge-success' : 'badge-
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</x-shared.card>
|
||||||
</div>
|
</div>
|
||||||
</x-mary-tab>
|
</x-mary-tab>
|
||||||
|
|
||||||
<!-- Sign On Tab -->
|
<!-- Sign On Tab -->
|
||||||
<x-mary-tab name="signon-tab" label="Sign On">
|
<x-mary-tab name="signon-tab" label="Sign On">
|
||||||
<div class="space-y-6">
|
<div class="space-y-6">
|
||||||
<div class="border border-gray-200 rounded-xl bg-white overflow-hidden shadow-2xs">
|
<x-shared.card title="Sign In Redirect URIs">
|
||||||
<div class="px-6 py-4 border-b border-gray-200 bg-gray-50/50">
|
<x-slot:actions>
|
||||||
<h3 class="font-bold text-sm text-gray-800 ">Sign In Redirect URIs</h3>
|
@if($editRedirectsMode)
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<x-mary-button
|
||||||
|
icon="lucide.check"
|
||||||
|
class="btn-primary"
|
||||||
|
label="Save"
|
||||||
|
wire:click="saveRedirectUris"
|
||||||
|
/>
|
||||||
|
<x-mary-button
|
||||||
|
icon="lucide.x"
|
||||||
|
class="btn-ghost"
|
||||||
|
label="Cancel"
|
||||||
|
wire:click="cancelEditRedirects"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@else
|
||||||
|
<x-mary-button
|
||||||
|
icon="lucide.pencil"
|
||||||
|
class="btn-outline border-gray-200"
|
||||||
|
label="Edit"
|
||||||
|
wire:click="startEditRedirects"
|
||||||
|
/>
|
||||||
|
@endif
|
||||||
|
</x-slot:actions>
|
||||||
|
|
||||||
<div class="p-6 space-y-3">
|
<div class="p-6 space-y-3">
|
||||||
@if(!empty($passportClient?->redirect_uris))
|
@if($editRedirectsMode)
|
||||||
@foreach($passportClient->redirect_uris as $uri)
|
<x-shared.repeater-input
|
||||||
|
model="editSignInUris"
|
||||||
|
:items="$editSignInUris"
|
||||||
|
placeholder="https://app.example.com/callback"
|
||||||
|
min="0"
|
||||||
|
/>
|
||||||
|
@else
|
||||||
|
@php
|
||||||
|
$onlySignInUris = array_values(array_filter(
|
||||||
|
$passportClient?->redirect_uris ?? [],
|
||||||
|
fn($uri) => !in_array($uri, $signOutUris, true)
|
||||||
|
));
|
||||||
|
@endphp
|
||||||
|
@if(!empty($onlySignInUris))
|
||||||
|
@foreach($onlySignInUris as $uri)
|
||||||
<div
|
<div
|
||||||
class="flex items-center gap-2 p-2 border border-gray-150 rounded-lg font-mono text-xs text-gray-600 bg-gray-50">
|
class="flex items-center gap-2 p-2 border border-gray-150 rounded-lg font-mono text-xs text-gray-600 bg-gray-50">
|
||||||
<x-lucide-link class="w-3.5 h-3.5 text-gray-400"/>
|
<x-lucide-link class="w-3.5 h-3.5 text-gray-400"/>
|
||||||
@ -344,17 +484,24 @@ class="flex items-center gap-2 p-2 border border-gray-150 rounded-lg font-mono t
|
|||||||
</div>
|
</div>
|
||||||
@endforeach
|
@endforeach
|
||||||
@else
|
@else
|
||||||
<span class="text-xs text-gray-400 italic">No Sign In Redirect URIs configured</span>
|
<span
|
||||||
|
class="text-xs text-gray-400 italic">No Sign In Redirect URIs configured</span>
|
||||||
|
@endif
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</x-shared.card>
|
||||||
|
|
||||||
<!-- Sign Out Redirect URIs -->
|
<!-- Sign Out Redirect URIs -->
|
||||||
<div class="border border-gray-200 rounded-xl bg-white overflow-hidden shadow-2xs">
|
<x-shared.card title="Sign Out Redirect URIs">
|
||||||
<div class="px-6 py-4 border-b border-gray-200 bg-gray-50/50">
|
|
||||||
<h3 class="font-bold text-sm text-gray-800 ">Sign Out Redirect URIs</h3>
|
|
||||||
</div>
|
|
||||||
<div class="p-6 space-y-3">
|
<div class="p-6 space-y-3">
|
||||||
|
@if($editRedirectsMode)
|
||||||
|
<x-shared.repeater-input
|
||||||
|
model="editSignOutUris"
|
||||||
|
:items="$editSignOutUris"
|
||||||
|
placeholder="https://app.example.com/logout"
|
||||||
|
min="0"
|
||||||
|
/>
|
||||||
|
@else
|
||||||
@if(!empty($signOutUris))
|
@if(!empty($signOutUris))
|
||||||
@foreach($signOutUris as $uri)
|
@foreach($signOutUris as $uri)
|
||||||
<div
|
<div
|
||||||
@ -364,20 +511,19 @@ class="flex items-center gap-2 p-2 border border-gray-150 rounded-lg font-mono t
|
|||||||
</div>
|
</div>
|
||||||
@endforeach
|
@endforeach
|
||||||
@else
|
@else
|
||||||
<span class="text-xs text-gray-400 italic">No Sign Out Redirect URIs configured</span>
|
<span
|
||||||
|
class="text-xs text-gray-400 italic">No Sign Out Redirect URIs configured</span>
|
||||||
|
@endif
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</x-shared.card>
|
||||||
</div>
|
</div>
|
||||||
</x-mary-tab>
|
</x-mary-tab>
|
||||||
|
|
||||||
<!-- Assignments Tab -->
|
<!-- Assignments Tab -->
|
||||||
<x-mary-tab name="assignments-tab" label="Assignments">
|
<x-mary-tab name="assignments-tab" label="Assignments">
|
||||||
<div class="">
|
<div class="">
|
||||||
<div class="border border-gray-200 rounded-xl bg-white overflow-hidden shadow-2xs">
|
<x-shared.card title="Assigned Roles">
|
||||||
<div class="px-6 py-4 border-b border-gray-200 bg-gray-50/50">
|
|
||||||
<h3 class="font-bold text-sm text-gray-800 ">Assigned Roles</h3>
|
|
||||||
</div>
|
|
||||||
<div class="overflow-x-auto">
|
<div class="overflow-x-auto">
|
||||||
<table class="table w-full text-xs">
|
<table class="table w-full text-xs">
|
||||||
<thead>
|
<thead>
|
||||||
@ -414,18 +560,14 @@ class="badge badge-soft badge-primary font-semibold text-xs px-2.5 py-1">
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</x-shared.card>
|
||||||
</div>
|
</div>
|
||||||
</x-mary-tab>
|
</x-mary-tab>
|
||||||
|
|
||||||
<!-- API Scopes Tab -->
|
<!-- Scopes Tab -->
|
||||||
<x-mary-tab name="scopes-tab" label="API Scopes">
|
<x-mary-tab name="scopes-tab" label="Scopes">
|
||||||
<div class="">
|
<div class="">
|
||||||
<div class="border border-gray-200 rounded-xl bg-white overflow-hidden shadow-2xs">
|
<x-shared.card title="Granted OAuth/OIDC Scopes">
|
||||||
<div class="px-6 py-4 border-b border-gray-200 bg-gray-50/50">
|
|
||||||
<h3 class="font-bold text-sm text-gray-800 ">Granted OAuth/OIDC
|
|
||||||
Scopes</h3>
|
|
||||||
</div>
|
|
||||||
<div class="p-6 space-y-4">
|
<div class="p-6 space-y-4">
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<div class="flex items-start gap-3 p-3 border border-gray-100 rounded-xl bg-gray-50/50">
|
<div class="flex items-start gap-3 p-3 border border-gray-100 rounded-xl bg-gray-50/50">
|
||||||
@ -470,10 +612,33 @@ class="badge badge-soft badge-primary font-semibold text-xs px-2.5 py-1">
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</x-shared.card>
|
||||||
</div>
|
</div>
|
||||||
</x-mary-tab>
|
</x-mary-tab>
|
||||||
|
|
||||||
</x-mary-tabs>
|
</x-mary-tabs>
|
||||||
</x-shared.card>
|
</x-shared.card>
|
||||||
|
|
||||||
|
<!-- Edit General Info Modal -->
|
||||||
|
<x-mary-modal wire:model="editGeneralModal" title="Edit App General Settings" class="backdrop-blur">
|
||||||
|
<x-mary-form wire:submit="saveGeneralInfo">
|
||||||
|
<x-mary-input
|
||||||
|
label="Application Name"
|
||||||
|
wire:model="editName"
|
||||||
|
placeholder="e.g. My App"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<x-mary-file
|
||||||
|
label="Application Logo"
|
||||||
|
wire:model="editLogo"
|
||||||
|
accept="image/jpeg,image/png"
|
||||||
|
hint="Optional. Max 2MB."
|
||||||
|
/>
|
||||||
|
|
||||||
|
<x-slot:actions>
|
||||||
|
<x-mary-button label="Cancel" @click="$wire.editGeneralModal = false" class="btn-ghost"/>
|
||||||
|
<x-mary-button label="Save Settings" type="submit" class="btn-primary" spinner="saveGeneralInfo"/>
|
||||||
|
</x-slot:actions>
|
||||||
|
</x-mary-form>
|
||||||
|
</x-mary-modal>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
347
resources/views/pages/apps/url/⚡show.blade.php
Normal file
347
resources/views/pages/apps/url/⚡show.blade.php
Normal file
@ -0,0 +1,347 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Concerns\HandlesOperations;
|
||||||
|
use App\Enums\Permissions\AppPermissionEnum;
|
||||||
|
use App\Services\ConnectedAppService;
|
||||||
|
use App\Services\ApplicationLogoUploader;
|
||||||
|
use App\Data\ConnectedApp\ConnectAppRequest;
|
||||||
|
use App\Enums\ConnectionStatusEnum;
|
||||||
|
use App\Models\{ConnectedApp, ConnectionStatus};
|
||||||
|
use Livewire\Component;
|
||||||
|
use Livewire\Attributes\Title;
|
||||||
|
use Livewire\WithFileUploads;
|
||||||
|
|
||||||
|
new #[Title('URL Application Details')]
|
||||||
|
class extends Component {
|
||||||
|
use HandlesOperations, WithFileUploads;
|
||||||
|
|
||||||
|
public int $appId;
|
||||||
|
public ConnectedApp $app;
|
||||||
|
public string $selectedTab = 'general-tab';
|
||||||
|
|
||||||
|
// Edit fields
|
||||||
|
public bool $editGeneralModal = false;
|
||||||
|
public string $editName = '';
|
||||||
|
public string $editAccessUrl = '';
|
||||||
|
public $editLogo = null;
|
||||||
|
public bool $editAccessUrlMode = false;
|
||||||
|
|
||||||
|
private ConnectedAppService $appService;
|
||||||
|
private ApplicationLogoUploader $logoUploader;
|
||||||
|
|
||||||
|
public function boot(ConnectedAppService $service, ApplicationLogoUploader $logoUploader): void
|
||||||
|
{
|
||||||
|
$this->appService = $service;
|
||||||
|
$this->logoUploader = $logoUploader;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function mount(int $id): void
|
||||||
|
{
|
||||||
|
$this->authorize(AppPermissionEnum::Read->value);
|
||||||
|
$this->appId = $id;
|
||||||
|
$this->loadAppData();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function loadAppData(): void
|
||||||
|
{
|
||||||
|
$this->app = ConnectedApp::with(['protocol', 'status', 'roles'])->findOrFail($this->appId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function changeStatus(string $statusName): void
|
||||||
|
{
|
||||||
|
$this->authorize(AppPermissionEnum::Update->value);
|
||||||
|
|
||||||
|
$this->attempt(
|
||||||
|
action: function () use ($statusName) {
|
||||||
|
$statusId = ConnectionStatus::where('name', $statusName)->firstOrFail()->id;
|
||||||
|
$request = new ConnectAppRequest(
|
||||||
|
name: $this->app->name,
|
||||||
|
connectionProtocolId: $this->app->connection_protocol_id,
|
||||||
|
connectionProviderId: $this->app->connection_provider_id,
|
||||||
|
slug: $this->app->slug,
|
||||||
|
connectionStatusId: $statusId,
|
||||||
|
settings: $this->app->settings,
|
||||||
|
logo: $this->app->logo,
|
||||||
|
);
|
||||||
|
$this->appService->update($this->app->id, $request);
|
||||||
|
$this->loadAppData();
|
||||||
|
},
|
||||||
|
successMessage: 'Application status updated successfully!'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function goBack(): void
|
||||||
|
{
|
||||||
|
$this->redirectRoute('apps.index', navigate: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function openEditGeneralModal(): void
|
||||||
|
{
|
||||||
|
$this->editName = $this->app->name;
|
||||||
|
$this->editLogo = null;
|
||||||
|
$this->editGeneralModal = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function saveGeneralInfo(): void
|
||||||
|
{
|
||||||
|
$this->authorize(AppPermissionEnum::Update->value);
|
||||||
|
|
||||||
|
$this->validate([
|
||||||
|
'editName' => 'required|string|max:255',
|
||||||
|
'editLogo' => 'nullable|image|max:2048',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->attempt(
|
||||||
|
action: function () {
|
||||||
|
$logoPath = $this->editLogo
|
||||||
|
? $this->logoUploader->store($this->editLogo)
|
||||||
|
: $this->app->logo;
|
||||||
|
|
||||||
|
$request = new ConnectAppRequest(
|
||||||
|
name: $this->editName,
|
||||||
|
connectionProtocolId: $this->app->connection_protocol_id,
|
||||||
|
connectionProviderId: $this->app->connection_provider_id,
|
||||||
|
slug: \Illuminate\Support\Str::slug($this->editName),
|
||||||
|
connectionStatusId: $this->app->connection_status_id,
|
||||||
|
settings: $this->app->settings,
|
||||||
|
logo: $logoPath,
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->appService->update($this->app->id, $request);
|
||||||
|
|
||||||
|
$this->editGeneralModal = false;
|
||||||
|
$this->success('Application updated successfully!');
|
||||||
|
$this->loadAppData();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function startEditAccessUrl(): void
|
||||||
|
{
|
||||||
|
$this->editAccessUrl = data_get($this->app, 'settings.access_url', '');
|
||||||
|
$this->editAccessUrlMode = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function cancelEditAccessUrl(): void
|
||||||
|
{
|
||||||
|
$this->editAccessUrlMode = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function saveAccessUrl(): void
|
||||||
|
{
|
||||||
|
$this->authorize(AppPermissionEnum::Update->value);
|
||||||
|
|
||||||
|
$this->validate([
|
||||||
|
'editAccessUrl' => 'required|url|max:255',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->attempt(
|
||||||
|
action: function () {
|
||||||
|
$settings = $this->app->settings ?? [];
|
||||||
|
$settings['access_url'] = $this->editAccessUrl;
|
||||||
|
|
||||||
|
$request = new ConnectAppRequest(
|
||||||
|
name: $this->app->name,
|
||||||
|
connectionProtocolId: $this->app->connection_protocol_id,
|
||||||
|
connectionProviderId: $this->app->connection_provider_id,
|
||||||
|
slug: $this->app->slug,
|
||||||
|
connectionStatusId: $this->app->connection_status_id,
|
||||||
|
settings: $settings,
|
||||||
|
logo: $this->app->logo,
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->appService->update($this->app->id, $request);
|
||||||
|
|
||||||
|
$this->editAccessUrlMode = false;
|
||||||
|
$this->success('Access URL updated successfully!');
|
||||||
|
$this->loadAppData();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="space-y-6">
|
||||||
|
<!-- Header Card -->
|
||||||
|
<x-shared.card class="p-6">
|
||||||
|
<div class="flex flex-col md:flex-row md:items-center justify-between gap-4">
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<div class="relative group">
|
||||||
|
<x-mary-avatar
|
||||||
|
:image="$app->logo ? asset($app->logo) : null"
|
||||||
|
:placeholder="$app->logo ? null : 'lucide.settings'"
|
||||||
|
class="w-16 h-16 rounded-xl border border-gray-200 p-2 shadow-xs bg-gray-50 group-hover:opacity-85 transition-opacity"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<h1 class="text-2xl font-bold text-gray-900 ">{{ $app->name }}</h1>
|
||||||
|
<button wire:click="openEditGeneralModal" class="text-gray-400 hover:text-gray-600 ">
|
||||||
|
<x-lucide-pencil class="w-4 h-4"/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<p class="text-sm text-gray-500 mt-0.5">Protocol: <span
|
||||||
|
class="font-semibold text-gray-700 ">{{ strtoupper($app->protocol->name) }}</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<!-- Status Dropdown/Trigger -->
|
||||||
|
<div class="dropdown dropdown-end">
|
||||||
|
<label tabindex="0"
|
||||||
|
class="btn btn-sm btn-outline gap-1.5 font-semibold {{ $app->status->name === 'connected' ? 'border-emerald-200 text-emerald-700 bg-emerald-50/30' : ($app->status->name === 'disconnected' ? 'border-rose-200 text-rose-700 bg-rose-50/30' : 'border-gray-200 text-gray-700 bg-gray-50/30') }}">
|
||||||
|
<span
|
||||||
|
class="w-2 h-2 rounded-full {{ $app->status->name === 'connected' ? 'bg-emerald-500' : ($app->status->name === 'disconnected' ? 'bg-rose-500' : 'bg-gray-400') }}"></span>
|
||||||
|
{{ ucfirst($app->status->name) }}
|
||||||
|
<x-lucide-chevron-down class="w-3.5 h-3.5 opacity-60"/>
|
||||||
|
</label>
|
||||||
|
<ul tabindex="0"
|
||||||
|
class="dropdown-content menu p-1 shadow-md bg-base-100 rounded-box w-44 border border-gray-100 mt-1 z-30">
|
||||||
|
@foreach(App\Enums\ConnectionStatusEnum::cases() as $statusCase)
|
||||||
|
<li>
|
||||||
|
<button wire:click="changeStatus('{{ $statusCase->value }}')"
|
||||||
|
class="font-medium text-xs py-2">
|
||||||
|
<span
|
||||||
|
class="w-2 h-2 rounded-full {{ $statusCase->value === 'connected' ? 'bg-emerald-500' : ($statusCase->value === 'disconnected' ? 'bg-rose-500' : 'bg-gray-400') }}"></span>
|
||||||
|
{{ ucfirst($statusCase->value) }}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
@endforeach
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<x-mary-button
|
||||||
|
icon="lucide.arrow-left"
|
||||||
|
class="btn-sm btn-ghost text-gray-500"
|
||||||
|
wire:click="goBack"
|
||||||
|
label="Back"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</x-shared.card>
|
||||||
|
|
||||||
|
<!-- Main Content Area -->
|
||||||
|
<x-shared.card class="p-0 overflow-hidden">
|
||||||
|
<x-mary-tabs wire:model="selectedTab" class="px-4" label-div-class="pt-3 bg-gray-50 border-b border-gray-200"
|
||||||
|
label-class="font-bold pb-3">
|
||||||
|
|
||||||
|
<!-- General Tab -->
|
||||||
|
<x-mary-tab name="general-tab" label="General">
|
||||||
|
<div class="space-y-6">
|
||||||
|
<x-shared.card title="Access URL">
|
||||||
|
<x-slot:actions>
|
||||||
|
@if($editAccessUrlMode)
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<x-mary-button
|
||||||
|
icon="lucide.check"
|
||||||
|
class="btn-primary"
|
||||||
|
label="Save"
|
||||||
|
wire:click="saveAccessUrl"
|
||||||
|
/>
|
||||||
|
<x-mary-button
|
||||||
|
icon="lucide.x"
|
||||||
|
class="btn-ghost"
|
||||||
|
label="Cancel"
|
||||||
|
wire:click="cancelEditAccessUrl"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
@else
|
||||||
|
<x-mary-button
|
||||||
|
icon="lucide.pencil"
|
||||||
|
label="Edit"
|
||||||
|
wire:click="startEditAccessUrl"
|
||||||
|
class="btn-primary btn-outline"
|
||||||
|
/>
|
||||||
|
@endif
|
||||||
|
</x-slot:actions>
|
||||||
|
<div class="p-6 space-y-6">
|
||||||
|
@if($editAccessUrlMode)
|
||||||
|
<x-mary-input
|
||||||
|
type="text"
|
||||||
|
wire:model="editAccessUrl"
|
||||||
|
class="input-sm font-mono w-full bg-white border-gray-200"
|
||||||
|
placeholder="https://app.example.com"
|
||||||
|
/>
|
||||||
|
@else
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
readonly
|
||||||
|
value="{{data_get($app, 'settings.access_url')}}"
|
||||||
|
class="input input-sm input-bordered font-mono w-full bg-gray-50 border-gray-200 focus:outline-hidden"
|
||||||
|
/>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</x-shared.card>
|
||||||
|
</div>
|
||||||
|
</x-mary-tab>
|
||||||
|
|
||||||
|
<!-- Assignments Tab -->
|
||||||
|
<x-mary-tab name="assignments-tab" label="Assignments">
|
||||||
|
<div class="">
|
||||||
|
<x-shared.card title="Assigned Roles">
|
||||||
|
<div class="overflow-x-auto">
|
||||||
|
<table class="table w-full text-xs">
|
||||||
|
<thead>
|
||||||
|
<tr class="bg-gray-50/30 text-gray-400 border-b border-gray-100">
|
||||||
|
<th class="font-semibold py-3 px-6 text-left">Role Name</th>
|
||||||
|
<th class="font-semibold py-3 px-6 text-left">Priority</th>
|
||||||
|
<th class="font-semibold py-3 px-6 text-right">Access Type</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@forelse($app->roles as $role)
|
||||||
|
<tr class="hover:bg-gray-50/50 border-b border-gray-100">
|
||||||
|
<td class="py-4 px-6 font-semibold text-gray-800 0">
|
||||||
|
{{ $role->name }}
|
||||||
|
</td>
|
||||||
|
<td class="py-4 px-6 text-gray-500">
|
||||||
|
{{ $role->priority }}
|
||||||
|
</td>
|
||||||
|
<td class="py-4 px-6 text-right">
|
||||||
|
<span
|
||||||
|
class="badge badge-soft badge-primary font-semibold text-xs px-2.5 py-1">
|
||||||
|
SSO Access
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
@empty
|
||||||
|
<tr>
|
||||||
|
<td colspan="3" class="text-center py-6 text-gray-400 italic">
|
||||||
|
No specific roles assigned. Accessible by everyone depending on user access
|
||||||
|
setting.
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
@endforelse
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</x-shared.card>
|
||||||
|
</div>
|
||||||
|
</x-mary-tab>
|
||||||
|
</x-mary-tabs>
|
||||||
|
</x-shared.card>
|
||||||
|
|
||||||
|
<!-- Edit General Info Modal -->
|
||||||
|
<x-mary-modal wire:model="editGeneralModal" title="Edit App General Settings" class="backdrop-blur">
|
||||||
|
<x-mary-form wire:submit="saveGeneralInfo">
|
||||||
|
<x-mary-input
|
||||||
|
label="Application Name"
|
||||||
|
wire:model="editName"
|
||||||
|
placeholder="e.g. My App"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<x-mary-file
|
||||||
|
label="Application Logo"
|
||||||
|
wire:model="editLogo"
|
||||||
|
accept="image/jpeg,image/png"
|
||||||
|
hint="Optional. Max 2MB."
|
||||||
|
/>
|
||||||
|
|
||||||
|
<x-slot:actions>
|
||||||
|
<x-mary-button label="Cancel" @click="$wire.editGeneralModal = false" class="btn-ghost"/>
|
||||||
|
<x-mary-button label="Save Settings" type="submit" class="btn-primary" spinner="saveGeneralInfo"/>
|
||||||
|
</x-slot:actions>
|
||||||
|
</x-mary-form>
|
||||||
|
</x-mary-modal>
|
||||||
|
</div>
|
||||||
@ -70,7 +70,7 @@ public function viewApp(int $appId): void
|
|||||||
if ($protocol === 'oidc') {
|
if ($protocol === 'oidc') {
|
||||||
$this->redirectRoute('apps.oidc.show', ['id' => $appId], navigate: true);
|
$this->redirectRoute('apps.oidc.show', ['id' => $appId], navigate: true);
|
||||||
} else {
|
} else {
|
||||||
$this->redirectRoute('apps.edit', ['id' => $appId], navigate: true);
|
$this->redirectRoute('apps.url.show', ['id' => $appId], navigate: true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -18,11 +18,17 @@
|
|||||||
->name(ConnectionProtocolEnum::OIDC->value.'.')
|
->name(ConnectionProtocolEnum::OIDC->value.'.')
|
||||||
->group(function (): void {
|
->group(function (): void {
|
||||||
Route::livewire('create', 'pages::apps.oidc.create')->name('create');
|
Route::livewire('create', 'pages::apps.oidc.create')->name('create');
|
||||||
|
Route::livewire(
|
||||||
|
'{id}/view',
|
||||||
|
'pages::apps.oidc.show'
|
||||||
|
)->middleware(PermissionMiddleware::using(AppPermissionEnum::Read))->name('show');
|
||||||
|
|
||||||
});
|
});
|
||||||
Route::prefix(ConnectionProtocolEnum::URL->value)
|
Route::prefix(ConnectionProtocolEnum::URL->value)
|
||||||
->name(ConnectionProtocolEnum::URL->value.'.')
|
->name(ConnectionProtocolEnum::URL->value.'.')
|
||||||
->group(function (): void {
|
->group(function (): void {
|
||||||
Route::livewire('create', 'pages::apps.url.create')->name('create');
|
Route::livewire('create', 'pages::apps.url.create')->name('create');
|
||||||
|
Route::livewire('{id}/view', 'pages::apps.url.show')->name('show');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -31,11 +37,6 @@
|
|||||||
'pages::apps.edit'
|
'pages::apps.edit'
|
||||||
)->middleware(PermissionMiddleware::using(AppPermissionEnum::Update))->name('edit');
|
)->middleware(PermissionMiddleware::using(AppPermissionEnum::Update))->name('edit');
|
||||||
|
|
||||||
Route::livewire(
|
|
||||||
'oidc/{id}/view',
|
|
||||||
'pages::apps.oidc.show'
|
|
||||||
)->middleware(PermissionMiddleware::using(AppPermissionEnum::Read))->name('oidc.show');
|
|
||||||
|
|
||||||
Route::livewire('/', 'pages::apps.index')->name('index');
|
Route::livewire('/', 'pages::apps.index')->name('index');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -87,3 +87,76 @@
|
|||||||
$this->passportClient->refresh();
|
$this->passportClient->refresh();
|
||||||
expect(Hash::check($newSecretPlain, $this->passportClient->secret))->toBeTrue();
|
expect(Hash::check($newSecretPlain, $this->passportClient->secret))->toBeTrue();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('allows updating app name and logo', function (): void {
|
||||||
|
$admin = User::factory()->create();
|
||||||
|
$admin->assignRole('admin');
|
||||||
|
|
||||||
|
$this->actingAs($admin);
|
||||||
|
|
||||||
|
$logo = Illuminate\Http\UploadedFile::fake()->image('logo.png');
|
||||||
|
|
||||||
|
Livewire::test('pages::apps.oidc.show', ['id' => $this->connectedApp->id])
|
||||||
|
->call('openEditGeneralModal')
|
||||||
|
->assertSet('editName', 'Test OIDC App')
|
||||||
|
->set('editName', 'Updated OIDC App Name')
|
||||||
|
->set('editLogo', $logo)
|
||||||
|
->call('saveGeneralInfo')
|
||||||
|
->assertHasNoErrors();
|
||||||
|
|
||||||
|
// Verify it updated in Database
|
||||||
|
$this->connectedApp->refresh();
|
||||||
|
expect($this->connectedApp->name)->toBe('Updated OIDC App Name')
|
||||||
|
->and($this->connectedApp->logo)->not->toBeNull();
|
||||||
|
|
||||||
|
// Verify Passport client name was updated
|
||||||
|
$this->passportClient->refresh();
|
||||||
|
expect($this->passportClient->name)->toBe('Updated OIDC App Name');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('allows updating connection status via changeStatus', function (): void {
|
||||||
|
$admin = User::factory()->create();
|
||||||
|
$admin->assignRole('admin');
|
||||||
|
|
||||||
|
$this->actingAs($admin);
|
||||||
|
|
||||||
|
// Initial status should be 'connected'
|
||||||
|
expect($this->connectedApp->status->name)->toBe('connected');
|
||||||
|
|
||||||
|
Livewire::test('pages::apps.oidc.show', ['id' => $this->connectedApp->id])
|
||||||
|
->call('changeStatus', 'disconnected')
|
||||||
|
->assertHasNoErrors();
|
||||||
|
|
||||||
|
// Verify status updated in DB
|
||||||
|
$this->connectedApp->refresh();
|
||||||
|
expect($this->connectedApp->status->name)->toBe('disconnected');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('allows updating redirect URIs', function (): void {
|
||||||
|
$admin = User::factory()->create();
|
||||||
|
$admin->assignRole('admin');
|
||||||
|
|
||||||
|
$this->actingAs($admin);
|
||||||
|
|
||||||
|
Livewire::test('pages::apps.oidc.show', ['id' => $this->connectedApp->id])
|
||||||
|
->call('startEditRedirects')
|
||||||
|
->assertSet('editSignInUris', ['https://test-oidc-app.example.com/callback'])
|
||||||
|
->assertSet('editSignOutUris', ['https://test-oidc-app.example.com/logout'])
|
||||||
|
// Add a new sign-in URI
|
||||||
|
->call('addRepeaterItem', 'editSignInUris')
|
||||||
|
->set('editSignInUris.1', 'https://test-oidc-app.example.com/callback2')
|
||||||
|
// Remove a sign-out URI
|
||||||
|
->call('removeRepeaterItem', 'editSignOutUris', 0)
|
||||||
|
->call('saveRedirectUris')
|
||||||
|
->assertHasNoErrors();
|
||||||
|
|
||||||
|
// Verify changes in Passport Client and Connected App settings
|
||||||
|
$this->passportClient->refresh();
|
||||||
|
expect($this->passportClient->redirect_uris)->toBe([
|
||||||
|
'https://test-oidc-app.example.com/callback',
|
||||||
|
'https://test-oidc-app.example.com/callback2',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->connectedApp->refresh();
|
||||||
|
expect($this->connectedApp->settings['signOutUris'] ?? [])->toBe([]);
|
||||||
|
});
|
||||||
|
|||||||
102
tests/Feature/UrlAppViewTest.php
Normal file
102
tests/Feature/UrlAppViewTest.php
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use App\Models\{ConnectedApp, ConnectionProtocol, ConnectionStatus, User};
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Livewire\Livewire;
|
||||||
|
|
||||||
|
uses(RefreshDatabase::class);
|
||||||
|
|
||||||
|
beforeEach(function (): void {
|
||||||
|
$this->seed(Database\Seeders\PermissionSeeder::class);
|
||||||
|
$this->seed(Database\Seeders\DefaultRoleWithPermissionSeeder::class);
|
||||||
|
$this->seed(Database\Seeders\ConnectionProtocolSeeder::class);
|
||||||
|
$this->seed(Database\Seeders\ConnectionProviderSeeder::class);
|
||||||
|
$this->seed(Database\Seeders\ConnectionStatusSeeder::class);
|
||||||
|
|
||||||
|
$this->urlProtocol = ConnectionProtocol::query()->where('name', 'url')->firstOrFail();
|
||||||
|
$this->status = ConnectionStatus::query()->where('name', 'connected')->firstOrFail();
|
||||||
|
|
||||||
|
$this->connectedApp = ConnectedApp::query()->create([
|
||||||
|
'name' => 'Test URL App',
|
||||||
|
'slug' => 'test-url-app',
|
||||||
|
'connection_protocol_id' => $this->urlProtocol->id,
|
||||||
|
'connection_provider_id' => 0,
|
||||||
|
'connection_status_id' => $this->status->id,
|
||||||
|
'settings' => [
|
||||||
|
'access_url' => 'https://test-url-app.example.com',
|
||||||
|
'is_connected_to_microsoft' => false,
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('authorizes only admin users to view the URL show page', function (): void {
|
||||||
|
$user = User::factory()->create();
|
||||||
|
$user->assignRole('user');
|
||||||
|
|
||||||
|
Livewire::actingAs($user)
|
||||||
|
->test('pages::apps.url.show', ['id' => $this->connectedApp->id])
|
||||||
|
->assertForbidden();
|
||||||
|
|
||||||
|
$admin = User::factory()->create();
|
||||||
|
$admin->assignRole('admin');
|
||||||
|
|
||||||
|
Livewire::actingAs($admin)
|
||||||
|
->test('pages::apps.url.show', ['id' => $this->connectedApp->id])
|
||||||
|
->assertOk();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('allows updating name and logo', function (): void {
|
||||||
|
$admin = User::factory()->create();
|
||||||
|
$admin->assignRole('admin');
|
||||||
|
|
||||||
|
$this->actingAs($admin);
|
||||||
|
|
||||||
|
$logo = Illuminate\Http\UploadedFile::fake()->image('logo.png');
|
||||||
|
|
||||||
|
Livewire::test('pages::apps.url.show', ['id' => $this->connectedApp->id])
|
||||||
|
->call('openEditGeneralModal')
|
||||||
|
->assertSet('editName', 'Test URL App')
|
||||||
|
->set('editName', 'Updated URL App Name')
|
||||||
|
->set('editLogo', $logo)
|
||||||
|
->call('saveGeneralInfo')
|
||||||
|
->assertHasNoErrors();
|
||||||
|
|
||||||
|
$this->connectedApp->refresh();
|
||||||
|
expect($this->connectedApp->name)->toBe('Updated URL App Name')
|
||||||
|
->and($this->connectedApp->logo)->not->toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('allows updating access URL in place', function (): void {
|
||||||
|
$admin = User::factory()->create();
|
||||||
|
$admin->assignRole('admin');
|
||||||
|
|
||||||
|
$this->actingAs($admin);
|
||||||
|
|
||||||
|
Livewire::test('pages::apps.url.show', ['id' => $this->connectedApp->id])
|
||||||
|
->call('startEditAccessUrl')
|
||||||
|
->assertSet('editAccessUrl', 'https://test-url-app.example.com')
|
||||||
|
->set('editAccessUrl', 'https://updated-url-app.example.com')
|
||||||
|
->call('saveAccessUrl')
|
||||||
|
->assertHasNoErrors();
|
||||||
|
|
||||||
|
$this->connectedApp->refresh();
|
||||||
|
expect(data_get($this->connectedApp->settings, 'access_url'))->toBe('https://updated-url-app.example.com');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('allows updating connection status via changeStatus', function (): void {
|
||||||
|
$admin = User::factory()->create();
|
||||||
|
$admin->assignRole('admin');
|
||||||
|
|
||||||
|
$this->actingAs($admin);
|
||||||
|
|
||||||
|
expect($this->connectedApp->status->name)->toBe('connected');
|
||||||
|
|
||||||
|
Livewire::test('pages::apps.url.show', ['id' => $this->connectedApp->id])
|
||||||
|
->call('changeStatus', 'disconnected')
|
||||||
|
->assertHasNoErrors();
|
||||||
|
|
||||||
|
$this->connectedApp->refresh();
|
||||||
|
expect($this->connectedApp->status->name)->toBe('disconnected');
|
||||||
|
});
|
||||||
Loading…
x
Reference in New Issue
Block a user