mirror of
https://github.com/coollabsio/coolify.git
synced 2026-11-06 13:10:37 -04:00
fix(auth): do not accept team invitations on login
A password login added the user to the first team that invited their email and switched to it, without asking. The user now accepts the invitation on the invitation page. The invitation link login flow works again, because login no longer consumes the invitation.
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\Team;
|
||||
use App\Models\TeamInvitation;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
config()->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);
|
||||
});
|
||||
Reference in New Issue
Block a user