Implements workspace-first enforcement and UX: - Workspace selected before tenant flows; /admin routes into choose-workspace/choose-tenant - Tenant lists and default tenant selection are scoped to current workspace - Workspaces UI is tenantless at /admin/workspaces Security hardening: - Workspaces can never have 0 owners (blocks last-owner removal/demotion) - Blocked attempts are audited with action_id=workspace_membership.last_owner_blocked + required metadata - Optional break-glass recovery page to re-assign workspace owner (audited) Tests: - Added/updated Pest feature tests covering redirects, scoping, tenantless workspaces, last-owner guards, and break-glass recovery. Notes: - Filament v5 strict Page property signatures respected in RepairWorkspaceOwners. Co-authored-by: Ahmed Darrazi <ahmeddarrazi@MacBookPro.fritz.box> Reviewed-on: #86
71 lines
3.4 KiB
PHP
71 lines
3.4 KiB
PHP
<x-filament-panels::page>
|
|
<x-filament::section>
|
|
<div class="flex flex-col gap-4">
|
|
<div class="text-sm text-gray-600 dark:text-gray-300">
|
|
Select a workspace to continue.
|
|
</div>
|
|
|
|
@php
|
|
$workspaces = $this->getWorkspaces();
|
|
|
|
$user = auth()->user();
|
|
$recommendedWorkspaceId = $user instanceof \App\Models\User ? (int) ($user->last_workspace_id ?? 0) : 0;
|
|
|
|
if ($recommendedWorkspaceId > 0) {
|
|
[$recommended, $other] = $workspaces->partition(fn ($workspace) => (int) $workspace->id === $recommendedWorkspaceId);
|
|
$workspaces = $recommended->concat($other)->values();
|
|
}
|
|
@endphp
|
|
|
|
@if ($workspaces->isEmpty())
|
|
<div class="rounded-md border border-gray-200 bg-gray-50 p-4 text-sm text-gray-700 dark:border-gray-800 dark:bg-gray-900 dark:text-gray-200">
|
|
No active workspaces are available for your account.
|
|
You can create one using the button above.
|
|
</div>
|
|
@else
|
|
<div class="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
|
@foreach ($workspaces as $workspace)
|
|
@php
|
|
$isRecommended = $recommendedWorkspaceId > 0 && (int) $workspace->id === $recommendedWorkspaceId;
|
|
@endphp
|
|
|
|
<div
|
|
wire:key="workspace-{{ $workspace->id }}"
|
|
x-data
|
|
@click="if ($event.target.closest('button,a,input,select,textarea')) return; $refs.form.submit();"
|
|
class="cursor-pointer rounded-lg border p-4 dark:border-gray-800 {{ $isRecommended ? 'border-amber-300 bg-amber-50 dark:border-amber-700 dark:bg-amber-950/30' : 'border-gray-200' }}"
|
|
>
|
|
<form x-ref="form" method="POST" action="{{ route('admin.switch-workspace') }}" class="flex flex-col gap-3">
|
|
@csrf
|
|
<input type="hidden" name="workspace_id" value="{{ (int) $workspace->id }}" />
|
|
|
|
<div class="flex flex-col gap-2">
|
|
<div class="font-medium text-gray-900 dark:text-gray-100">
|
|
{{ $workspace->name }}
|
|
</div>
|
|
|
|
@if ($isRecommended)
|
|
<div>
|
|
<x-filament::badge color="warning" size="sm">
|
|
Last used
|
|
</x-filament::badge>
|
|
</div>
|
|
@endif
|
|
</div>
|
|
|
|
<x-filament::button
|
|
type="submit"
|
|
color="primary"
|
|
class="w-full"
|
|
>
|
|
Continue
|
|
</x-filament::button>
|
|
</form>
|
|
</div>
|
|
@endforeach
|
|
</div>
|
|
@endif
|
|
</div>
|
|
</x-filament::section>
|
|
</x-filament-panels::page>
|