route()?->getName(); if (is_string($routeName) && str_contains($routeName, '.auth.')) { return $next($request); } $path = '/'.ltrim($request->path(), '/'); // --- Step 1: workspace-optional bypass --- if ($this->isWorkspaceOptionalPath($request, $path)) { return $next($request); } // Tenant-scoped routes are handled separately. if (str_starts_with($path, '/admin/t/')) { return $next($request); } $user = $request->user(); if (! $user instanceof User) { return $next($request); } /** @var WorkspaceContext $context */ $context = app(WorkspaceContext::class); // --- Step 2: forced chooser via ?choose=1 --- if ($request->query('choose') === '1') { return $this->redirectToChooser(); } // --- Step 3: validate active session --- $currentId = $context->currentWorkspaceId($request); if ($currentId !== null) { $workspace = Workspace::query()->whereKey($currentId)->first(); if ( $workspace instanceof Workspace && empty($workspace->archived_at) && $context->isMember($user, $workspace) ) { return $next($request); } // Stale session — clear and warn. $this->clearStaleSession($context, $user, $request, $workspace); return $this->redirectToChooser(); } // --- Step 4: load selectable workspace memberships --- $selectableMemberships = WorkspaceMembership::query() ->where('user_id', $user->getKey()) ->join('workspaces', 'workspace_memberships.workspace_id', '=', 'workspaces.id') ->whereNull('workspaces.archived_at') ->select('workspace_memberships.*') ->get(); // --- Step 5: single membership auto-resume --- if ($selectableMemberships->count() === 1) { /** @var WorkspaceMembership $membership */ $membership = $selectableMemberships->first(); $workspace = Workspace::query()->whereKey($membership->workspace_id)->first(); if ($workspace instanceof Workspace) { $context->setCurrentWorkspace($workspace, $user, $request); $this->emitAuditEvent( workspace: $workspace, user: $user, actionId: AuditActionId::WorkspaceAutoSelected, method: 'auto', reason: 'single_membership', ); return $this->redirectViaTenantBranching($workspace, $user); } } // --- Step 6: last_workspace_id auto-resume --- if ($user->last_workspace_id !== null) { $lastWorkspace = Workspace::query()->whereKey($user->last_workspace_id)->first(); if ( $lastWorkspace instanceof Workspace && empty($lastWorkspace->archived_at) && $context->isMember($user, $lastWorkspace) ) { $context->setCurrentWorkspace($lastWorkspace, $user, $request); $this->emitAuditEvent( workspace: $lastWorkspace, user: $user, actionId: AuditActionId::WorkspaceAutoSelected, method: 'auto', reason: 'last_used', ); return $this->redirectViaTenantBranching($lastWorkspace, $user); } // Stale last_workspace_id — clear and warn. $workspaceName = $lastWorkspace?->name; $user->forceFill(['last_workspace_id' => null])->save(); if ($workspaceName !== null) { Notification::make() ->title("Your access to {$workspaceName} was removed.") ->danger() ->send(); } } // --- Step 7: fallback to chooser --- if ($selectableMemberships->isNotEmpty()) { WorkspaceIntendedUrl::storeFromRequest($request); } $canCreate = $user->can('create', Workspace::class); $target = ($selectableMemberships->isNotEmpty() || $canCreate) ? '/admin/choose-workspace' : '/admin/no-access'; return new \Illuminate\Http\Response('', 302, ['Location' => $target]); } private function isWorkspaceOptionalPath(Request $request, string $path): bool { if (str_starts_with($path, '/admin/workspaces')) { return true; } if (in_array($path, ['/admin/choose-workspace', '/admin/no-access', '/admin/onboarding', '/admin/settings/workspace'], true)) { return true; } 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) { return true; } } return preg_match('#^/admin/operations/[^/]+$#', $path) === 1; } private function redirectToChooser(): Response { return new \Illuminate\Http\Response('', 302, ['Location' => '/admin/choose-workspace']); } private function redirectViaTenantBranching(Workspace $workspace, User $user): Response { /** @var WorkspaceRedirectResolver $resolver */ $resolver = app(WorkspaceRedirectResolver::class); $url = $resolver->resolve($workspace, $user); return new \Illuminate\Http\Response('', 302, ['Location' => $url]); } private function clearStaleSession(WorkspaceContext $context, User $user, Request $request, ?Workspace $workspace): void { $workspaceName = $workspace?->name; $session = $request->hasSession() ? $request->session() : session(); $session->forget(WorkspaceContext::SESSION_KEY); if ($user->last_workspace_id !== null && $context->currentWorkspaceId($request) === null) { $user->forceFill(['last_workspace_id' => null])->save(); } if ($workspaceName !== null) { Notification::make() ->title("Your access to {$workspaceName} was removed.") ->danger() ->send(); } } private function emitAuditEvent( Workspace $workspace, User $user, AuditActionId $actionId, string $method, string $reason, ?int $prevWorkspaceId = null, ): void { /** @var WorkspaceAuditLogger $logger */ $logger = app(WorkspaceAuditLogger::class); $logger->log( workspace: $workspace, action: $actionId->value, context: [ 'metadata' => [ 'method' => $method, 'reason' => $reason, 'prev_workspace_id' => $prevWorkspaceId, ], ], actor: $user, resourceType: 'workspace', resourceId: (string) $workspace->getKey(), ); } }