From 3452faec87b4e8015814892f34a58ede255e4b6f Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Tue, 29 Sep 2026 10:31:32 +0200 Subject: [PATCH] fix(livewire): bind team broadcast listeners to the mounted team Livewire rebuilds listener names from the session team on every event call. After a team switch in another tab, open pages still listen on the old team channel, so events failed with EventHandlerDoesNotExist (500), or ran handlers that load resources scoped to the new team (404). Store the team at mount and build team channel listeners from it. Events for a team that is no longer the session team are ignored. Co-Authored-By: Claude Opus 5.5 --- app/Livewire/LayoutPopups.php | 11 +-- .../Project/Application/Deployment/Index.php | 11 +-- app/Livewire/Project/Application/Domains.php | 6 +- app/Livewire/Project/Application/Heading.php | 10 +-- .../Project/Application/ServerStatusBadge.php | 22 ++---- app/Livewire/Project/Application/Status.php | 13 ++-- app/Livewire/Project/Database/BackupEdit.php | 12 ++-- .../Project/Database/BackupExecutions.php | 10 +-- .../Project/Database/Clickhouse/General.php | 18 ++--- .../Project/Database/Dragonfly/General.php | 18 ++--- app/Livewire/Project/Database/Heading.php | 10 +-- app/Livewire/Project/Database/Import.php | 12 ++-- .../Project/Database/Keydb/General.php | 18 ++--- app/Livewire/Project/Database/Status.php | 13 ++-- .../Project/Service/BackupExecutions.php | 8 ++- .../Project/Service/Configuration.php | 8 ++- app/Livewire/Project/Service/Domains.php | 6 +- app/Livewire/Project/Service/Heading.php | 11 +-- app/Livewire/Project/Service/ResourceCard.php | 19 ++--- app/Livewire/Project/Service/Status.php | 13 ++-- app/Livewire/Project/Service/Storage.php | 8 ++- .../Project/Service/VolumeBackup/Index.php | 10 +-- .../Project/Shared/ConfigurationChecker.php | 9 ++- app/Livewire/Project/Shared/Destination.php | 10 +-- app/Livewire/Project/Shared/Logs.php | 11 +-- .../Shared/ScheduledTask/Executions.php | 11 +-- app/Livewire/Server/CloudflareTunnel.php | 10 +-- .../Server/DockerCleanupExecutions.php | 11 +-- app/Livewire/Server/Navbar.php | 12 ++-- app/Livewire/Server/Proxy.php | 8 ++- .../Server/Proxy/DynamicConfigurations.php | 8 ++- app/Livewire/Server/Resources.php | 10 +-- app/Livewire/Server/Security/Patches.php | 10 +-- app/Traits/HasDatabaseStatusInfo.php | 12 ++-- app/Traits/ListensToTeamChannel.php | 53 ++++++++++++++ .../Livewire/TeamChannelListenersTest.php | 71 +++++++++++++++++++ .../ServiceDatabaseVerticalNavigationTest.php | 9 +-- 37 files changed, 319 insertions(+), 203 deletions(-) create mode 100644 app/Traits/ListensToTeamChannel.php create mode 100644 tests/Feature/Livewire/TeamChannelListenersTest.php diff --git a/app/Livewire/LayoutPopups.php b/app/Livewire/LayoutPopups.php index f2ba788938..7d4c5b3414 100644 --- a/app/Livewire/LayoutPopups.php +++ b/app/Livewire/LayoutPopups.php @@ -2,17 +2,18 @@ namespace App\Livewire; +use App\Traits\ListensToTeamChannel; use Livewire\Component; class LayoutPopups extends Component { + use ListensToTeamChannel; + public function getListeners() { - $teamId = auth()->user()->currentTeam()->id; - - return [ - "echo-private:team.{$teamId},TestEvent" => 'testEvent', - ]; + return $this->teamChannelListeners([ + 'TestEvent' => 'testEvent', + ]); } public function testEvent() diff --git a/app/Livewire/Project/Application/Deployment/Index.php b/app/Livewire/Project/Application/Deployment/Index.php index e94f37544f..1c8bc27ca3 100644 --- a/app/Livewire/Project/Application/Deployment/Index.php +++ b/app/Livewire/Project/Application/Deployment/Index.php @@ -5,11 +5,14 @@ namespace App\Livewire\Project\Application\Deployment; use App\Enums\ApplicationDeploymentStatus; use App\Models\Application; use App\Models\ApplicationDeploymentQueue; +use App\Traits\ListensToTeamChannel; use Illuminate\Support\Collection; use Livewire\Component; class Index extends Component { + use ListensToTeamChannel; + public Application $application; public ?Collection $deployments; @@ -60,11 +63,9 @@ class Index extends Component public function getListeners() { - $teamId = auth()->user()->currentTeam()->id; - - return [ - "echo-private:team.{$teamId},ServiceChecked" => '$refresh', - ]; + return $this->teamChannelListeners([ + 'ServiceChecked' => '$refresh', + ]); } public function mount() diff --git a/app/Livewire/Project/Application/Domains.php b/app/Livewire/Project/Application/Domains.php index 7e9790f5d0..6803e0fd97 100644 --- a/app/Livewire/Project/Application/Domains.php +++ b/app/Livewire/Project/Application/Domains.php @@ -12,6 +12,7 @@ use App\Models\Server; use App\Support\DomainPortOverrides; use App\Support\DomainUrlParts; use App\Support\ValidationPatterns; +use App\Traits\ListensToTeamChannel; use Illuminate\Database\Eloquent\Model; use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Illuminate\Support\Collection; @@ -23,6 +24,7 @@ class Domains extends Component use AuthorizesRequests; use InteractsWithCloudflareDomainConnect; use InteractsWithDnsProviders; + use ListensToTeamChannel; protected bool $notifyRedirectUpdate = true; @@ -132,9 +134,7 @@ class Domains extends Component public function getListeners(): array { - return array_merge($this->listeners, [ - 'echo-private:team.'.currentTeam()->id.',DnsRecordConfigurationFinished' => 'dnsRecordConfigurationFinished', - ]); + return array_merge($this->listeners, $this->teamChannelListeners(['DnsRecordConfigurationFinished' => 'dnsRecordConfigurationFinished'])); } protected function rules(): array diff --git a/app/Livewire/Project/Application/Heading.php b/app/Livewire/Project/Application/Heading.php index e52d7c32ec..7c3aac02cd 100644 --- a/app/Livewire/Project/Application/Heading.php +++ b/app/Livewire/Project/Application/Heading.php @@ -6,12 +6,14 @@ use App\Actions\Application\StopApplication; use App\Actions\Docker\GetContainersStatus; use App\Models\Application; use App\Models\ApplicationDeploymentQueue; +use App\Traits\ListensToTeamChannel; use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Livewire\Component; class Heading extends Component { use AuthorizesRequests; + use ListensToTeamChannel; public Application $application; @@ -29,13 +31,13 @@ class Heading extends Component public function getListeners() { - $teamId = auth()->user()->currentTeam()->id; - return [ - "echo-private:team.{$teamId},ServiceStatusChanged" => 'checkStatus', - "echo-private:team.{$teamId},ServiceChecked" => '$refresh', 'compose_loaded' => '$refresh', 'update_links' => '$refresh', + ...$this->teamChannelListeners([ + 'ServiceStatusChanged' => 'checkStatus', + 'ServiceChecked' => '$refresh', + ]), ]; } diff --git a/app/Livewire/Project/Application/ServerStatusBadge.php b/app/Livewire/Project/Application/ServerStatusBadge.php index 459271e288..a86447d568 100644 --- a/app/Livewire/Project/Application/ServerStatusBadge.php +++ b/app/Livewire/Project/Application/ServerStatusBadge.php @@ -3,30 +3,22 @@ namespace App\Livewire\Project\Application; use App\Models\Application; +use App\Traits\ListensToTeamChannel; use Illuminate\Contracts\View\View; -use Illuminate\Support\Facades\Auth; use Livewire\Component; class ServerStatusBadge extends Component { + use ListensToTeamChannel; + public Application $application; public function getListeners(): array { - $user = Auth::user(); - if (! $user) { - return []; - } - - $team = $user->currentTeam(); - if (! $team) { - return []; - } - - return [ - "echo-private:team.{$team->id},ServiceStatusChanged" => 'refreshStatus', - "echo-private:team.{$team->id},ServiceChecked" => 'refreshStatus', - ]; + return $this->teamChannelListeners([ + 'ServiceStatusChanged' => 'refreshStatus', + 'ServiceChecked' => 'refreshStatus', + ]); } public function refreshStatus(): void diff --git a/app/Livewire/Project/Application/Status.php b/app/Livewire/Project/Application/Status.php index a1393be86f..00f6e65a09 100644 --- a/app/Livewire/Project/Application/Status.php +++ b/app/Livewire/Project/Application/Status.php @@ -3,21 +3,22 @@ namespace App\Livewire\Project\Application; use App\Models\Application; +use App\Traits\ListensToTeamChannel; use Illuminate\Contracts\View\View; use Livewire\Component; class Status extends Component { + use ListensToTeamChannel; + public Application $application; public function getListeners(): array { - $teamId = auth()->user()->currentTeam()->id; - - return [ - "echo-private:team.{$teamId},ServiceStatusChanged" => 'refreshStatus', - "echo-private:team.{$teamId},ServiceChecked" => 'refreshStatus', - ]; + return $this->teamChannelListeners([ + 'ServiceStatusChanged' => 'refreshStatus', + 'ServiceChecked' => 'refreshStatus', + ]); } public function refreshStatus(): void diff --git a/app/Livewire/Project/Database/BackupEdit.php b/app/Livewire/Project/Database/BackupEdit.php index 04e86e261e..279a475f4e 100644 --- a/app/Livewire/Project/Database/BackupEdit.php +++ b/app/Livewire/Project/Database/BackupEdit.php @@ -6,6 +6,7 @@ use App\Jobs\DatabaseBackupJob; use App\Models\S3Storage; use App\Models\ScheduledDatabaseBackup; use App\Models\ServiceDatabase; +use App\Traits\ListensToTeamChannel; use Exception; use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Illuminate\Support\Collection; @@ -17,6 +18,7 @@ use Livewire\Component; class BackupEdit extends Component { use AuthorizesRequests; + use ListensToTeamChannel; public ScheduledDatabaseBackup $backup; @@ -100,12 +102,10 @@ class BackupEdit extends Component $listeners["echo-private:user.{$user->id},DatabaseStatusChanged"] = 'refreshStatus'; - $team = $user->currentTeam(); - if ($team) { - $listeners["echo-private:team.{$team->id},ServiceChecked"] = 'refreshStatus'; - } - - return $listeners; + return [ + ...$listeners, + ...$this->teamChannelListeners(['ServiceChecked' => 'refreshStatus']), + ]; } public function mount() diff --git a/app/Livewire/Project/Database/BackupExecutions.php b/app/Livewire/Project/Database/BackupExecutions.php index 2786a45c3d..6f17e2fcf5 100644 --- a/app/Livewire/Project/Database/BackupExecutions.php +++ b/app/Livewire/Project/Database/BackupExecutions.php @@ -4,6 +4,7 @@ namespace App\Livewire\Project\Database; use App\Models\ScheduledDatabaseBackup; use App\Models\ServiceDatabase; +use App\Traits\ListensToTeamChannel; use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Illuminate\Support\Collection; use Livewire\Component; @@ -11,6 +12,7 @@ use Livewire\Component; class BackupExecutions extends Component { use AuthorizesRequests; + use ListensToTeamChannel; public ?ScheduledDatabaseBackup $backup = null; @@ -38,11 +40,9 @@ class BackupExecutions extends Component public function getListeners(): array { - $teamId = currentTeam()->id; - - return [ - "echo-private:team.{$teamId},BackupCreated" => 'refreshBackupExecutions', - ]; + return $this->teamChannelListeners([ + 'BackupCreated' => 'refreshBackupExecutions', + ]); } public function cleanupFailed() diff --git a/app/Livewire/Project/Database/Clickhouse/General.php b/app/Livewire/Project/Database/Clickhouse/General.php index 2f87edcb2b..d61a19f2c7 100644 --- a/app/Livewire/Project/Database/Clickhouse/General.php +++ b/app/Livewire/Project/Database/Clickhouse/General.php @@ -7,14 +7,15 @@ use App\Actions\Database\StopDatabaseProxy; use App\Models\Server; use App\Models\StandaloneClickhouse; use App\Support\ValidationPatterns; +use App\Traits\ListensToTeamChannel; use Exception; use Illuminate\Foundation\Auth\Access\AuthorizesRequests; -use Illuminate\Support\Facades\Auth; use Livewire\Component; class General extends Component { use AuthorizesRequests; + use ListensToTeamChannel; public ?Server $server = null; @@ -46,18 +47,9 @@ class General extends Component public function getListeners(): array { - $user = Auth::user(); - if (! $user) { - return []; - } - $team = $user->currentTeam(); - if (! $team) { - return []; - } - - return [ - "echo-private:team.{$team->id},DatabaseProxyStopped" => 'databaseProxyStopped', - ]; + return $this->teamChannelListeners([ + 'DatabaseProxyStopped' => 'databaseProxyStopped', + ]); } public function mount() diff --git a/app/Livewire/Project/Database/Dragonfly/General.php b/app/Livewire/Project/Database/Dragonfly/General.php index 556925727d..1505aad781 100644 --- a/app/Livewire/Project/Database/Dragonfly/General.php +++ b/app/Livewire/Project/Database/Dragonfly/General.php @@ -7,14 +7,15 @@ use App\Actions\Database\StopDatabaseProxy; use App\Models\Server; use App\Models\StandaloneDragonfly; use App\Support\ValidationPatterns; +use App\Traits\ListensToTeamChannel; use Exception; use Illuminate\Foundation\Auth\Access\AuthorizesRequests; -use Illuminate\Support\Facades\Auth; use Livewire\Component; class General extends Component { use AuthorizesRequests; + use ListensToTeamChannel; public ?Server $server = null; @@ -44,18 +45,9 @@ class General extends Component public function getListeners(): array { - $user = Auth::user(); - if (! $user) { - return []; - } - $team = $user->currentTeam(); - if (! $team) { - return []; - } - - return [ - "echo-private:team.{$team->id},DatabaseProxyStopped" => 'databaseProxyStopped', - ]; + return $this->teamChannelListeners([ + 'DatabaseProxyStopped' => 'databaseProxyStopped', + ]); } public function mount() diff --git a/app/Livewire/Project/Database/Heading.php b/app/Livewire/Project/Database/Heading.php index 030da438ac..a7e8cfedd8 100644 --- a/app/Livewire/Project/Database/Heading.php +++ b/app/Livewire/Project/Database/Heading.php @@ -8,12 +8,14 @@ use App\Actions\Database\StopDatabase; use App\Actions\Docker\GetContainersStatus; use App\Events\ServiceStatusChanged; use App\Support\ResourceStartActivity; +use App\Traits\ListensToTeamChannel; use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Livewire\Component; class Heading extends Component { use AuthorizesRequests; + use ListensToTeamChannel; public $database; @@ -27,14 +29,14 @@ class Heading extends Component public function getListeners() { - $teamId = auth()->user()->currentTeam()->id; - return [ - "echo-private:team.{$teamId},ServiceStatusChanged" => 'checkStatus', - "echo-private:team.{$teamId},ServiceChecked" => 'activityFinished', 'refresh' => '$refresh', 'compose_loaded' => '$refresh', 'update_links' => '$refresh', + ...$this->teamChannelListeners([ + 'ServiceStatusChanged' => 'checkStatus', + 'ServiceChecked' => 'activityFinished', + ]), ]; } diff --git a/app/Livewire/Project/Database/Import.php b/app/Livewire/Project/Database/Import.php index ea04658cfd..6d6bf94867 100644 --- a/app/Livewire/Project/Database/Import.php +++ b/app/Livewire/Project/Database/Import.php @@ -7,6 +7,7 @@ use App\Models\StandaloneClickhouse; use App\Models\StandaloneDragonfly; use App\Models\StandaloneKeydb; use App\Models\StandaloneRedis; +use App\Traits\ListensToTeamChannel; use Illuminate\Contracts\View\View; use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Illuminate\Support\Facades\Auth; @@ -16,6 +17,7 @@ use Livewire\Component; class Import extends Component { use AuthorizesRequests; + use ListensToTeamChannel; #[Locked] public ?int $resourceId = null; @@ -40,12 +42,10 @@ class Import extends Component $listeners["echo-private:user.{$user->id},DatabaseStatusChanged"] = 'refreshStatus'; - $team = $user->currentTeam(); - if ($team) { - $listeners["echo-private:team.{$team->id},ServiceChecked"] = 'refreshStatus'; - } - - return $listeners; + return [ + ...$listeners, + ...$this->teamChannelListeners(['ServiceChecked' => 'refreshStatus']), + ]; } public function mount(): void diff --git a/app/Livewire/Project/Database/Keydb/General.php b/app/Livewire/Project/Database/Keydb/General.php index c32a814a1a..2ae64f3121 100644 --- a/app/Livewire/Project/Database/Keydb/General.php +++ b/app/Livewire/Project/Database/Keydb/General.php @@ -7,14 +7,15 @@ use App\Actions\Database\StopDatabaseProxy; use App\Models\Server; use App\Models\StandaloneKeydb; use App\Support\ValidationPatterns; +use App\Traits\ListensToTeamChannel; use Exception; use Illuminate\Foundation\Auth\Access\AuthorizesRequests; -use Illuminate\Support\Facades\Auth; use Livewire\Component; class General extends Component { use AuthorizesRequests; + use ListensToTeamChannel; public ?Server $server = null; @@ -46,18 +47,9 @@ class General extends Component public function getListeners(): array { - $user = Auth::user(); - if (! $user) { - return []; - } - $team = $user->currentTeam(); - if (! $team) { - return []; - } - - return [ - "echo-private:team.{$team->id},DatabaseProxyStopped" => 'databaseProxyStopped', - ]; + return $this->teamChannelListeners([ + 'DatabaseProxyStopped' => 'databaseProxyStopped', + ]); } public function mount() diff --git a/app/Livewire/Project/Database/Status.php b/app/Livewire/Project/Database/Status.php index eb40cf817e..8ac20ff16f 100644 --- a/app/Livewire/Project/Database/Status.php +++ b/app/Livewire/Project/Database/Status.php @@ -2,21 +2,22 @@ namespace App\Livewire\Project\Database; +use App\Traits\ListensToTeamChannel; use Illuminate\Contracts\View\View; use Livewire\Component; class Status extends Component { + use ListensToTeamChannel; + public $database; public function getListeners(): array { - $teamId = auth()->user()->currentTeam()->id; - - return [ - "echo-private:team.{$teamId},ServiceStatusChanged" => 'refreshStatus', - "echo-private:team.{$teamId},ServiceChecked" => 'refreshStatus', - ]; + return $this->teamChannelListeners([ + 'ServiceStatusChanged' => 'refreshStatus', + 'ServiceChecked' => 'refreshStatus', + ]); } public function refreshStatus(): void diff --git a/app/Livewire/Project/Service/BackupExecutions.php b/app/Livewire/Project/Service/BackupExecutions.php index 6d7fb97f68..2f21e60383 100644 --- a/app/Livewire/Project/Service/BackupExecutions.php +++ b/app/Livewire/Project/Service/BackupExecutions.php @@ -8,6 +8,7 @@ use App\Models\ScheduledVolumeBackup; use App\Models\ScheduledVolumeBackupExecution; use App\Models\Service; use App\Models\ServiceDatabase; +use App\Traits\ListensToTeamChannel; use Illuminate\Contracts\View\View; use Illuminate\Database\Query\Builder; use Illuminate\Foundation\Auth\Access\AuthorizesRequests; @@ -18,6 +19,7 @@ use Livewire\WithPagination; class BackupExecutions extends Component { use AuthorizesRequests; + use ListensToTeamChannel; use WithPagination; public Service $service; @@ -30,11 +32,11 @@ class BackupExecutions extends Component public function getListeners(): array { - $teamId = currentTeam()->id; - return [ 'modalClosed' => 'closeExecutionModal', - "echo-private:team.{$teamId},BackupCreated" => '$refresh', + ...$this->teamChannelListeners([ + 'BackupCreated' => '$refresh', + ]), ]; } diff --git a/app/Livewire/Project/Service/Configuration.php b/app/Livewire/Project/Service/Configuration.php index f3ec3ba4df..51f3b69999 100644 --- a/app/Livewire/Project/Service/Configuration.php +++ b/app/Livewire/Project/Service/Configuration.php @@ -3,12 +3,14 @@ namespace App\Livewire\Project\Service; use App\Models\Service; +use App\Traits\ListensToTeamChannel; use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Livewire\Component; class Configuration extends Component { use AuthorizesRequests; + use ListensToTeamChannel; public $currentRoute; @@ -28,13 +30,13 @@ class Configuration extends Component public function getListeners(): array { - $teamId = auth()->user()->currentTeam()->id; - return [ 'refreshServices' => 'refreshServices', 'refresh' => 'refreshServices', 'configurationChanged' => 'refreshServices', - "echo-private:team.{$teamId},ApplicationConfigurationChanged" => 'refreshServices', + ...$this->teamChannelListeners([ + 'ApplicationConfigurationChanged' => 'refreshServices', + ]), ]; } diff --git a/app/Livewire/Project/Service/Domains.php b/app/Livewire/Project/Service/Domains.php index 1a6dc91c7a..d51e5c6642 100644 --- a/app/Livewire/Project/Service/Domains.php +++ b/app/Livewire/Project/Service/Domains.php @@ -13,6 +13,7 @@ use App\Models\ServiceApplication; use App\Support\DomainPortOverrides; use App\Support\DomainUrlParts; use App\Support\ValidationPatterns; +use App\Traits\ListensToTeamChannel; use Illuminate\Database\Eloquent\Model; use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Illuminate\Support\Collection; @@ -24,6 +25,7 @@ class Domains extends Component use AuthorizesRequests; use InteractsWithCloudflareDomainConnect; use InteractsWithDnsProviders; + use ListensToTeamChannel; protected bool $notifyRedirectUpdate = true; @@ -123,9 +125,7 @@ class Domains extends Component public function getListeners(): array { - return array_merge($this->listeners, [ - 'echo-private:team.'.currentTeam()->id.',DnsRecordConfigurationFinished' => 'dnsRecordConfigurationFinished', - ]); + return array_merge($this->listeners, $this->teamChannelListeners(['DnsRecordConfigurationFinished' => 'dnsRecordConfigurationFinished'])); } protected function rules(): array diff --git a/app/Livewire/Project/Service/Heading.php b/app/Livewire/Project/Service/Heading.php index fc6720b218..e8d5375038 100644 --- a/app/Livewire/Project/Service/Heading.php +++ b/app/Livewire/Project/Service/Heading.php @@ -11,14 +11,15 @@ use App\Models\Service; use App\Models\ServiceApplication; use App\Models\ServiceDatabase; use App\Support\ResourceStartActivity; +use App\Traits\ListensToTeamChannel; use Illuminate\Foundation\Auth\Access\AuthorizesRequests; -use Illuminate\Support\Facades\Auth; use Livewire\Component; use Spatie\Activitylog\Models\Activity; class Heading extends Component { use AuthorizesRequests; + use ListensToTeamChannel; public Service $service; @@ -48,13 +49,13 @@ class Heading extends Component public function getListeners() { - $teamId = Auth::user()->currentTeam()->id; - return [ - "echo-private:team.{$teamId},ServiceStatusChanged" => 'checkStatus', - "echo-private:team.{$teamId},ServiceChecked" => 'serviceChecked', 'refresh' => '$refresh', 'envsUpdated' => '$refresh', + ...$this->teamChannelListeners([ + 'ServiceStatusChanged' => 'checkStatus', + 'ServiceChecked' => 'serviceChecked', + ]), ]; } diff --git a/app/Livewire/Project/Service/ResourceCard.php b/app/Livewire/Project/Service/ResourceCard.php index fd27f60c30..22ed772a80 100644 --- a/app/Livewire/Project/Service/ResourceCard.php +++ b/app/Livewire/Project/Service/ResourceCard.php @@ -5,14 +5,15 @@ namespace App\Livewire\Project\Service; use App\Models\Service; use App\Models\ServiceApplication; use App\Models\ServiceDatabase; +use App\Traits\ListensToTeamChannel; use Illuminate\Contracts\View\View; use Illuminate\Foundation\Auth\Access\AuthorizesRequests; -use Illuminate\Support\Facades\Auth; use Livewire\Component; class ResourceCard extends Component { use AuthorizesRequests; + use ListensToTeamChannel; public Service $service; @@ -22,19 +23,9 @@ class ResourceCard extends Component public function getListeners(): array { - $user = Auth::user(); - if (! $user) { - return []; - } - - $team = $user->currentTeam(); - if (! $team) { - return []; - } - - return [ - "echo-private:team.{$team->id},ServiceChecked" => 'refreshResource', - ]; + return $this->teamChannelListeners([ + 'ServiceChecked' => 'refreshResource', + ]); } public function refreshResource(): void diff --git a/app/Livewire/Project/Service/Status.php b/app/Livewire/Project/Service/Status.php index 27919f1ae7..e807e22171 100644 --- a/app/Livewire/Project/Service/Status.php +++ b/app/Livewire/Project/Service/Status.php @@ -3,11 +3,14 @@ namespace App\Livewire\Project\Service; use App\Models\Service; +use App\Traits\ListensToTeamChannel; use Illuminate\Contracts\View\View; use Livewire\Component; class Status extends Component { + use ListensToTeamChannel; + public Service $service; public ?string $selectedResourceUuid = null; @@ -19,12 +22,10 @@ class Status extends Component public function getListeners(): array { - $teamId = auth()->user()->currentTeam()->id; - - return [ - "echo-private:team.{$teamId},ServiceStatusChanged" => 'refreshStatus', - "echo-private:team.{$teamId},ServiceChecked" => 'refreshStatus', - ]; + return $this->teamChannelListeners([ + 'ServiceStatusChanged' => 'refreshStatus', + 'ServiceChecked' => 'refreshStatus', + ]); } public function refreshStatus(): void diff --git a/app/Livewire/Project/Service/Storage.php b/app/Livewire/Project/Service/Storage.php index 6bb1d3a588..0827a59994 100644 --- a/app/Livewire/Project/Service/Storage.php +++ b/app/Livewire/Project/Service/Storage.php @@ -9,6 +9,7 @@ use App\Models\LocalPersistentVolume; use App\Models\ServiceApplication; use App\Models\ServiceDatabase; use App\Support\ValidationPatterns; +use App\Traits\ListensToTeamChannel; use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Illuminate\Support\Facades\Gate; use Illuminate\Support\Str; @@ -18,6 +19,7 @@ use Livewire\Component; class Storage extends Component { use AuthorizesRequests; + use ListensToTeamChannel; public $resource; @@ -60,12 +62,12 @@ class Storage extends Component public function getListeners() { - $teamId = auth()->user()->currentTeam()->id; - return [ - "echo-private:team.{$teamId},FileStorageChanged" => 'refreshStoragesFromEvent', 'storageCountsChanged' => 'refreshStorages', 'addNewVolume', + ...$this->teamChannelListeners([ + 'FileStorageChanged' => 'refreshStoragesFromEvent', + ]), ]; } diff --git a/app/Livewire/Project/Service/VolumeBackup/Index.php b/app/Livewire/Project/Service/VolumeBackup/Index.php index 600b8b9047..77ee2b9d1c 100644 --- a/app/Livewire/Project/Service/VolumeBackup/Index.php +++ b/app/Livewire/Project/Service/VolumeBackup/Index.php @@ -8,6 +8,7 @@ use App\Models\ScheduledDatabaseBackup; use App\Models\ScheduledVolumeBackup; use App\Models\Service; use App\Models\ServiceDatabase; +use App\Traits\ListensToTeamChannel; use Illuminate\Contracts\View\View; use Illuminate\Database\Eloquent\Collection; use Illuminate\Foundation\Auth\Access\AuthorizesRequests; @@ -17,6 +18,7 @@ use Livewire\Component; class Index extends Component { use AuthorizesRequests; + use ListensToTeamChannel; public Service $service; @@ -37,13 +39,13 @@ class Index extends Component public function getListeners(): array { - $teamId = currentTeam()->id; - return [ 'refreshVolumeBackups' => '$refresh', 'modalClosed' => 'closeScheduleModal', - "echo-private:team.{$teamId},ServiceChecked" => '$refresh', - "echo-private:team.{$teamId},BackupCreated" => '$refresh', + ...$this->teamChannelListeners([ + 'ServiceChecked' => '$refresh', + 'BackupCreated' => '$refresh', + ]), ]; } diff --git a/app/Livewire/Project/Shared/ConfigurationChecker.php b/app/Livewire/Project/Shared/ConfigurationChecker.php index 1a6015042c..14ce65f95f 100644 --- a/app/Livewire/Project/Shared/ConfigurationChecker.php +++ b/app/Livewire/Project/Shared/ConfigurationChecker.php @@ -13,11 +13,14 @@ use App\Models\StandaloneMysql; use App\Models\StandalonePostgresql; use App\Models\StandaloneRedis; use App\Models\StandaloneSqlite; +use App\Traits\ListensToTeamChannel; use Illuminate\Contracts\View\View; use Livewire\Component; class ConfigurationChecker extends Component { + use ListensToTeamChannel; + public bool $isConfigurationChanged = false; public array $configurationDiff = []; @@ -30,12 +33,12 @@ class ConfigurationChecker extends Component public function getListeners(): array { - $teamId = auth()->user()->currentTeam()->id; - return [ - "echo-private:team.{$teamId},ApplicationConfigurationChanged" => 'configurationChanged', 'configurationChanged' => 'configurationChanged', 'envsUpdated' => 'configurationChanged', + ...$this->teamChannelListeners([ + 'ApplicationConfigurationChanged' => 'configurationChanged', + ]), ]; } diff --git a/app/Livewire/Project/Shared/Destination.php b/app/Livewire/Project/Shared/Destination.php index 1bb46b2aa8..55d3f64e84 100644 --- a/app/Livewire/Project/Shared/Destination.php +++ b/app/Livewire/Project/Shared/Destination.php @@ -9,6 +9,7 @@ use App\Models\Application; use App\Models\Server; use App\Models\StandaloneDocker; use App\Services\ScheduleNextRunRecalculator; +use App\Traits\ListensToTeamChannel; use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Illuminate\Support\Collection; use Livewire\Component; @@ -16,6 +17,7 @@ use Livewire\Component; class Destination extends Component { use AuthorizesRequests; + use ListensToTeamChannel; public $resource; @@ -23,12 +25,12 @@ class Destination extends Component public function getListeners() { - $teamId = auth()->user()->currentTeam()->id; - return [ - "echo-private:team.{$teamId},ApplicationStatusChanged" => 'loadData', - "echo-private:team.{$teamId},ServiceStatusChanged" => 'mount', 'refresh' => 'mount', + ...$this->teamChannelListeners([ + 'ApplicationStatusChanged' => 'loadData', + 'ServiceStatusChanged' => 'mount', + ]), ]; } diff --git a/app/Livewire/Project/Shared/Logs.php b/app/Livewire/Project/Shared/Logs.php index f5e4f0c33a..609d2efa3b 100644 --- a/app/Livewire/Project/Shared/Logs.php +++ b/app/Livewire/Project/Shared/Logs.php @@ -13,11 +13,14 @@ use App\Models\StandaloneMysql; use App\Models\StandalonePostgresql; use App\Models\StandaloneRedis; use App\Models\StandaloneSqlite; +use App\Traits\ListensToTeamChannel; use Illuminate\Support\Collection; use Livewire\Component; class Logs extends Component { + use ListensToTeamChannel; + public ?string $type = null; public Application|Service|StandalonePostgresql|StandaloneRedis|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|StandaloneDragonfly|StandaloneClickhouse|StandaloneSqlite $resource; @@ -47,11 +50,9 @@ class Logs extends Component public function getListeners() { - $teamId = auth()->user()->currentTeam()->id; - - return [ - "echo-private:team.{$teamId},ServiceChecked" => 'loadAllContainers', - ]; + return $this->teamChannelListeners([ + 'ServiceChecked' => 'loadAllContainers', + ]); } public function loadAllContainers() diff --git a/app/Livewire/Project/Shared/ScheduledTask/Executions.php b/app/Livewire/Project/Shared/ScheduledTask/Executions.php index cf426e0ca8..76e0735ff4 100644 --- a/app/Livewire/Project/Shared/ScheduledTask/Executions.php +++ b/app/Livewire/Project/Shared/ScheduledTask/Executions.php @@ -3,6 +3,7 @@ namespace App\Livewire\Project\Shared\ScheduledTask; use App\Models\ScheduledTask; +use App\Traits\ListensToTeamChannel; use Illuminate\Support\Collection; use Illuminate\Support\Facades\Auth; use Livewire\Attributes\Locked; @@ -10,6 +11,8 @@ use Livewire\Component; class Executions extends Component { + use ListensToTeamChannel; + #[Locked] public ScheduledTask $task; @@ -36,11 +39,9 @@ class Executions extends Component public function getListeners() { - $teamId = Auth::user()->currentTeam()->id; - - return [ - "echo-private:team.{$teamId},ScheduledTaskDone" => 'refreshExecutions', - ]; + return $this->teamChannelListeners([ + 'ScheduledTaskDone' => 'refreshExecutions', + ]); } public function mount($taskId) diff --git a/app/Livewire/Server/CloudflareTunnel.php b/app/Livewire/Server/CloudflareTunnel.php index 2ab8298543..df3ad5c197 100644 --- a/app/Livewire/Server/CloudflareTunnel.php +++ b/app/Livewire/Server/CloudflareTunnel.php @@ -4,6 +4,7 @@ namespace App\Livewire\Server; use App\Actions\Server\ConfigureCloudflared; use App\Models\Server; +use App\Traits\ListensToTeamChannel; use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Livewire\Attributes\Validate; use Livewire\Component; @@ -11,6 +12,7 @@ use Livewire\Component; class CloudflareTunnel extends Component { use AuthorizesRequests; + use ListensToTeamChannel; public Server $server; @@ -25,11 +27,9 @@ class CloudflareTunnel extends Component public function getListeners() { - $teamId = auth()->user()->currentTeam()->id; - - return [ - "echo-private:team.{$teamId},CloudflareTunnelConfigured" => 'refresh', - ]; + return $this->teamChannelListeners([ + 'CloudflareTunnelConfigured' => 'refresh', + ]); } public function refresh() diff --git a/app/Livewire/Server/DockerCleanupExecutions.php b/app/Livewire/Server/DockerCleanupExecutions.php index 6a739bc84c..6cb76fc9f3 100644 --- a/app/Livewire/Server/DockerCleanupExecutions.php +++ b/app/Livewire/Server/DockerCleanupExecutions.php @@ -3,11 +3,14 @@ namespace App\Livewire\Server; use App\Models\Server; +use App\Traits\ListensToTeamChannel; use Illuminate\Support\Collection; use Livewire\Component; class DockerCleanupExecutions extends Component { + use ListensToTeamChannel; + public Server $server; public Collection $executions; @@ -24,11 +27,9 @@ class DockerCleanupExecutions extends Component public function getListeners() { - $teamId = auth()->user()->currentTeam()->id; - - return [ - "echo-private:team.{$teamId},DockerCleanupDone" => 'refreshExecutions', - ]; + return $this->teamChannelListeners([ + 'DockerCleanupDone' => 'refreshExecutions', + ]); } public function mount(Server $server) diff --git a/app/Livewire/Server/Navbar.php b/app/Livewire/Server/Navbar.php index 545494db51..1a64d0a15e 100644 --- a/app/Livewire/Server/Navbar.php +++ b/app/Livewire/Server/Navbar.php @@ -9,6 +9,7 @@ use App\Enums\ProxyTypes; use App\Jobs\RestartProxyJob; use App\Models\Server; use App\Services\ProxyDashboardCacheService; +use App\Traits\ListensToTeamChannel; use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Illuminate\Support\Carbon; use Livewire\Component; @@ -16,6 +17,7 @@ use Livewire\Component; class Navbar extends Component { use AuthorizesRequests; + use ListensToTeamChannel; public Server $server; @@ -39,14 +41,14 @@ class Navbar extends Component public function getListeners() { - $teamId = auth()->user()->currentTeam()->id; - return [ 'refreshServerShow' => 'refreshServer', 'sentinel-restart-requested' => 'hideSentinelWarning', - "echo-private:team.{$teamId},ProxyStatusChangedUI" => 'showNotification', - "echo-private:team.{$teamId},SentinelRestarted" => 'refreshSentinelStatus', - "echo-private:team.{$teamId},SentinelSynchronized" => 'refreshSentinelStatus', + ...$this->teamChannelListeners([ + 'ProxyStatusChangedUI' => 'showNotification', + 'SentinelRestarted' => 'refreshSentinelStatus', + 'SentinelSynchronized' => 'refreshSentinelStatus', + ]), ]; } diff --git a/app/Livewire/Server/Proxy.php b/app/Livewire/Server/Proxy.php index cc46364272..f5da7146af 100644 --- a/app/Livewire/Server/Proxy.php +++ b/app/Livewire/Server/Proxy.php @@ -9,12 +9,14 @@ use App\Actions\Proxy\SaveProxyConfiguration; use App\Enums\ProxyTypes; use App\Models\Server; use App\Rules\SafeExternalUrl; +use App\Traits\ListensToTeamChannel; use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Livewire\Component; class Proxy extends Component { use AuthorizesRequests; + use ListensToTeamChannel; public Server $server; @@ -40,11 +42,11 @@ class Proxy extends Component public function getListeners() { - $teamId = auth()->user()->currentTeam()->id; - return [ 'saveConfiguration' => 'submit', - "echo-private:team.{$teamId},ProxyStatusChangedUI" => '$refresh', + ...$this->teamChannelListeners([ + 'ProxyStatusChangedUI' => '$refresh', + ]), ]; } diff --git a/app/Livewire/Server/Proxy/DynamicConfigurations.php b/app/Livewire/Server/Proxy/DynamicConfigurations.php index 6351dace86..a090872d66 100644 --- a/app/Livewire/Server/Proxy/DynamicConfigurations.php +++ b/app/Livewire/Server/Proxy/DynamicConfigurations.php @@ -3,6 +3,7 @@ namespace App\Livewire\Server\Proxy; use App\Models\Server; +use App\Traits\ListensToTeamChannel; use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Illuminate\Support\Collection; use Livewire\Component; @@ -10,6 +11,7 @@ use Livewire\Component; class DynamicConfigurations extends Component { use AuthorizesRequests; + use ListensToTeamChannel; public const MAX_CONFIGURATION_FILE_SIZE_BYTES = 1024 * 1024; @@ -25,11 +27,11 @@ class DynamicConfigurations extends Component public function getListeners() { - $teamId = auth()->user()->currentTeam()->id; - return [ - "echo-private:team.{$teamId},ProxyStatusChangedUI" => 'loadDynamicConfigurations', 'loadDynamicConfigurations', + ...$this->teamChannelListeners([ + 'ProxyStatusChangedUI' => 'loadDynamicConfigurations', + ]), ]; } diff --git a/app/Livewire/Server/Resources.php b/app/Livewire/Server/Resources.php index 5d0d2538bd..dd62a9e776 100644 --- a/app/Livewire/Server/Resources.php +++ b/app/Livewire/Server/Resources.php @@ -4,6 +4,7 @@ namespace App\Livewire\Server; use App\Models\Server; use App\Support\ValidationPatterns; +use App\Traits\ListensToTeamChannel; use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Illuminate\Pagination\LengthAwarePaginator; use Livewire\Component; @@ -12,6 +13,7 @@ use Livewire\WithPagination; class Resources extends Component { use AuthorizesRequests; + use ListensToTeamChannel; use WithPagination; public int $perPage = 10; @@ -39,11 +41,9 @@ class Resources extends Component public function getListeners() { - $teamId = auth()->user()->currentTeam()->id; - - return [ - "echo-private:team.{$teamId},ApplicationStatusChanged" => 'refreshStatus', - ]; + return $this->teamChannelListeners([ + 'ApplicationStatusChanged' => 'refreshStatus', + ]); } public function startUnmanaged($id) diff --git a/app/Livewire/Server/Security/Patches.php b/app/Livewire/Server/Security/Patches.php index 087836da3b..4dd97519aa 100644 --- a/app/Livewire/Server/Security/Patches.php +++ b/app/Livewire/Server/Security/Patches.php @@ -7,12 +7,14 @@ use App\Actions\Server\UpdatePackage; use App\Events\ServerPackageUpdated; use App\Models\Server; use App\Notifications\Server\ServerPatchCheck; +use App\Traits\ListensToTeamChannel; use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Livewire\Component; class Patches extends Component { use AuthorizesRequests; + use ListensToTeamChannel; public array $parameters; @@ -30,11 +32,9 @@ class Patches extends Component public function getListeners() { - $teamId = auth()->user()->currentTeam()->id; - - return [ - "echo-private:team.{$teamId},ServerPackageUpdated" => 'checkForUpdatesDispatch', - ]; + return $this->teamChannelListeners([ + 'ServerPackageUpdated' => 'checkForUpdatesDispatch', + ]); } public function mount() diff --git a/app/Traits/HasDatabaseStatusInfo.php b/app/Traits/HasDatabaseStatusInfo.php index 4b84748fd4..de9374a87f 100644 --- a/app/Traits/HasDatabaseStatusInfo.php +++ b/app/Traits/HasDatabaseStatusInfo.php @@ -20,6 +20,8 @@ use Illuminate\Support\Facades\Auth; */ trait HasDatabaseStatusInfo { + use ListensToTeamChannel; + public ?string $dbUrl = null; public ?string $dbUrlPublic = null; @@ -65,12 +67,10 @@ trait HasDatabaseStatusInfo $listeners["echo-private:user.{$user->id},DatabaseStatusChanged"] = 'refresh'; - $team = $user->currentTeam(); - if ($team) { - $listeners["echo-private:team.{$team->id},ServiceChecked"] = 'refresh'; - } - - return $listeners; + return [ + ...$listeners, + ...$this->teamChannelListeners(['ServiceChecked' => 'refresh']), + ]; } public function mount(): void diff --git a/app/Traits/ListensToTeamChannel.php b/app/Traits/ListensToTeamChannel.php new file mode 100644 index 0000000000..03242989b6 --- /dev/null +++ b/app/Traits/ListensToTeamChannel.php @@ -0,0 +1,53 @@ +teamChannelId = currentTeam()?->id; + } + + public function ignoreStaleTeamChannelEvent(): void + { + $this->skipRender(); + } + + /** + * @param array $handlers broadcast event name => handler method + * @return array + */ + protected function teamChannelListeners(array $handlers): array + { + $currentTeamId = currentTeam()?->id; + $teamId = $this->teamChannelId ?? $currentTeamId; + + if ($teamId === null) { + return []; + } + + $isStale = $teamId !== $currentTeamId; + + $listeners = []; + foreach ($handlers as $event => $handler) { + $listeners["echo-private:team.{$teamId},{$event}"] = $isStale ? 'ignoreStaleTeamChannelEvent' : $handler; + } + + return $listeners; + } +} diff --git a/tests/Feature/Livewire/TeamChannelListenersTest.php b/tests/Feature/Livewire/TeamChannelListenersTest.php new file mode 100644 index 0000000000..78127815b0 --- /dev/null +++ b/tests/Feature/Livewire/TeamChannelListenersTest.php @@ -0,0 +1,71 @@ + 0]); + $this->team = Team::factory()->create(); + $this->otherTeam = Team::factory()->create(); + $this->user = User::factory()->create(); + $this->team->members()->attach($this->user->id, ['role' => 'owner']); + $this->otherTeam->members()->attach($this->user->id, ['role' => 'owner']); + $this->actingAs($this->user); + session(['currentTeam' => $this->team]); +}); + +it('keeps team broadcast listeners of the mounted team after the session team changes', function () { + $project = Project::factory()->create(['team_id' => $this->team->id]); + $environment = Environment::factory()->create(['project_id' => $project->id]); + $application = Application::factory()->create(['environment_id' => $environment->id]); + + $component = Livewire::test(ServerStatusBadge::class, ['application' => $application]); + + // A team switch in another browser tab changes the shared session. + session(['currentTeam' => $this->otherTeam]); + + $component + ->dispatch("echo-private:team.{$this->team->id},ServiceChecked", ['teamId' => $this->team->id]) + ->assertOk(); +}); + +it('runs team broadcast handlers while the session team matches the mounted team', function () { + Livewire::test(LayoutPopups::class) + ->dispatch("echo-private:team.{$this->team->id},TestEvent") + ->assertDispatched('success'); +}); + +it('ignores team broadcasts of the mounted team after the session team changes', function () { + $component = Livewire::test(LayoutPopups::class); + + session(['currentTeam' => $this->otherTeam]); + + expect($component->instance()->getListeners()) + ->toBe(["echo-private:team.{$this->team->id},TestEvent" => 'ignoreStaleTeamChannelEvent']); + + $component + ->dispatch("echo-private:team.{$this->team->id},TestEvent") + ->assertOk() + ->assertNotDispatched('success'); +}); + +it('registers no team broadcast listeners without a current team', function () { + $project = Project::factory()->create(['team_id' => $this->team->id]); + $environment = Environment::factory()->create(['project_id' => $project->id]); + $application = Application::factory()->create(['environment_id' => $environment->id]); + session()->forget('currentTeam'); + + $component = Livewire::test(ServerStatusBadge::class, ['application' => $application]); + + expect($component->instance()->getListeners())->toBe([]); +}); diff --git a/tests/Feature/ServiceDatabaseVerticalNavigationTest.php b/tests/Feature/ServiceDatabaseVerticalNavigationTest.php index 00bf6a49ac..2c18dcf2ad 100644 --- a/tests/Feature/ServiceDatabaseVerticalNavigationTest.php +++ b/tests/Feature/ServiceDatabaseVerticalNavigationTest.php @@ -7,12 +7,13 @@ it('refreshes backup executions from backup broadcasts on the current team chann $databaseBackupJob = file_get_contents(app_path('Jobs/DatabaseBackupJob.php')); expect($serviceExecutions) - ->toContain('echo-private:team.{$teamId},BackupCreated') - ->toContain("=> '\$refresh'") + ->toContain('teamChannelListeners([') + ->toContain("'BackupCreated' => '\$refresh'") ->and($serviceBackups) - ->toContain('echo-private:team.{$teamId},BackupCreated') + ->toContain('teamChannelListeners([') + ->toContain("'BackupCreated' => '\$refresh'") ->and($databaseExecutions) - ->toContain('$teamId = currentTeam()->id') + ->toContain("'BackupCreated' => 'refreshBackupExecutions'") ->not->toContain('$userId = Auth::id()') ->and(strpos($databaseBackupJob, "'finished_at' => Carbon::now()->toImmutable()")) ->toBeLessThan(strrpos($databaseBackupJob, 'BackupCreated::dispatch($this->team->id)'));