where('organisation_id', $org->id) ->pending() ->where('expires_at', '>', now()) ->first(); if ($existingInvitation) { throw ValidationException::withMessages([ 'email' => ['Er is al een openstaande uitnodiging voor dit e-mailadres.'], ]); } $existingMember = $org->users()->where('email', $email)->first(); if ($existingMember) { throw ValidationException::withMessages([ 'email' => ['Gebruiker is al lid van deze organisatie.'], ]); } $plainToken = bin2hex(random_bytes(32)); $invitation = new UserInvitation(['email' => $email]); $invitation->invited_by_user_id = $invitedBy->id; $invitation->organisation_id = $org->id; $invitation->role = $role; $invitation->token = hash('sha256', $plainToken); $invitation->status = 'pending'; $invitation->expires_at = now()->addDays(7); $invitation->save(); // Set transient plain token for use in the email URL $invitation->plainToken = $plainToken; $this->emailService->send( type: EmailTemplateType::INVITATION, recipientEmail: $email, recipientName: $email, variables: [ 'organisation_name' => $org->name, ], actionUrl: config('app.frontend_app_url') . '/invitations/' . $plainToken . '/accept', organisation: $org, triggeredByUserId: $invitedBy->id, ); activity('invitation') ->performedOn($invitation) ->causedBy($invitedBy) ->withProperties(['email' => $email, 'role' => $role]) ->log("Invited {$email} as {$role}"); return $invitation; } public function accept(UserInvitation $invitation, ?string $password = null): User { if (! $invitation->isPending() || $invitation->isExpired()) { throw ValidationException::withMessages([ 'token' => ['Deze uitnodiging is niet meer geldig.'], ]); } $user = User::where('email', $invitation->email)->first(); if (! $user) { if (! $password) { throw ValidationException::withMessages([ 'password' => ['Een wachtwoord is vereist om een nieuw account aan te maken.'], ]); } $user = User::create([ 'first_name' => Str::before($invitation->email, '@'), 'last_name' => '', 'email' => $invitation->email, 'password' => $password, 'email_verified_at' => now(), ]); app(PersonIdentityService::class)->detectMatchesForUser($user); } $organisation = $invitation->organisation; if (! $organisation->users()->where('user_id', $user->id)->exists()) { $organisation->users()->attach($user, ['role' => $invitation->role]); } $invitation->markAsAccepted(); activity('invitation') ->performedOn($invitation) ->causedBy($user) ->withProperties(['email' => $invitation->email]) ->log("Accepted invitation for {$organisation->name}"); return $user; } public function expireOldInvitations(): int { return UserInvitation::where('status', 'pending') ->where('expires_at', '<', now()) ->update(['status' => 'expired']); } }