diff --git a/app/Actions/Team/DeleteTeam.php b/app/Actions/Team/DeleteTeam.php index be880b7e7..5d0c1e688 100644 --- a/app/Actions/Team/DeleteTeam.php +++ b/app/Actions/Team/DeleteTeam.php @@ -50,6 +50,7 @@ public function handle(Team $team, User $user): ?Team ->get() ->each(function (User $member) use ($team): void { $member->teams()->detach($team); + $member->clearStoredTeamIfMatches($team->id); DB::table('sessions')->where('user_id', $member->id)->delete(); }); diff --git a/app/Livewire/Team/Member.php b/app/Livewire/Team/Member.php index 38c932c39..ab3f7938a 100644 --- a/app/Livewire/Team/Member.php +++ b/app/Livewire/Team/Member.php @@ -89,6 +89,7 @@ public function remove() DB::transaction(function () use ($teamId): void { $this->member->teams()->detach($teamId); RevokeUserTeamTokens::forUserTeam($this->member, $teamId); + $this->member->clearStoredTeamIfMatches($teamId); }); // Clear cache for the removed user - both old and new key formats Cache::forget("team:{$this->member->id}"); diff --git a/app/Models/User.php b/app/Models/User.php index 47f6f2fd4..ab9a817bf 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -401,6 +401,21 @@ public function resolveStoredTeam(): ?Team return null; } + /** + * Reset the persisted active team when it points to the given team. + * + * Called when the user is removed from a team (or the team is deleted) so a + * stale current_team_id can never be trusted after the fact. Read paths + * already re-validate membership; this is defense-in-depth that clears the + * dangling value at the source event instead of relying on self-healing. + */ + public function clearStoredTeamIfMatches(int $teamId): void + { + if ($this->current_team_id === $teamId) { + $this->forceFill(['current_team_id' => null])->saveQuietly(); + } + } + public function role(): ?string { if (data_get($this, 'pivot')) { diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php index 169132cb7..1c19fd1ca 100644 --- a/bootstrap/helpers/shared.php +++ b/bootstrap/helpers/shared.php @@ -581,8 +581,13 @@ function refreshSession(?Team $team = null): void if (! $team) { // The user has no team left (e.g. just deleted their current team and // belongs to no other): clear the stale session reference instead of - // dereferencing null. + // dereferencing null, and drop the persisted choice so it is not + // restored on next login. session()->forget('currentTeam'); + $user = Auth::user(); + if ($user && ! is_null($user->current_team_id)) { + $user->forceFill(['current_team_id' => null])->saveQuietly(); + } return; } diff --git a/tests/Feature/Team/ActiveTeamPersistenceTest.php b/tests/Feature/Team/ActiveTeamPersistenceTest.php index 4dc1570cd..b481cce31 100644 --- a/tests/Feature/Team/ActiveTeamPersistenceTest.php +++ b/tests/Feature/Team/ActiveTeamPersistenceTest.php @@ -1,5 +1,6 @@ headers->get('Location'))->not->toBe(route('team.select')); }); +it('clears the stored team when the member is removed from it', function () { + [$user, , $second] = userWithTwoTeams(); + $user->update(['current_team_id' => $second->id]); + + // Simulate the removal event (Team\Member::remove detaches then clears). + $user->teams()->detach($second->id); + $user->clearStoredTeamIfMatches($second->id); + + expect($user->fresh()->current_team_id)->toBeNull(); +}); + +it('keeps the stored team when the member is removed from a different team', function () { + [$user, $personal, $second] = userWithTwoTeams(); + $user->update(['current_team_id' => $second->id]); + + $user->teams()->detach($personal->id); + $user->clearStoredTeamIfMatches($personal->id); + + expect($user->fresh()->current_team_id)->toBe($second->id); +}); + +it('clears the stored team for members when their team is deleted', function () { + [$owner, , $shared] = userWithTwoTeams(); + $member = User::factory()->create(); + $member->teams()->attach($shared, ['role' => 'member']); + $member->update(['current_team_id' => $shared->id]); + + app(DeleteTeam::class)->handle($shared->fresh(), $owner); + + expect($member->fresh()->current_team_id)->toBeNull(); +}); + it('bounces users who already have an active team away from the select screen', function () { [$user, , $second] = userWithTwoTeams(); $user->update(['current_team_id' => $second->id]);