Back to Course |
Laravel 11 Multi-Tenancy: All You Need To Know

Accept Invitation: Existing User or Register New User

Let's finish the invitation system by accepting the invitation.

If I'm logged in to another tenant at the moment, I need to:

  1. Accept the invitation.
  2. Attach me to the tenant.
  3. Set the current tenant from the invitation.
  4. Redirect to the invited tenants' dashboard.

app/Http/Controllers/UserController.php:

class UserController extends Controller
{
// ...
 
public function acceptInvitation(string $token)
{
$invitation = Invitation::where('token', $token)
->whereNull('accepted_at')
->firstOrFail();
 
if (auth()->check()) {
$invitation->update(['accepted_at' => now()]);
 
auth()->user()->tenants()->attach($invitation->tenant_id);
 
auth()->user()->update(['current_tenant_id' => $invitation->tenant_id]);
 
$tenantDomain = str_replace('://', '://' . $invitation->tenant->subdomain . '.', config('app.url'));
return redirect($tenantDomain . route('dashboard', absolute: false));
} else {
// redirect to register
}
}
}

Now, if I register with a new user, this user has only its own tenant.

From another user, I send email invitations to the newly registered user.

As you can see, when the invitation is accepted, the user should be redirected to the bbb subdomain tenant and should have two tenants.

We can see that after accepting the invitation, the user is redirected to the correct subdomain. The user now belongs to two tenants. The tenant in bold is active, which is correct in this case.

Good. The first case is working correctly. Now, let's cover the case of registering a fresh user after the invitation.


In the UserController, we only need to redirect the user to the registration page with a token in the URL.

app/Http/Controllers/UserController.php:

class UserController extends Controller
{
// ...
 
public function acceptInvitation(string $token): RedirectResponse
{
$invitation = Invitation::where('token', $token)
->whereNull('accepted_at')
->firstOrFail();
 
if (auth()->check()) {
$invitation->update(['accepted_at' => now()]);
 
auth()->user()->tenants()->attach($invitation->tenant_id);
 
auth()->user()->update(['current_tenant_id' => $invitation->tenant_id]);
 
$tenantDomain = str_replace('://', '://' . $invitation->tenant->subdomain . '.', config('app.url'));
return redirect($tenantDomain . route('dashboard', absolute: false));
} else {
return redirect()->route('register', ['token' => $invitation->token]);
}
}
}

Now, we will pass the email to the register form. We must get that email from the invitation token.

app/Http/Controllers/Auth/RegisteredUserController.php:

use App\Models\Invitation;
 
class RegisteredUserController extends Controller
{
public function create(): View
{
$invitationEmail = null;
 
if (request('token')) {
$invitation = Invitation::where('token', request('token'))
->whereNull('accepted_at')
->firstOrFail();
 
$invitationEmail = $invitation->email;
}
 
return view('auth.register');
return view('auth.register', compact('invitationEmail'));
}
 
// ...
}

We don't need the subdomain in the View because it will already exist. Also, we will disable the email field and set the value to $invitationEmail.

resources/views/auth/register.blade.php:

// ...
 
<!-- Email Address -->
<div class="mt-4">
<x-input-label for="email" :value="__('Email')" />
<x-text-input id="email" class="block mt-1 w-full" type="email" name="email" :value="old('email')" required autocomplete="username" :disabled="! is_null($invitationEmail)" /> {{-- [tl! ++] --}}
<x-input-error :messages="$errors->get('email')" class="mt-2" />
</div>
 
<!-- Subdomain -->
@empty($invitationEmail) {{-- [tl! ++] --}}
<div class="mt-4">
<x-input-label for="subdomain" :value="__('Subdomain')" />
<x-text-input id="subdomain" class="block mt-1 mr-2 w-full" type="text" name="subdomain" :value="old('subdomain')" required />
<x-input-error :messages="$errors->get('subdomain')" class="mt-2" />
</div>
@endempty {{-- [tl! ++] --}}
 
// ...

We also need the token as a hidden input.

resources/views/auth/register.blade.php:

// ...
 
@csrf
@empty(! $invitationEmail)
<input type="hidden" value="{{ request('token') }}">
@endempty
 
// ...

If we try to invite with an email that isn't registered already after opening the invitation URL from the email, we are redirected to the tenant's registration page. We don't see the subdomain input in the registration page, and email is disabled.

Finally, we need to register the new user. First, we must change the validation rule from required to sometimes for the email and subdomain inputs.

app/Http/Controllers/Auth/RegisteredUserController.php:

class RegisteredUserController extends Controller
{
// ...
 
public function store(Request $request): RedirectResponse
{
$request->validate([
'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'string', 'lowercase', 'email', 'max:255', 'unique:'.User::class],
'email' => ['sometimes', 'string', 'lowercase', 'email', 'max:255', 'unique:'.User::class],
'password' => ['required', 'confirmed', Rules\Password::defaults()],
'subdomain' => ['required', 'alpha:ascii', 'unique:'.Tenant::class],
'subdomain' => ['sometimes', 'alpha:ascii', 'unique:'.Tenant::class],
]);
 
// ...
}
}

Next, we must check the token, and if it's invalid, throw a validation error.

app/Http/Controllers/Auth/RegisteredUserController.php:

class RegisteredUserController extends Controller
{
// ...
 
public function store(Request $request): RedirectResponse
{
$request->validate([
'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'string', 'lowercase', 'email', 'max:255', 'unique:'.User::class],
'email' => ['sometimes', 'string', 'lowercase', 'email', 'max:255', 'unique:'.User::class],
'password' => ['required', 'confirmed', Rules\Password::defaults()],
'subdomain' => ['required', 'alpha:ascii', 'unique:'.Tenant::class],
'subdomain' => ['sometimes', 'alpha:ascii', 'unique:'.Tenant::class],
]);
 
$email = $request->email;
if ($request->has('token')) {
$invitation = Invitation::with('tenant')
->where('token', $request->token)
->whereNull('accepted_at')
->firstOr(function () {
throw ValidationException::withMessages([
'email' => __('Invitation link incorrect'),
]);
});
 
$email = $invitation->email;
}
 
$user = User::create([
'name' => $request->name,
'email' => $request->email,
'email' => $email,
'password' => Hash::make($request->password),
]);
 
// ...
}
}

Then, if we have an invitation, the user should be attached to the existing one instead of creating a tenant. Finally, it is redirected to the correct subdomain.

app/Http/Controllers/Auth/RegisteredUserController.php:

class RegisteredUserController extends Controller
{
// ...
 
public function store(Request $request): RedirectResponse
{
// ...
 
$user = User::create([
'name' => $request->name,
'email' => $email,
'password' => Hash::make($request->password),
]);
 
$subdomain = $request->subdomain;
if ($invitation) {
$invitation->update(['accepted_at' => now()]);
$invitation->tenant->users()->attach($user->id);
$user->update(['current_tenant_id' => $invitation->tenant_id]);
$subdomain = $invitation->tenant->subdomain;
} else {
$tenant = Tenant::create([
'name' => $request->name . ' Team',
'subdomain' => $request->subdomain,
]);
$tenant->users()->attach($user, ['is_owner' => true]);
$user->update(['current_tenant_id' => $tenant->id]);
}
 
event(new Registered($user));
 
Auth::login($user);
 
$tenantDomain = str_replace('://', '://' . $request->subdomain . '.', config('app.url'));
$tenantDomain = str_replace('://', '://' . $subdomain . '.', config('app.url'));
return redirect($tenantDomain . route('dashboard', absolute: false));
}
}

The new user's registration from the invitation link should now work. Now, we have the entire process of seeing the invited users, inviting a new user, and then accepting the invitation as an existing or new user.


We have finished the chapter on single database multi-tenancy without any packages.

In the following sections, we will repeat almost the same things with extra packages and see how they help us.