- 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.
347 lines
15 KiB
PHP
347 lines
15 KiB
PHP
<?php
|
|
|
|
use App\Data\Ui\Sidebar\NavItemData;
|
|
use App\Data\Ui\Sidebar\NavSubItemData;
|
|
use App\Enums\Permissions\{AppPermissionEnum,
|
|
ConnectionProviderPermissionEnum,
|
|
ProfilePermissionEnum,
|
|
RoleAndAppsPermissionEnum,
|
|
SecurityPermissionEnum,
|
|
UserPermissionEnum,
|
|
PermissionPermissionEnum
|
|
};
|
|
use Livewire\Component;
|
|
use Spatie\LaravelData\DataCollection;
|
|
|
|
new class extends Component {
|
|
/** Collapsed = icons-only mode */
|
|
public bool $collapsed = false;
|
|
|
|
public function mount(): void
|
|
{
|
|
$this->collapsed = session('sidebar_collapsed', false);
|
|
}
|
|
|
|
public function toggle(): void
|
|
{
|
|
$this->collapsed = !$this->collapsed;
|
|
session(['sidebar_collapsed' => $this->collapsed]);
|
|
}
|
|
|
|
/**
|
|
* @return DataCollection<int, NavItemData>
|
|
*/
|
|
public function navItems(): DataCollection
|
|
{
|
|
$items = NavItemData::collect([
|
|
new NavItemData(
|
|
label: 'Dashboard',
|
|
icon: 'lucide.house',
|
|
active_pattern: 'dashboard',
|
|
route: 'dashboard',
|
|
),
|
|
|
|
new NavItemData(
|
|
label: 'Apps',
|
|
icon: 'lucide.grid-2x2-plus',
|
|
active_pattern: 'apps.*',
|
|
sub_menu: NavSubItemData::collect([
|
|
new NavSubItemData(
|
|
label: 'All Apps',
|
|
route: 'apps.index',
|
|
active_pattern: 'apps.index',
|
|
permission: AppPermissionEnum::Access
|
|
),
|
|
// new NavSubItemData(
|
|
// label: 'Providers',
|
|
// route: 'apps.connectionProviders.index',
|
|
// active_pattern: 'apps.connectionProviders.*',
|
|
// permission: ConnectionProviderPermissionEnum::Access
|
|
// ),
|
|
// new NavSubItemData(
|
|
// label: 'Microsoft Graph',
|
|
// route: 'apps.microsoft-federation',
|
|
// active_pattern: 'apps.microsoft-federation',
|
|
// permission: ConnectionProviderPermissionEnum::Access
|
|
// )
|
|
], DataCollection::class)
|
|
),
|
|
|
|
new NavItemData(
|
|
label: 'Access Manager',
|
|
icon: 'lucide.shield',
|
|
active_pattern: 'access-manager.*',
|
|
sub_menu: NavSubItemData::collect([
|
|
new NavSubItemData(
|
|
label: 'Roles and Apps',
|
|
route: 'access-manager.roles-and-apps.index',
|
|
active_pattern: 'access-manager.roles-and-apps.*',
|
|
permission: RoleAndAppsPermissionEnum::Access
|
|
),
|
|
new NavSubItemData(
|
|
label: 'Permission Manager',
|
|
route: 'access-manager.permissions.index',
|
|
active_pattern: 'access-manager.permissions.*',
|
|
permission: PermissionPermissionEnum::Access
|
|
),
|
|
new NavSubItemData(
|
|
label: 'Users',
|
|
route: 'access-manager.users.index',
|
|
active_pattern: 'access-manager.users.*',
|
|
permission: UserPermissionEnum::Access
|
|
),
|
|
], DataCollection::class)
|
|
),
|
|
|
|
new NavItemData(
|
|
label: 'Support Tickets',
|
|
icon: 'lucide.life-buoy',
|
|
active_pattern: 'tickets*',
|
|
route: 'tickets.index',
|
|
),
|
|
|
|
new NavItemData(
|
|
label: 'Settings',
|
|
icon: 'lucide.settings',
|
|
active_pattern: 'settings.*', // Kept as fallback, but handled dynamically in view
|
|
sub_menu: NavSubItemData::collect([
|
|
new NavSubItemData(
|
|
label: 'Profile',
|
|
route: 'profile.edit',
|
|
active_pattern: 'profile.edit',
|
|
permission: ProfilePermissionEnum::Access
|
|
),
|
|
new NavSubItemData(
|
|
label: 'Security',
|
|
route: 'security.edit',
|
|
active_pattern: 'security.edit',
|
|
permission: SecurityPermissionEnum::Access
|
|
),
|
|
], DataCollection::class)
|
|
),
|
|
], DataCollection::class);
|
|
|
|
// Filter out items the user shouldn't see
|
|
return $this->filterNavMenu($items);
|
|
}
|
|
|
|
/**
|
|
* Filter out menus that the current user does not have permission.
|
|
* @param DataCollection<NavItemData> $items
|
|
* @returns DataCollection<NavItemData>
|
|
*/
|
|
private function filterNavMenu(DataCollection $items): DataCollection
|
|
{
|
|
$filteredItems = $items->toCollection()->filter(function (NavItemData $item) {
|
|
if ($item->sub_menu) {
|
|
$allowedSubItems = $item->sub_menu->toCollection()->filter(function (NavSubItemData $subItem) {
|
|
$permission = $subItem->permission instanceof UnitEnum
|
|
? $subItem->permission->value
|
|
: $subItem->permission;
|
|
|
|
return auth()->user()->can($permission);
|
|
});
|
|
|
|
// If the user has NO access to any submenus, discard the parent menu item
|
|
if ($allowedSubItems->isEmpty()) {
|
|
return false;
|
|
}
|
|
|
|
// Update the DataCollection with authorized items
|
|
$item->sub_menu = NavSubItemData::collect($allowedSubItems, DataCollection::class);
|
|
}
|
|
|
|
return true;
|
|
});
|
|
|
|
return NavItemData::collect($filteredItems->values()->all(), DataCollection::class);
|
|
}
|
|
};
|
|
?>
|
|
|
|
<aside
|
|
x-data="{ collapsed: @entangle('collapsed') }"
|
|
:class="collapsed ? 'w-12' : 'w-64'"
|
|
class="relative flex flex-col h-screen bg-white border-r border-gray-100
|
|
transition-[width] duration-300 ease-in-out shrink-0 overflow-hidden"
|
|
:aria-expanded="String(!collapsed)"
|
|
>
|
|
|
|
<div class="flex items-center h-20 px-2 border-b border-gray-100 shrink-0">
|
|
<a
|
|
href="{{ route('dashboard') }}"
|
|
wire:navigate
|
|
class="shrink-0 flex items-center justify-center w-8 h-8
|
|
rounded-lg
|
|
focus-visible:outline-none
|
|
focus-visible:ring-2 focus-visible:ring-cyan-400"
|
|
aria-label="Go to dashboard"
|
|
>
|
|
<img src="{{asset('images/logo.png')}}" width="32px" height="32px" alt="">
|
|
</a>
|
|
|
|
<span
|
|
x-cloak
|
|
x-show="!collapsed"
|
|
x-transition:enter="transition-all duration-200 delay-150 ease-out"
|
|
x-transition:enter-start="opacity-0 -translate-x-2"
|
|
x-transition:enter-end="opacity-100 translate-x-0"
|
|
x-transition:leave="transition-opacity duration-100"
|
|
x-transition:leave-start="opacity-100"
|
|
x-transition:leave-end="opacity-0"
|
|
class="ml-3 text-sm font-semibold text-gray-900 whitespace-nowrap tracking-tight"
|
|
>
|
|
<h1 class="text-lg">Company SSO</h1>
|
|
<h2 class="text-sm font-light">Admin Panel</h2>
|
|
</span>
|
|
</div>
|
|
|
|
<nav
|
|
class="flex-1 py-3 px-2 space-y-1 overflow-y-auto overflow-x-hidden"
|
|
aria-label="Main navigation"
|
|
>
|
|
@foreach ($this->navItems() as $item)
|
|
@php
|
|
$isActive = request()->routeIs($item->active_pattern);
|
|
|
|
// Check sub-menu active states without triggering Arrayable casting
|
|
if (! $isActive && $item->sub_menu) {
|
|
foreach ($item->sub_menu as $subItem) {
|
|
if (request()->routeIs($subItem->active_pattern)) {
|
|
$isActive = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
@endphp
|
|
|
|
@if ($item->sub_menu)
|
|
{{-- ITEM WITH SUBMENU --}}
|
|
<div x-data="{ expanded: {{ $isActive ? 'true' : 'false' }} }">
|
|
<button
|
|
@click="expanded = !expanded; if(collapsed) collapsed = false"
|
|
@class([
|
|
'group relative w-full flex items-center justify-between px-2 py-2.5 rounded-lg',
|
|
'text-sm font-medium transition-colors duration-150 cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-cyan-400',
|
|
'bg-gray-100 text-gray-900' => $isActive,
|
|
'text-gray-500 hover:bg-gray-50 hover:text-gray-800' => ! $isActive,
|
|
])
|
|
aria-haspopup="true"
|
|
:aria-expanded="String(expanded)"
|
|
>
|
|
<div class="flex items-center gap-3">
|
|
<x-mary-icon :name="$item->icon" class="w-4 h-4 shrink-0"/>
|
|
<span
|
|
x-cloak
|
|
x-show="!collapsed"
|
|
class="whitespace-nowrap overflow-hidden leading-none"
|
|
>
|
|
{{ $item->label }}
|
|
</span>
|
|
</div>
|
|
|
|
{{-- Chevron Arrow (Hidden when collapsed) --}}
|
|
<span x-cloak x-show="!collapsed" class="shrink-0 transition-transform duration-200"
|
|
:class="expanded ? 'rotate-90' : ''">
|
|
<x-mary-icon name="lucide.chevron-right" class="w-4 h-4"/>
|
|
</span>
|
|
|
|
{{-- Tooltip (Collapsed Mode) --}}
|
|
<span x-cloak x-show="collapsed" role="tooltip"
|
|
class="pointer-events-none absolute left-full ml-3 z-50 whitespace-nowrap rounded-md bg-gray-900 px-2.5 py-1.5 text-xs font-medium text-white shadow-xl opacity-0 group-hover:opacity-100 transition-opacity duration-150 delay-75">
|
|
{{ $item->label }}
|
|
<span
|
|
class="absolute right-full top-1/2 -translate-y-1/2 border-[5px] border-transparent border-r-gray-900"
|
|
aria-hidden="true"></span>
|
|
</span>
|
|
</button>
|
|
|
|
{{-- Submenu Dropdown --}}
|
|
<div x-cloak x-show="expanded && !collapsed" x-collapse>
|
|
<div class="mt-1 space-y-1 pl-9 pr-2">
|
|
@foreach ($item->sub_menu as $subItem)
|
|
@php $isSubActive = request()->routeIs($subItem->active_pattern); @endphp
|
|
@can($subItem->permission)
|
|
<a
|
|
href="{{ route($subItem->route) }}"
|
|
wire:navigate
|
|
@class([
|
|
'block px-3 py-2 rounded-md text-sm font-medium transition-colors',
|
|
'bg-gray-100 text-blue-700' => $isSubActive,
|
|
'text-gray-500 hover:bg-gray-50 hover:text-gray-900' => ! $isSubActive,
|
|
])
|
|
>
|
|
{{ $subItem->label }}
|
|
</a>
|
|
@endcan
|
|
@endforeach
|
|
</div>
|
|
</div>
|
|
</div>
|
|
@else
|
|
{{-- STANDARD ITEM --}}
|
|
<a
|
|
href="{{ route($item->route) }}"
|
|
wire:navigate
|
|
@class([
|
|
'group relative flex items-center gap-3 px-2 py-2.5 rounded-lg',
|
|
'text-sm font-medium transition-colors duration-150',
|
|
'bg-gray-100 text-gray-900' => $isActive,
|
|
'text-gray-500 hover:bg-gray-50 hover:text-gray-800' => ! $isActive,
|
|
])
|
|
aria-current="{{ $isActive ? 'page' : 'false' }}"
|
|
>
|
|
@if ($isActive)
|
|
<span class="absolute left-0 inset-y-2 w-0.5 rounded-r-full bg-blue-600"
|
|
aria-hidden="true"></span>
|
|
@endif
|
|
|
|
<x-mary-icon :name="$item->icon" class="w-4 h-4 shrink-0"/>
|
|
|
|
<span
|
|
x-cloak
|
|
x-show="!collapsed"
|
|
class="whitespace-nowrap overflow-hidden leading-none"
|
|
>
|
|
{{ $item->label }}
|
|
</span>
|
|
|
|
{{-- Tooltip (Collapsed Mode) --}}
|
|
<span x-cloak x-show="collapsed" role="tooltip"
|
|
class="pointer-events-none absolute left-full ml-3 z-50 whitespace-nowrap rounded-md bg-gray-900 px-2.5 py-1.5 text-xs font-medium text-white shadow-xl opacity-0 group-hover:opacity-100 transition-opacity duration-150 delay-75">
|
|
{{ $item->label }}
|
|
<span
|
|
class="absolute right-full top-1/2 -translate-y-1/2 border-[5px] border-transparent border-r-gray-900"
|
|
aria-hidden="true"></span>
|
|
</span>
|
|
</a>
|
|
@endif
|
|
@endforeach
|
|
</nav>
|
|
|
|
{{-- Toggle Footer --}}
|
|
<div class="border-t border-gray-100 px-2 py-3 shrink-0">
|
|
<button
|
|
wire:click="toggle"
|
|
type="button"
|
|
class="group w-full flex items-center gap-3 px-2 py-2.5 rounded-lg
|
|
text-sm font-medium text-gray-400
|
|
hover:bg-gray-50 hover:text-gray-700
|
|
transition-colors duration-150
|
|
focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-teal-400"
|
|
:aria-label="collapsed ? 'Expand sidebar' : 'Collapse sidebar'"
|
|
>
|
|
<span class="shrink-0 flex items-center justify-center w-5 h-5" aria-hidden="true">
|
|
<svg :class="collapsed ? 'rotate-180' : 'rotate-0'"
|
|
class="w-4 h-4 transition-transform duration-300 ease-in-out" fill="none" viewBox="0 0 24 24"
|
|
stroke-width="2" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 19.5L8.25 12l7.5-7.5"/>
|
|
</svg>
|
|
</span>
|
|
<span x-cloak x-show="!collapsed" class="whitespace-nowrap">
|
|
Collapse
|
|
</span>
|
|
</button>
|
|
</div>
|
|
</aside>
|