TenantAtlas/app/Support/Middleware/EnsureFilamentTenantSelected.php
ahmido 8e34b6084f 073-unified-managed-tenant-onboarding-wizard (#90)
Kontext / Ziel
Diese PR liefert den einzigen kanonischen Onboarding-Entry unter /admin/onboarding (workspace-first, tenantless bis zur Aktivierung) und ergänzt einen tenantless OperationRun-Viewer unter /admin/operations/{run} mit membership→404 Semantik.

Was ist enthalten?
Single entry point: /admin/onboarding ist der einzige Einstieg; Legacy Entry Points liefern echte 404 (keine Redirects).
Wizard v1 (Enterprise): idempotentes Identifizieren eines Managed Tenants (per Entra Tenant ID), resumable Session-Flow.
Provider Connection Step: Auswahl oder Erstellung, Secrets werden nie erneut gerendert / nicht in Session-State persistiert.
Verification als OperationRun: async/queued, DB-only Rendering im Wizard (keine Graph-Calls beim Rendern).
Tenantless Run Viewing: /admin/operations/{run} funktioniert ohne ausgewählten Workspace/Tenant, aber bleibt über Workspace-Mitgliedschaft autorisiert (non-member → 404).
RBAC-UX Semantik: non-member → 404, member ohne Capability → UI disabled + tooltip, server-side Action → 403.
Auditability: Aktivierung/Overrides sind auditierbar, stable action IDs, keine Secrets.
Tech / Version-Safety
Filament v5 / Livewire v4.0+ kompatibel.
Laravel 11+: Panel Provider Registrierung in providers.php (unverändert).
Tests / Format
vendor/bin/sail bin pint --dirty
Full suite: vendor/bin/sail artisan test --no-ansi → 984 passed, 5 skipped (exit 0)
Ops / Deployment Notes
Keine zusätzlichen Services vorausgesetzt.
Falls Assets registriert wurden: Deployment weiterhin mit php artisan filament:assets (wie üblich im Projekt).

Co-authored-by: Ahmed Darrazi <ahmeddarrazi@adsmac.fritz.box>
Co-authored-by: Ahmed Darrazi <ahmeddarrazi@MacBookPro.fritz.box>
Reviewed-on: #90
2026-02-04 23:30:55 +00:00

207 lines
5.7 KiB
PHP

<?php
namespace App\Support\Middleware;
use App\Filament\Pages\ChooseWorkspace;
use App\Models\Tenant;
use App\Models\User;
use App\Models\Workspace;
use App\Services\Auth\CapabilityResolver;
use App\Support\Workspaces\WorkspaceContext;
use Closure;
use Filament\Facades\Filament;
use Filament\Models\Contracts\HasTenants;
use Filament\Navigation\NavigationBuilder;
use Filament\Navigation\NavigationItem;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class EnsureFilamentTenantSelected
{
/**
* @param Closure(Request): Response $next
*/
public function handle(Request $request, Closure $next): Response
{
$panel = Filament::getCurrentOrDefaultPanel();
$path = '/'.ltrim($request->path(), '/');
if ($path === '/livewire/update') {
$refererPath = parse_url((string) $request->headers->get('referer', ''), PHP_URL_PATH) ?? '';
$refererPath = '/'.ltrim((string) $refererPath, '/');
if (preg_match('#^/admin/operations/[^/]+$#', $refererPath) === 1) {
$this->configureNavigationForRequest($panel);
return $next($request);
}
}
if (preg_match('#^/admin/operations/[^/]+$#', $path) === 1) {
$this->configureNavigationForRequest($panel);
return $next($request);
}
if ($request->route()?->hasParameter('tenant')) {
$user = $request->user();
if ($user === null) {
return $next($request);
}
if (! $user instanceof HasTenants) {
abort(404);
}
if (! $panel->hasTenancy()) {
return $next($request);
}
$tenantParameter = $request->route()->parameter('tenant');
$tenant = $panel->getTenant($tenantParameter);
if (! $tenant instanceof Tenant) {
abort(404);
}
$workspaceContext = app(WorkspaceContext::class);
$workspaceId = $workspaceContext->currentWorkspaceId($request);
if ($workspaceId === null) {
abort(404);
}
if ((int) $tenant->workspace_id !== (int) $workspaceId) {
abort(404);
}
$workspace = Workspace::query()->whereKey($workspaceId)->first();
if (! $workspace instanceof Workspace) {
abort(404);
}
if (! $user instanceof User || ! $workspaceContext->isMember($user, $workspace)) {
abort(404);
}
if (! $user->canAccessTenant($tenant)) {
abort(404);
}
Filament::setTenant($tenant, true);
$this->configureNavigationForRequest($panel);
return $next($request);
}
if (
str_starts_with($path, '/admin/w/')
|| str_starts_with($path, '/admin/workspaces')
|| in_array($path, ['/admin/choose-workspace', '/admin/choose-tenant', '/admin/no-access'], true)
) {
$this->configureNavigationForRequest($panel);
return $next($request);
}
if (filled(Filament::getTenant())) {
$this->configureNavigationForRequest($panel);
return $next($request);
}
$user = $request->user();
if (! $user instanceof User) {
$this->configureNavigationForRequest($panel);
return $next($request);
}
$tenant = null;
$workspaceId = app(WorkspaceContext::class)->currentWorkspaceId($request);
if ($workspaceId !== null) {
$tenant = $user->tenants()
->where('workspace_id', $workspaceId)
->where('status', 'active')
->first();
if (! $tenant) {
$tenant = $user->tenants()
->where('workspace_id', $workspaceId)
->first();
}
if (! $tenant) {
$tenant = $user->tenants()
->withTrashed()
->where('workspace_id', $workspaceId)
->first();
}
}
if (! $tenant) {
try {
$tenant = Tenant::current();
} catch (\RuntimeException) {
$tenant = null;
}
if ($tenant instanceof Tenant && ! app(CapabilityResolver::class)->isMember($user, $tenant)) {
$tenant = null;
}
}
if (! $tenant) {
$tenant = $user->tenants()
->where('status', 'active')
->first();
}
if (! $tenant) {
$tenant = $user->tenants()->first();
}
if (! $tenant) {
$tenant = $user->tenants()->withTrashed()->first();
}
if ($tenant) {
Filament::setTenant($tenant, true);
}
$this->configureNavigationForRequest($panel);
return $next($request);
}
private function configureNavigationForRequest(\Filament\Panel $panel): void
{
if (! $panel->hasTenancy()) {
return;
}
if (filled(Filament::getTenant())) {
$panel->navigation(true);
return;
}
$panel->navigation(function (): NavigationBuilder {
return app(NavigationBuilder::class)
->item(
NavigationItem::make('Workspaces')
->url(fn (): string => ChooseWorkspace::getUrl())
->icon('heroicon-o-squares-2x2')
->group('Settings')
->sort(10),
);
});
}
}