= feaa8b6c93 Feat: Add modular URL app creation with role-based access and Microsoft integration support
- Introduced `StoreURLAppForm` and `StoreURLAppData` for form handling and data transformation.
- Added `UrlAppService` to streamline URL app creation with settings like `accessUrl` and `isConnectedToMicrosoft`.
- Enhanced user access controls with role-based permissions using `UserAccessTypeEnum`.
- Updated UI for URL app creation with support for logos and dynamic role selection.
- Implemented feature tests to validate URL app creation and access URL formatting.
2026-06-22 07:21:26 +00:00

218 lines
9.2 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"));
}
}
#[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();
}
};
?>
<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->isWarning ? '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->isWarning)
<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->isConnectedToMicrosoft)
<div
class="flex flex-col gap-3">
<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->protocolName === 'oidc' || ( $service->protocolName === 'url' && !$service->isConnectedToMicrosoft))
<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->isWarning ? 'text-amber-500 animate-pulse' : 'text-gray-400 dark:text-gray-500' }}"/>
<span
class="text-sm font-medium {{ $service->isWarning ? 'text-amber-600 dark:text-amber-400 font-semibold' : 'text-gray-600 dark:text-gray-400' }}">
{{ $service->isUnlimited ? 'Unlimited' : Illuminate\Support\Carbon::parse($service->duration)->diffForHumans() }}
</span>
</div>
<div class="flex items-center gap-2">
@if($service->isWarning)
<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>