diff --git a/app/Providers/FortifyServiceProvider.php b/app/Providers/FortifyServiceProvider.php index 70e2ed6fd6..9bdc5d7371 100644 --- a/app/Providers/FortifyServiceProvider.php +++ b/app/Providers/FortifyServiceProvider.php @@ -6,7 +6,6 @@ use App\Actions\Fortify\CreateNewUser; use App\Actions\Fortify\ResetUserPassword; use App\Actions\Fortify\UpdateUserPassword; use App\Models\OauthSetting; -use App\Models\TeamInvitation; use App\Models\User; use Illuminate\Http\Request; use Illuminate\Support\Facades\Hash; @@ -82,29 +81,18 @@ class FortifyServiceProvider extends ServiceProvider $user->updated_at = now(); $user->save(); - // Check if user has a pending invitation they haven't accepted yet - $invitation = TeamInvitation::whereEmail($email)->first(); - if ($invitation && $invitation->isValid()) { - // User is logging in for the first time after being invited - // Attach them to the invited team if not already attached - if (! $user->teams()->where('team_id', $invitation->team->id)->exists()) { - $user->teams()->attach($invitation->team->id, ['role' => $invitation->role]); - } - $team = $invitation->team; - $invitation->delete(); - session(['currentTeam' => $team]); - } else { - // Restore the last active team; only fall back when unambiguous. - $team = $user->resolveStoredTeam(); - if (! $team && $user->teams->isEmpty()) { - $team = $user->recreate_personal_team(); - } - if ($team) { - session(['currentTeam' => $team]); - } - // Otherwise (multiple teams, no stored choice) leave the session - // team unset so the user is sent to the team-selection screen. + // Pending team invitations are not accepted here; the user accepts + // them explicitly on the invitation page (team.invitation.show). + // Restore the last active team; only fall back when unambiguous. + $team = $user->resolveStoredTeam(); + if (! $team && $user->teams->isEmpty()) { + $team = $user->recreate_personal_team(); } + if ($team) { + session(['currentTeam' => $team]); + } + // Otherwise (multiple teams, no stored choice) leave the session + // team unset so the user is sent to the team-selection screen. return $user; } diff --git a/tests/Feature/Auth/LoginKeepsPendingInvitationTest.php b/tests/Feature/Auth/LoginKeepsPendingInvitationTest.php new file mode 100644 index 0000000000..d1b6a4de3e --- /dev/null +++ b/tests/Feature/Auth/LoginKeepsPendingInvitationTest.php @@ -0,0 +1,65 @@ +set('app.maintenance.store', 'array'); + InstanceSettings::unguarded(fn () => InstanceSettings::firstOrCreate(['id' => 0])); + + $this->invitee = User::factory()->create(['email' => 'invitee@example.com']); + $this->personalTeam = $this->invitee->teams->first(); + $this->personalTeam->update(['show_boarding' => false]); + + $this->invitingTeam = Team::factory()->create(['show_boarding' => false]); + $this->invitation = TeamInvitation::create([ + 'team_id' => $this->invitingTeam->id, + 'uuid' => 'pending-login-invitation', + 'email' => $this->invitee->email, + 'role' => 'admin', + 'link' => route('team.invitation.show', ['uuid' => 'pending-login-invitation']), + 'via' => 'email', + ]); +}); + +it('does not accept a pending invitation on password login', function () { + $this->post('/login', ['email' => $this->invitee->email, 'password' => 'password']) + ->assertRedirect(); + + $this->assertAuthenticatedAs($this->invitee); + expect($this->invitee->teams()->where('team_id', $this->invitingTeam->id)->exists())->toBeFalse() + ->and(data_get(session('currentTeam'), 'id'))->toBe($this->personalTeam->id); + $this->assertDatabaseHas('team_invitations', ['id' => $this->invitation->id]); +}); + +it('lets the user accept the invitation explicitly after login', function () { + $this->post('/login', ['email' => $this->invitee->email, 'password' => 'password']); + + $this->get(route('team.invitation.show', $this->invitation->uuid)) + ->assertSuccessful() + ->assertSee($this->invitingTeam->name); + + $this->post(route('team.invitation.accept', $this->invitation->uuid)) + ->assertRedirect(route('team.index')); + + expect($this->invitee->teams()->where('team_id', $this->invitingTeam->id)->first()?->pivot->role)->toBe('admin') + ->and(data_get(session('currentTeam'), 'id'))->toBe($this->invitingTeam->id); + $this->assertDatabaseMissing('team_invitations', ['id' => $this->invitation->id]); +}); + +it('returns to the invitation page after logging in from the invitation link', function () { + $this->get(route('team.invitation.show', $this->invitation->uuid)) + ->assertRedirect(route('login')); + + $this->post('/login', ['email' => $this->invitee->email, 'password' => 'password']) + ->assertRedirect(route('team.invitation.show', $this->invitation->uuid)); + + $this->get(route('team.invitation.show', $this->invitation->uuid)) + ->assertSuccessful() + ->assertSee($this->invitingTeam->name); +});