- 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.
285 lines
12 KiB
PHP
285 lines
12 KiB
PHP
<?php
|
|
|
|
use App\Models\OauthToken;
|
|
use App\Services\UserAppAccessService;
|
|
use Illuminate\Support\Collection;
|
|
use Livewire\Attributes\{Computed, Layout, Title};
|
|
use Livewire\Component;
|
|
use Mary\Traits\Toast;
|
|
|
|
new #[Layout("layouts.app.sidebar")]
|
|
#[Title("Dashboard")]
|
|
class extends
|
|
Component {
|
|
use Toast;
|
|
|
|
private UserAppAccessService $service;
|
|
|
|
public function boot(UserAppAccessService $service): void
|
|
{
|
|
$this->service = $service;
|
|
}
|
|
|
|
public function mount(): void
|
|
{
|
|
$user = auth()->user();
|
|
|
|
// Using spatie/laravel-activitylog v5 to log dashboard access
|
|
if ($user) {
|
|
activity()
|
|
->causedBy($user)
|
|
->event("dashboard_access")
|
|
->log("User accessed their assigned services dashboard.");
|
|
}
|
|
|
|
// Show toast from OAuth redirect flash data
|
|
if (session("entra_success")) {
|
|
$this->success(session("entra_success"));
|
|
}
|
|
|
|
if (session("entra_error")) {
|
|
$this->error(session("entra_error"));
|
|
}
|
|
|
|
if (session("keka_success")) {
|
|
$this->success(session("keka_success"));
|
|
}
|
|
|
|
if (session("keka_error")) {
|
|
$this->error(session("keka_error"));
|
|
}
|
|
}
|
|
|
|
#[Computed]
|
|
public function services(): Collection
|
|
{
|
|
$user = auth()->user();
|
|
|
|
if (!$user) {
|
|
return collect();
|
|
}
|
|
|
|
return $this->service->getActiveServices($user);
|
|
}
|
|
|
|
#[Computed]
|
|
public function entraToken(): ?OauthToken
|
|
{
|
|
return auth()->user()?->oauthToken('microsoft');
|
|
}
|
|
|
|
#[Computed]
|
|
public function isEntraConnected(): bool
|
|
{
|
|
$token = $this->entraToken();
|
|
|
|
return $token !== null && !$token->isExpired();
|
|
}
|
|
|
|
#[Computed]
|
|
public function kekaToken(): ?OauthToken
|
|
{
|
|
return auth()->user()?->oauthToken('keka');
|
|
}
|
|
|
|
#[Computed]
|
|
public function isKekaConnected(): bool
|
|
{
|
|
$token = $this->kekaToken();
|
|
|
|
return $token !== null && !$token->isExpired();
|
|
}
|
|
};
|
|
?>
|
|
|
|
<div class="space-y-6">
|
|
<x-shared.card class="p-4 flex items-center justify-between">
|
|
<x-mary-avatar
|
|
:placeholder="auth()->user()->initials()"
|
|
:title="'Welcome back, ' . auth()->user()->name . '!'"
|
|
>
|
|
<x-slot:subtitle>
|
|
<p class="text-sm text-gray-500 dark:text-gray-400 mt-0.5">
|
|
Manage and access your authenticated single sign-on services.
|
|
</p>
|
|
</x-slot:subtitle>
|
|
</x-mary-avatar>
|
|
|
|
<div class="flex items-center gap-4">
|
|
<x-mary-badge class="badge-soft badge-primary font-semibold text-xs px-3 py-1.5">
|
|
Active Account
|
|
</x-mary-badge>
|
|
<span class="text-xs text-gray-400 dark:text-gray-500 flex items-center gap-1">
|
|
<x-mary-icon name="lucide.calendar" class="w-3.5 h-3.5"/>
|
|
{{ now()->format('F j, Y') }}
|
|
</span>
|
|
</div>
|
|
</x-shared.card>
|
|
|
|
@if($this->services->isEmpty())
|
|
<x-shared.card
|
|
class="flex flex-col items-center justify-center text-center p-12 min-h-100">
|
|
<div class="p-4 bg-gray-50 rounded-full mb-4">
|
|
<x-mary-icon
|
|
name="lucide.shield-alert"
|
|
class="w-12 h-12 text-gray-600 animate-pulse"
|
|
/>
|
|
</div>
|
|
<h3 class="text-lg font-semibold text-gray-900 mb-2">No Active Services Assigned</h3>
|
|
<p class="text-sm text-gray-500 max-w-md mb-6">
|
|
Your account does not currently have any active or visible services assigned by your role, or your
|
|
access duration has expired.
|
|
</p>
|
|
<x-mary-alert icon="lucide.info" class="alert-soft alert-info">
|
|
To request access to specific applications, please contact your system administrator or IT helpdesk with
|
|
your account details
|
|
</x-mary-alert>
|
|
</x-shared.card>
|
|
@else
|
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
@foreach($this->services as $service)
|
|
<x-shared.card
|
|
:title="$service->name"
|
|
icon="lucide-key-round"
|
|
class="group relative {{ $service->is_warning ? 'border-amber-300 dark:border-amber-900/40 bg-amber-50/10' :'' }} rounded-2xl shadow-xs flex flex-col justify-between"
|
|
>
|
|
<x-slot:actions>
|
|
@if($service->is_warning)
|
|
<x-mary-badge
|
|
class="badge-soft badge-warning font-semibold text-xs px-2.5 py-1 flex items-center gap-1.5">
|
|
<x-mary-icon name="lucide.alert-triangle"
|
|
class="w-3.5 h-3.5 inline animate-pulse text-amber-600 dark:text-amber-400"/>
|
|
<span>Expiring Soon</span>
|
|
</x-mary-badge>
|
|
@else
|
|
<x-mary-badge class="badge-soft badge-success text-xs font-semibold px-2.5 py-1">
|
|
Active
|
|
</x-mary-badge>
|
|
@endif
|
|
</x-slot:actions>
|
|
|
|
<div class="p-4 flex-1 flex flex-col justify-between gap-4">
|
|
<div>
|
|
@if($service->protocol_name === 'url' && $service->provider_slug === 'azure-fd')
|
|
<div
|
|
class="flex flex-col gap-3">
|
|
<div class="flex items-center justify-between">
|
|
<span class="text-xs font-semibold text-gray-700">Microsoft Integration</span>
|
|
</div>
|
|
|
|
<div class="flex items-center gap-2">
|
|
@if($this->isEntraConnected)
|
|
<x-mary-button
|
|
external
|
|
icon="lucide.external-link"
|
|
:link="route('entra.open')"
|
|
class=" btn-primary flex-1"
|
|
label="Open Outlook"
|
|
/>
|
|
|
|
<form method="POST" action="{{ route('entra.disconnect') }}"
|
|
class="inline">
|
|
@csrf
|
|
@method('DELETE')
|
|
<x-mary-button
|
|
icon="lucide.unplug"
|
|
type="submit"
|
|
class=" btn-error"
|
|
tooltip="Disconnect"
|
|
/>
|
|
</form>
|
|
@else
|
|
<x-mary-button
|
|
external
|
|
no-wire-navigate
|
|
icon="lucide.plug"
|
|
:link="route('entra.connect')"
|
|
class="btn-primary flex-1"
|
|
label="Connect"
|
|
/>
|
|
@endif
|
|
</div>
|
|
</div>
|
|
@endif
|
|
|
|
@if($service->protocol_name === 'url' && $service->provider_slug === 'keka-hr')
|
|
<div
|
|
class="flex flex-col gap-3">
|
|
<div class="flex items-center justify-between">
|
|
<span class="text-xs font-semibold text-gray-700">Keka Integration</span>
|
|
</div>
|
|
|
|
<div class="flex items-center gap-2">
|
|
@if($this->isKekaConnected)
|
|
<x-mary-button
|
|
icon="lucide.external-link"
|
|
:link="route('keka.connect')"
|
|
class=" btn-primary flex-1"
|
|
label="Open Keka"
|
|
/>
|
|
|
|
<form method="POST" action="{{ route('keka.disconnect') }}"
|
|
class="inline">
|
|
@csrf
|
|
@method('DELETE')
|
|
<x-mary-button
|
|
icon="lucide.unplug"
|
|
type="submit"
|
|
class=" btn-error"
|
|
tooltip="Disconnect"
|
|
/>
|
|
</form>
|
|
@else
|
|
<x-mary-button
|
|
external
|
|
no-wire-navigate
|
|
icon="lucide.plug"
|
|
:link="route('keka.connect')"
|
|
class="btn-primary flex-1"
|
|
label="Connect"
|
|
/>
|
|
@endif
|
|
</div>
|
|
</div>
|
|
@endif
|
|
|
|
@if($service->protocol_name === 'oidc')
|
|
<x-mary-button
|
|
external
|
|
:link="$service->accessUrl"
|
|
class="btn-primary w-full"
|
|
label="Open"
|
|
/>
|
|
@endif
|
|
</div>
|
|
|
|
<div class="pt-4 border-t border-gray-200 flex items-center justify-between gap-4">
|
|
<div class="flex items-center gap-2">
|
|
<x-mary-icon name="lucide.clock"
|
|
class="w-4 h-4 {{ $service->is_warning ? 'text-amber-500 animate-pulse' : 'text-gray-400 dark:text-gray-500' }}"/>
|
|
<span
|
|
class="text-sm font-medium {{ $service->is_warning ? 'text-amber-600 dark:text-amber-400 font-semibold' : 'text-gray-600 dark:text-gray-400' }}">
|
|
{{ Illuminate\Support\Carbon::parse($service->duration)->diffForHumans() }}
|
|
</span>
|
|
</div>
|
|
|
|
<div class="flex items-center gap-2">
|
|
@if($service->is_warning)
|
|
<x-mary-button
|
|
icon="lucide.life-buoy"
|
|
:link="route('tickets.index', ['raise' => 1, 'app_id' => $service->id])"
|
|
wire:navigate
|
|
class="btn-sm btn-circle btn-soft btn-warning"
|
|
tooltip="Raise Ticket"
|
|
/>
|
|
@endif
|
|
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</x-shared.card>
|
|
@endforeach
|
|
</div>
|
|
@endif
|
|
</div>
|