From bf2798c0f44a082110b7169344f5672318c2a8f9 Mon Sep 17 00:00:00 2001 From: Gregor Vostrak Date: Thu, 23 Jul 2026 20:01:32 +0200 Subject: [PATCH] fix invitations not being respected during signup when email case differs --- app/Service/InvitationService.php | 6 +++++- tests/Feature/RegistrationTest.php | 34 ++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/app/Service/InvitationService.php b/app/Service/InvitationService.php index 345933a7..4de14912 100644 --- a/app/Service/InvitationService.php +++ b/app/Service/InvitationService.php @@ -23,6 +23,10 @@ class InvitationService */ public function inviteUser(Organization $organization, string $email, Role $role, User $inviter): OrganizationInvitation { + // Normalize the email so it matches how user emails are stored (see UserService::createUser), + // otherwise a mixed-case invite silently fails to link on registration. + $email = strtolower($email); + if (app(MemberService::class)->isEmailAlreadyMember($organization, $email)) { throw new UserIsAlreadyMemberOfOrganizationApiException; } @@ -55,7 +59,7 @@ class InvitationService $organizations = new Collection; $invitations = OrganizationInvitation::query() - ->where('email', $user->email) + ->whereRaw('lower(email) = ?', [strtolower($user->email)]) ->whereNotNull('accepted_at') ->get(); diff --git a/tests/Feature/RegistrationTest.php b/tests/Feature/RegistrationTest.php index d5b59ef0..1ba1aada 100644 --- a/tests/Feature/RegistrationTest.php +++ b/tests/Feature/RegistrationTest.php @@ -380,6 +380,40 @@ class RegistrationTest extends TestCaseWithDatabase $this->assertSame($user->organization->id, $organizations->first()->id); } + public function test_registration_joins_invited_organization_even_if_invitation_email_casing_differs(): void + { + // Arrange: invitation stored with a different casing than the registration email + $user = $this->createUserWithPermission(); + OrganizationInvitation::factory() + ->forOrganization($user->organization) + ->role(Role::Employee) + ->accepted() + ->create([ + 'email' => 'Invited.User@example.com', + ]); + + // Act + $response = $this->post('/register', [ + 'name' => 'Invited User', + 'email' => 'invited.user@example.com', + 'password' => 'password', + 'password_confirmation' => 'password', + 'terms' => true, + ]); + + // Assert: joined the inviting organization, no extra personal organization, invitation consumed + $this->assertAuthenticated(); + $response->assertRedirect(RouteServiceProvider::HOME); + $newUser = User::where('email', 'invited.user@example.com')->first(); + $this->assertNotNull($newUser); + $this->assertDatabaseMissing(OrganizationInvitation::class, [ + 'email' => 'Invited.User@example.com', + ]); + $organizations = $newUser->organizations; + $this->assertCount(1, $organizations); + $this->assertSame($user->organization->id, $organizations->first()->id); + } + public function test_registration_logs_and_skips_accepted_invitation_with_invalid_role(): void { // Arrange