- Improved logging for `MicrosoftGraphService` methods to provide detailed insights into federation configuration processes. - Refactored error handling for domain federation configuration, including specific checks for authorization and resource errors. - Streamlined federation flow logic and made it more resilient to exceptions. - Updated UI components to handle configuration states more effectively and added button functionality for failed connections. - Expanded test coverage for Microsoft Graph Federation scenarios, including authorization failures and domain validation. - masked the sensitive fields
599 lines
31 KiB
PHP
599 lines
31 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Concerns\HandlesOperations;
|
|
use App\Data\MicrosoftGraph\MicrosoftGraphCredentialsData;
|
|
use App\Enums\ConnectionStatusEnum;
|
|
use App\Enums\FederatedIdpMfaBehaviorEnum;
|
|
use App\Services\MicrosoftGraphService;
|
|
use Livewire\Attributes\Computed;
|
|
use Livewire\Attributes\Title;
|
|
use Livewire\Component;
|
|
|
|
new #[Title("Microsoft Graph Federation")]
|
|
class extends Component {
|
|
use HandlesOperations;
|
|
|
|
protected MicrosoftGraphService $service;
|
|
|
|
public bool $credentialsLoaded = false;
|
|
public bool $certificateLoaded = false;
|
|
|
|
public string $tenantId = '';
|
|
public string $clientId = '';
|
|
public string $clientSecret = '';
|
|
public string $domainName = '';
|
|
public string $displayName = '';
|
|
public FederatedIdpMfaBehaviorEnum $mfaBehavior;
|
|
|
|
public ConnectionStatusEnum $connectionStatus = ConnectionStatusEnum::Disconnected;
|
|
public ?string $federationId = null;
|
|
public ?array $activeFederationConfig = null;
|
|
|
|
public array $debugResponses = [];
|
|
|
|
public function boot(MicrosoftGraphService $service): void
|
|
{
|
|
$this->service = $service;
|
|
}
|
|
|
|
#[Computed]
|
|
public function mfaBehaviorOptions(): array
|
|
{
|
|
return array_map(fn($case) => [
|
|
'id' => $case->value,
|
|
'name' => match ($case) {
|
|
FederatedIdpMfaBehaviorEnum::AcceptIfMfaDoneByFederatedIdp => 'Accept if MFA Done by Federated IdP',
|
|
FederatedIdpMfaBehaviorEnum::EnforceMfaByFederatedIdp => 'Enforce MFA by Federated IdP',
|
|
FederatedIdpMfaBehaviorEnum::RejectMfaByFederatedIdp => 'Always perform MFA (Reject Federated IdP MFA)',
|
|
},
|
|
], FederatedIdpMfaBehaviorEnum::cases());
|
|
}
|
|
|
|
public function mount(): void
|
|
{
|
|
$config = config('services.microsoft_graph');
|
|
|
|
$this->tenantId = (string) ($config['tenant_id'] ?? '');
|
|
$this->clientId = (string) ($config['client_id'] ?? '');
|
|
$this->clientSecret = (string) ($config['client_secret'] ?? '');
|
|
$this->domainName = (string) ($config['domain_name'] ?? '');
|
|
$this->displayName = (string) ($config['display_name'] ?? 'Company SSO');
|
|
$this->mfaBehavior = FederatedIdpMfaBehaviorEnum::tryFrom(
|
|
(string) ($config['mfa_behavior'] ?? '')
|
|
) ?? FederatedIdpMfaBehaviorEnum::AcceptIfMfaDoneByFederatedIdp;
|
|
|
|
if (
|
|
!empty($this->tenantId) &&
|
|
!empty($this->clientId) &&
|
|
!empty($this->clientSecret) &&
|
|
!empty($this->domainName)
|
|
) {
|
|
$this->credentialsLoaded = true;
|
|
}
|
|
|
|
if (file_exists(storage_path("app/saml/idp.crt"))) {
|
|
$this->certificateLoaded = true;
|
|
}
|
|
|
|
if ($this->credentialsLoaded) {
|
|
$this->checkStatus(false);
|
|
}
|
|
}
|
|
|
|
public function checkStatus(bool $silent = false): void
|
|
{
|
|
$this->attempt(
|
|
action: function () use ($silent) {
|
|
$credentials = new MicrosoftGraphCredentialsData(
|
|
tenantId: $this->tenantId,
|
|
clientId: $this->clientId,
|
|
clientSecret: $this->clientSecret,
|
|
domainName: $this->domainName,
|
|
displayName: $this->displayName,
|
|
mfaBehavior: $this->mfaBehavior,
|
|
);
|
|
|
|
$result = $this->service->getFederationConfig($credentials);
|
|
|
|
foreach ($result["transactions"] as $tx) {
|
|
$this->debugResponses[] = array_merge($tx, [
|
|
"timestamp" => now()->format("Y-m-d H:i:s"),
|
|
]);
|
|
}
|
|
|
|
if ($result["status"] === "success") {
|
|
if ($result["config"] !== null) {
|
|
$this->connectionStatus =
|
|
ConnectionStatusEnum::Connected;
|
|
$this->federationId = $result["config"]["id"];
|
|
$this->activeFederationConfig = $result["config"];
|
|
if (!$silent) {
|
|
$this->success(
|
|
"Federation is active on Microsoft Entra ID!",
|
|
);
|
|
}
|
|
} else {
|
|
$this->connectionStatus =
|
|
ConnectionStatusEnum::Disconnected;
|
|
$this->federationId = null;
|
|
$this->activeFederationConfig = null;
|
|
if (!$silent) {
|
|
$this->success(
|
|
"Domain is managed. No federation configuration found.",
|
|
);
|
|
}
|
|
}
|
|
} else {
|
|
$this->connectionStatus = ConnectionStatusEnum::Error;
|
|
if (!$silent) {
|
|
$this->error(
|
|
$result["message"] ?? "Failed to check status.",
|
|
);
|
|
}
|
|
}
|
|
},
|
|
showSuccess: false,
|
|
);
|
|
}
|
|
|
|
public function connect(): void
|
|
{
|
|
if (!$this->certificateLoaded) {
|
|
$this->error(
|
|
"Missing public certificate! Run php artisan saml:generate-keys first.",
|
|
);
|
|
return;
|
|
}
|
|
|
|
$this->attempt(
|
|
action: function () {
|
|
$credentials = new MicrosoftGraphCredentialsData(
|
|
tenantId: $this->tenantId,
|
|
clientId: $this->clientId,
|
|
clientSecret: $this->clientSecret,
|
|
domainName: $this->domainName,
|
|
displayName: $this->displayName,
|
|
mfaBehavior: $this->mfaBehavior,
|
|
);
|
|
|
|
$certPem = file_get_contents(storage_path("app/saml/idp.crt"));
|
|
$issuerUri = route("saml.metadata");
|
|
$passiveSignInUri = route("saml.sso");
|
|
$activeSignInUri = route("saml.sso");
|
|
$signOutUri = route("home");
|
|
|
|
$result = $this->service->connectFederation(
|
|
credentials: $credentials,
|
|
certPem: $certPem,
|
|
issuerUri: $issuerUri,
|
|
passiveSignInUri: $passiveSignInUri,
|
|
activeSignInUri: $activeSignInUri,
|
|
signOutUri: $signOutUri,
|
|
);
|
|
|
|
foreach ($result["transactions"] as $tx) {
|
|
$this->debugResponses[] = array_merge($tx, [
|
|
"timestamp" => now()->format("Y-m-d H:i:s"),
|
|
]);
|
|
}
|
|
|
|
if ($result["status"] === "success") {
|
|
$this->connectionStatus = ConnectionStatusEnum::Connected;
|
|
$this->federationId = $result["config"]["id"] ?? null;
|
|
$this->activeFederationConfig = $result["config"];
|
|
$this->success(
|
|
"SAML IDP successfully federated with Microsoft Entra ID!",
|
|
);
|
|
} else {
|
|
$this->connectionStatus = ConnectionStatusEnum::Error;
|
|
$this->error(
|
|
$result["message"] ?? "Failed to connect federation.",
|
|
);
|
|
}
|
|
},
|
|
showSuccess: false,
|
|
);
|
|
}
|
|
|
|
public function disconnect(): void
|
|
{
|
|
if (empty($this->federationId)) {
|
|
$this->error("No active federation ID found. Check status first.");
|
|
return;
|
|
}
|
|
|
|
$this->attempt(
|
|
action: function () {
|
|
$credentials = new MicrosoftGraphCredentialsData(
|
|
tenantId: $this->tenantId,
|
|
clientId: $this->clientId,
|
|
clientSecret: $this->clientSecret,
|
|
domainName: $this->domainName,
|
|
displayName: $this->displayName,
|
|
mfaBehavior: $this->mfaBehavior,
|
|
);
|
|
|
|
$result = $this->service->disconnectFederation(
|
|
$credentials,
|
|
$this->federationId,
|
|
);
|
|
|
|
foreach ($result["transactions"] as $tx) {
|
|
$this->debugResponses[] = array_merge($tx, [
|
|
"timestamp" => now()->format("Y-m-d H:i:s"),
|
|
]);
|
|
}
|
|
|
|
if ($result["status"] === "success") {
|
|
$this->connectionStatus =
|
|
ConnectionStatusEnum::Disconnected;
|
|
$this->federationId = null;
|
|
$this->activeFederationConfig = null;
|
|
$this->success(
|
|
"SAML IDP disconnected from Microsoft Entra ID. Domain returned to Managed.",
|
|
);
|
|
} else {
|
|
$this->connectionStatus = ConnectionStatusEnum::Error;
|
|
$this->error(
|
|
$result["message"] ??
|
|
"Failed to disconnect federation.",
|
|
);
|
|
}
|
|
},
|
|
showSuccess: false,
|
|
);
|
|
}
|
|
|
|
public function clearDebug(): void
|
|
{
|
|
$this->debugResponses = [];
|
|
$this->success("Debug outputs cleared.");
|
|
}
|
|
};
|
|
?>
|
|
|
|
<div>
|
|
<div class="space-y-6">
|
|
<!-- Banner Alerts -->
|
|
@if(!$credentialsLoaded)
|
|
<div
|
|
class="rounded-xl border border-amber-200/50 bg-amber-50/50 p-4 shadow-sm backdrop-blur-xs flex items-start gap-x-3">
|
|
<span class="text-amber-500 mt-0.5"><x-mary-icon name="lucide.triangle-alert" class="w-5 h-5"/></span>
|
|
<div>
|
|
<h3 class="font-medium text-amber-900">Environment Configuration Required</h3>
|
|
<p class="text-sm text-amber-700 mt-1 leading-relaxed">
|
|
To enable the Microsoft Graph API integration, please configure the environment, to include
|
|
needed values.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
@endif
|
|
|
|
@if($credentialsLoaded && !$certificateLoaded)
|
|
<div
|
|
class="rounded-xl border border-rose-200/50 bg-rose-50/50 p-4 shadow-sm backdrop-blur-xs flex items-start gap-x-3">
|
|
<span class="text-rose-500 mt-0.5"><x-mary-icon name="lucide.shield-alert" class="w-5 h-5"/></span>
|
|
<div>
|
|
<h3 class="font-medium text-rose-900">SAML Signing Certificate Missing</h3>
|
|
<p class="text-sm text-rose-700 mt-1">
|
|
A verified public certificate is required to sign SAML requests sent to Microsoft Entra ID. No
|
|
certificate was found.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
@endif
|
|
|
|
<!-- Main Card Group -->
|
|
<div class="grid grid-cols-1 xl:grid-cols-3 gap-6">
|
|
<!-- Left Side: Configuration details & Actions -->
|
|
<div class="xl:col-span-2 space-y-6">
|
|
<x-shared.card title="Microsoft Entra Federation Settings">
|
|
<x-slot:actions>
|
|
<div class="flex items-center gap-2">
|
|
<span class="text-xs text-gray-400 font-medium">Status:</span>
|
|
@if($connectionStatus === ConnectionStatusEnum::Connected)
|
|
<span
|
|
class="inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-xs font-semibold bg-emerald-100 text-emerald-800 border border-emerald-200">
|
|
<span class="w-2 h-2 rounded-full bg-emerald-500 animate-pulse"></span>
|
|
Federated
|
|
</span>
|
|
@elseif($connectionStatus === ConnectionStatusEnum::Disconnected)
|
|
<span
|
|
class="inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-xs font-semibold bg-gray-100 text-gray-800 border border-gray-200">
|
|
<span class="w-2 h-2 rounded-full bg-gray-400"></span>
|
|
Managed
|
|
</span>
|
|
@elseif($connectionStatus === ConnectionStatusEnum::Pending)
|
|
<span
|
|
class="inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-xs font-semibold bg-amber-100 text-amber-800 border border-amber-200">
|
|
<span class="w-2 h-2 rounded-full bg-amber-500 animate-spin"></span>
|
|
Connecting...
|
|
</span>
|
|
@else
|
|
<span
|
|
class="inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-xs font-semibold bg-rose-100 text-rose-800 border border-rose-200">
|
|
<span class="w-2 h-2 rounded-full bg-rose-500"></span>
|
|
Error
|
|
</span>
|
|
@endif
|
|
</div>
|
|
</x-slot:actions>
|
|
|
|
<div class="p-6 space-y-6">
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
<div>
|
|
<x-mary-input label="Microsoft Entra Tenant ID"
|
|
:value="$tenantId ? '••••••••••••••••••••••••': 'Not Configured'"
|
|
disabled class="bg-gray-50/50" icon="lucide.key-round"/>
|
|
<span class="text-xs text-gray-400 mt-1 block">MICROSOFT_GRAPH_TENANT_ID</span>
|
|
</div>
|
|
|
|
<div>
|
|
<x-mary-input label="Application (Client) ID"
|
|
:value="$clientId ? '••••••••••••••••••••••••' : 'Not Configured'"
|
|
disabled class="bg-gray-50/50" icon="lucide.app-window"/>
|
|
<span class="text-xs text-gray-400 mt-1 block">MICROSOFT_GRAPH_CLIENT_ID</span>
|
|
</div>
|
|
|
|
<div>
|
|
<x-mary-input label="Client Secret" value="••••••••••••••••••••••••" disabled
|
|
class="bg-gray-50/50" icon="lucide.lock"/>
|
|
<span class="text-xs text-gray-400 mt-1 block">MICROSOFT_GRAPH_CLIENT_SECRET</span>
|
|
</div>
|
|
|
|
<div>
|
|
<x-mary-input label="Verified Domain Name" :value="$domainName ?: 'Not Configured'"
|
|
disabled class="bg-gray-50/50" icon="lucide.globe"/>
|
|
<span class="text-xs text-gray-400 mt-1 block">MICROSOFT_GRAPH_DOMAIN_NAME</span>
|
|
</div>
|
|
|
|
<div>
|
|
<x-mary-input label="Federated IDP Display Name" :value="$displayName" disabled
|
|
class="bg-gray-50/50" icon="lucide.fingerprint"/>
|
|
<span class="text-xs text-gray-400 mt-1 block">MICROSOFT_GRAPH_DISPLAY_NAME</span>
|
|
</div>
|
|
|
|
<div>
|
|
<x-mary-choices
|
|
label="Federated IDP MFA Behavior"
|
|
wire:model="mfaBehavior"
|
|
:options="$this->mfaBehaviorOptions"
|
|
:single="true"
|
|
disabled
|
|
class="bg-gray-50/50 text-xs"
|
|
icon="lucide.shield-check"
|
|
/>
|
|
<span class="text-xs text-gray-400 mt-1 block">MICROSOFT_GRAPH_MFA_BEHAVIOR</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- SAML Endpoints Preview Card -->
|
|
<div class="rounded-xl border border-gray-100 bg-gray-50/50 p-4 space-y-3">
|
|
<h4 class="text-sm font-semibold text-gray-700 flex items-center gap-1.5">
|
|
<x-mary-icon name="lucide.link" class="w-4 h-4 text-sky-500"/>
|
|
local Identity Provider Endpoints
|
|
</h4>
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 text-xs font-mono">
|
|
<div>
|
|
<span class="text-gray-400 block font-sans font-medium text-[10px] uppercase">Issuer URI (Entity ID)</span>
|
|
<span class="text-gray-700 select-all">{{ route('saml.metadata') }}</span>
|
|
</div>
|
|
<div>
|
|
<span class="text-gray-400 block font-sans font-medium text-[10px] uppercase">Passive Sign-In Endpoint (SSO)</span>
|
|
<span class="text-gray-700 select-all">{{ route('saml.sso') }}</span>
|
|
</div>
|
|
<div>
|
|
<span class="text-gray-400 block font-sans font-medium text-[10px] uppercase">Active Sign-In Endpoint (WS-Trust)</span>
|
|
<span class="text-gray-700 select-all">{{ route('saml.sso') }}</span>
|
|
</div>
|
|
<div>
|
|
<span class="text-gray-400 block font-sans font-medium text-[10px] uppercase">Sign-Out URI</span>
|
|
<span class="text-gray-700 select-all">{{ route('home') }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Action controls -->
|
|
<div
|
|
class="flex flex-wrap items-center justify-between gap-4 border-t border-gray-100 pt-6 mt-4">
|
|
<x-mary-button wire:click="checkStatus" spinner="checkStatus" icon="lucide.refresh-cw"
|
|
:disabled="!$credentialsLoaded" class="btn-outline">
|
|
Check Status
|
|
</x-mary-button>
|
|
|
|
<div class="flex items-center gap-2 ml-auto">
|
|
@if($connectionStatus === ConnectionStatusEnum::Connected)
|
|
<x-mary-button wire:click="disconnect" spinner="disconnect" icon="lucide.unlink"
|
|
variant="danger" class="btn-error text-white font-medium shadow-sm">
|
|
Disconnect Federation
|
|
</x-mary-button>
|
|
@else
|
|
<x-mary-button wire:click="connect" spinner="connect" icon="lucide.link-2"
|
|
:disabled="!$credentialsLoaded || !$certificateLoaded"
|
|
class="btn-primary text-white font-medium shadow-sm">
|
|
Connect & Federate
|
|
</x-mary-button>
|
|
@endif
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</x-shared.card>
|
|
</div>
|
|
|
|
<!-- Right Side: Connection Status Inspector details -->
|
|
<div class="space-y-6">
|
|
<x-shared.card title="Active Federation State">
|
|
<div class="p-6 space-y-4">
|
|
@if($activeFederationConfig)
|
|
<div class="space-y-4">
|
|
<div class="flex justify-between border-b border-gray-100 pb-2 text-xs">
|
|
<span class="text-gray-400 font-medium">Federation ID</span>
|
|
<span
|
|
class="text-gray-700 font-mono select-all">{{ Str::limit($activeFederationConfig['id'], 18) }}</span>
|
|
</div>
|
|
<div class="flex justify-between border-b border-gray-100 pb-2 text-xs">
|
|
<span class="text-gray-400 font-medium">Preferred Protocol</span>
|
|
<span
|
|
class="text-gray-700 font-semibold">{{ $activeFederationConfig['preferredAuthenticationProtocol'] ?? 'Unknown' }}</span>
|
|
</div>
|
|
<div class="flex justify-between border-b border-gray-100 pb-2 text-xs">
|
|
<span class="text-gray-400 font-medium">Entra MFA Rule</span>
|
|
<span
|
|
class="text-gray-700 font-semibold">{{ $activeFederationConfig['federatedIdpMfaBehavior'] ?? 'Unknown' }}</span>
|
|
</div>
|
|
<div class="space-y-1">
|
|
<span class="text-gray-400 font-medium text-xs block">Active Signing Certificate Thumbprint</span>
|
|
<div
|
|
class="bg-gray-50 font-mono text-[10px] p-2.5 rounded border border-gray-100 break-all select-all text-gray-600">
|
|
{{ hash('sha1', base64_decode($activeFederationConfig['signingCertificate'])) }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
@else
|
|
<div class="text-center py-12">
|
|
<div
|
|
class="w-12 h-12 rounded-full bg-gray-100 flex items-center justify-center mx-auto text-gray-400 mb-3">
|
|
<x-mary-icon name="lucide.cloud-off" class="w-6 h-6"/>
|
|
</div>
|
|
<h4 class="font-medium text-gray-700">No Active Federation</h4>
|
|
<p class="text-xs text-gray-400 mt-1 max-w-45 mx-auto leading-relaxed mb-4">
|
|
Connect this IDP to Microsoft Entra to sync active configurations.
|
|
</p>
|
|
<x-mary-button
|
|
wire:click="connect"
|
|
spinner="connect"
|
|
icon="lucide.link-2"
|
|
:disabled="!$credentialsLoaded || !$certificateLoaded"
|
|
class="btn-primary btn-sm text-white font-medium shadow-sm">
|
|
Create Federation
|
|
</x-mary-button>
|
|
</div>
|
|
@endif
|
|
</div>
|
|
</x-shared.card>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Debugging section: Full transaction logs -->
|
|
<x-shared.card title="Microsoft Graph Transactions (Debug Logs)">
|
|
<x-slot:actions>
|
|
@if(count($debugResponses) > 0)
|
|
<x-mary-button wire:click="clearDebug" icon="lucide.trash-2"
|
|
class="btn-xs btn-outline btn-error text-red-500">
|
|
Clear Logs
|
|
</x-mary-button>
|
|
@endif
|
|
</x-slot:actions>
|
|
|
|
<div class="p-6">
|
|
@if(count($debugResponses) > 0)
|
|
<div class="space-y-4 max-h-150 overflow-y-auto pr-2 custom-scrollbar">
|
|
@foreach(array_reverse($debugResponses) as $index => $log)
|
|
<div x-data="{ open: false }"
|
|
class="rounded-xl border border-slate-800 bg-slate-950/90 shadow-lg overflow-hidden font-mono">
|
|
<!-- Log Header -->
|
|
<button @click="open = !open"
|
|
class="w-full flex flex-wrap items-center justify-between p-4 gap-4 text-left border-b border-slate-800 hover:bg-slate-900/50 transition-colors">
|
|
<div class="flex items-center gap-3">
|
|
<span class="text-xs text-slate-500">{{ $log['timestamp'] }}</span>
|
|
<span @class([
|
|
'px-2 py-0.5 rounded text-[10px] font-bold uppercase',
|
|
'bg-emerald-950 text-emerald-400 border border-emerald-800' => $log['method'] === 'GET',
|
|
'bg-sky-950 text-sky-400 border border-sky-800' => $log['method'] === 'POST',
|
|
'bg-rose-950 text-rose-400 border border-rose-800' => $log['method'] === 'DELETE',
|
|
])>{{ $log['method'] }}</span>
|
|
<span class="font-semibold text-xs text-slate-200">{{ $log['name'] }}</span>
|
|
</div>
|
|
<div class="flex items-center gap-3">
|
|
<span
|
|
class="text-slate-400 text-xs truncate max-w-50 sm:max-w-xs md:max-w-md">{{ $log['url'] }}</span>
|
|
@if(is_numeric($log['status']))
|
|
<span @class([
|
|
'px-2.5 py-0.5 rounded-full text-xs font-bold',
|
|
'bg-emerald-950 text-emerald-400 border border-emerald-850' => $log['status'] >= 200 && $log['status'] < 300,
|
|
'bg-rose-950 text-rose-400 border border-rose-850' => $log['status'] >= 400,
|
|
])>{{ $log['status'] }}</span>
|
|
@else
|
|
<span
|
|
class="px-2.5 py-0.5 rounded-full text-xs font-bold bg-rose-950 text-rose-400 border border-rose-850">{{ $log['status'] }}</span>
|
|
@endif
|
|
<span class="text-slate-500 text-xs" :class="open ? 'rotate-180' : ''">
|
|
<x-mary-icon name="lucide.chevron-down"
|
|
class="w-4 h-4 transition-transform"/>
|
|
</span>
|
|
</div>
|
|
</button>
|
|
|
|
<!-- Log Body details -->
|
|
<div x-cloak x-show="open" class="p-4 space-y-4 border-t border-slate-900 text-xs">
|
|
<!-- Request Details -->
|
|
@if(!empty($log['request_headers']) || !empty($log['request_body']))
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
@if(!empty($log['request_headers']))
|
|
<div class="space-y-1">
|
|
<span
|
|
class="text-slate-500 font-sans font-medium uppercase text-[10px]">Request Headers</span>
|
|
<pre
|
|
class="bg-slate-900 text-emerald-500/80 p-3 rounded-lg overflow-x-auto text-[11px] leading-relaxed">{{ json_encode($log['request_headers'], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) }}</pre>
|
|
</div>
|
|
@endif
|
|
@if(!empty($log['request_body']))
|
|
<div class="space-y-1">
|
|
<span
|
|
class="text-slate-500 font-sans font-medium uppercase text-[10px]">Request Payload</span>
|
|
<pre
|
|
class="bg-slate-900 text-emerald-500/80 p-3 rounded-lg overflow-x-auto text-[11px] leading-relaxed">{{ json_encode($log['request_body'], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) }}</pre>
|
|
</div>
|
|
@endif
|
|
</div>
|
|
@endif
|
|
|
|
<!-- Response Details -->
|
|
<div class="space-y-1">
|
|
<span class="text-slate-500 font-sans font-medium uppercase text-[10px]">Response Body Payload</span>
|
|
<pre
|
|
class="bg-slate-900 text-cyan-400/90 p-4 rounded-lg overflow-x-auto text-[11px] leading-relaxed">{{ json_encode($log['response_body'], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) }}</pre>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
@endforeach
|
|
</div>
|
|
@else
|
|
<div class="text-center py-12 text-gray-400">
|
|
<div
|
|
class="w-12 h-12 rounded-full bg-gray-50 flex items-center justify-center mx-auto text-gray-400 mb-3 border border-gray-100">
|
|
<x-mary-icon name="lucide.terminal" class="w-5 h-5"/>
|
|
</div>
|
|
<h4 class="font-medium text-gray-600">Terminal Output Empty</h4>
|
|
<p class="text-xs text-gray-400 mt-1 max-w-60 mx-auto leading-relaxed">Federation
|
|
transactions will appear here as HTTP raw records when you click Check, Connect, or
|
|
Disconnect.</p>
|
|
</div>
|
|
@endif
|
|
</div>
|
|
</x-shared.card>
|
|
</div>
|
|
</div>
|
|
|
|
<style>
|
|
.custom-scrollbar::-webkit-scrollbar {
|
|
width: 6px;
|
|
height: 6px;
|
|
}
|
|
|
|
.custom-scrollbar::-webkit-scrollbar-track {
|
|
background: transparent;
|
|
}
|
|
|
|
.custom-scrollbar::-webkit-scrollbar-thumb {
|
|
background: rgba(148, 163, 184, 0.2);
|
|
border-radius: 4px;
|
|
}
|
|
|
|
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
|
|
background: rgba(148, 163, 184, 0.4);
|
|
}
|
|
</style>
|