diff --git a/app/Actions/Team/DeleteTeam.php b/app/Actions/Team/DeleteTeam.php index bde72448d..904460d34 100644 --- a/app/Actions/Team/DeleteTeam.php +++ b/app/Actions/Team/DeleteTeam.php @@ -60,7 +60,12 @@ public function handle(Team $team, User $user): ?Team $team->delete(); - return $user->teams()->first(); + // Resolve the next active team the same way login does: the user's + // stored choice when still valid, or their sole remaining team. + // Returns null for a multi-team user whose active team was just + // deleted, so refreshSession sends them to the selection screen + // instead of silently dropping them into an arbitrary first team. + return User::query()->find($user->id)?->resolveStoredTeam(); }); Cache::forget("user:{$user->id}:team:{$team->id}"); diff --git a/app/Livewire/Admin/Index.php b/app/Livewire/Admin/Index.php index 226d2e332..f54f40ffd 100644 --- a/app/Livewire/Admin/Index.php +++ b/app/Livewire/Admin/Index.php @@ -33,7 +33,7 @@ public function back() if (session('impersonating')) { session()->forget('impersonating'); $user = User::find(0); - $team_to_switch_to = $user->teams->first(); + $team_to_switch_to = $user->resolveStoredTeam() ?? $user->teams->first(); Auth::login($user); refreshSession($team_to_switch_to); @@ -69,7 +69,7 @@ public function switchUser(int $user_id) if (! $user) { abort(404); } - $team_to_switch_to = $user->teams->first(); + $team_to_switch_to = $user->resolveStoredTeam() ?? $user->teams->first(); Auth::login($user); refreshSession($team_to_switch_to); diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php index e9d17a4e0..5c9516b79 100644 --- a/bootstrap/helpers/shared.php +++ b/bootstrap/helpers/shared.php @@ -602,9 +602,11 @@ function refreshSession(?Team $team = null): void }); session(['currentTeam' => $team]); - // Persist the active team so it can be restored after logout/login. + // Persist the active team so it can be restored after logout/login — but + // never while an admin is impersonating, so viewing another user's account + // does not overwrite that user's real last-active team. $user = Auth::user(); - if ($user && $user->current_team_id !== $team->id) { + if ($user && ! session('impersonating') && $user->current_team_id !== $team->id) { $user->current_team_id = $team->id; $user->saveQuietly(); } diff --git a/tests/Feature/Team/ActiveTeamPersistenceTest.php b/tests/Feature/Team/ActiveTeamPersistenceTest.php index 4c0329e36..a0cadd5fd 100644 --- a/tests/Feature/Team/ActiveTeamPersistenceTest.php +++ b/tests/Feature/Team/ActiveTeamPersistenceTest.php @@ -183,6 +183,54 @@ function userWithTwoTeams(): array expect($user->fresh()->current_team_id)->toBe($personal->id); }); +it('returns the sole remaining team when the deleting owner has one team left', function () { + [$owner, $personal, $shared] = userWithTwoTeams(); + $owner->update(['current_team_id' => $shared->id]); + + $next = app(DeleteTeam::class)->handle($shared->fresh(), $owner); + + expect($next?->id)->toBe($personal->id); +}); + +it('returns null (picker) when the deleting owner still has multiple teams left', function () { + [$owner, , $shared] = userWithTwoTeams(); + $third = Team::factory()->create(['show_boarding' => false]); + $owner->teams()->attach($third, ['role' => 'owner']); + $owner->update(['current_team_id' => $shared->id]); + + // Deleting the active team leaves personal + third: ambiguous, so no team is + // chosen silently and refreshSession(null) routes to the selection screen. + $next = app(DeleteTeam::class)->handle($shared->fresh(), $owner->fresh()); + + expect($next)->toBeNull(); +}); + +it('keeps the active team when the deleted team was not the active one', function () { + [$owner, $personal, $shared] = userWithTwoTeams(); + $third = Team::factory()->create(['show_boarding' => false]); + $owner->teams()->attach($third, ['role' => 'owner']); + $owner->update(['current_team_id' => $personal->id]); + + // Deleting a non-active team must not move the owner off their active team. + $next = app(DeleteTeam::class)->handle($shared->fresh(), $owner->fresh()); + + expect($next?->id)->toBe($personal->id); +}); + +it('does not persist current_team_id while impersonating', function () { + [$user, , $second] = userWithTwoTeams(); + $user->update(['current_team_id' => $second->id]); + $this->actingAs($user); + session(['impersonating' => true]); + + // Viewing a user's account switches the session team but must never + // overwrite that user's stored last-active team. + refreshSession($user->teams->first()); + + expect(data_get(session('currentTeam'), 'id'))->toBe($user->teams->first()->id) + ->and($user->fresh()->current_team_id)->toBe($second->id); +}); + 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]);