In a few upcoming lessons, let's work on the invitation system.
So, I've just registered, and I want to invite new users to my team/tenant. For that, we will create a new menu item on top called Users
where we will manage the invitations and the users who are invited to the team.
For that, we must introduce a concept called "Team Owner" or the person who created the tenant. Again, there are multiple ways how you can do that in the database. We will add an is_owner
boolean column to the tenant_user
pivot table, which will default to false
and set to true
during registration.
Database Changes
So, first, let's add a column titled is_owner
.
php artisan make:migration "add is owner to tenant user table"
database/migrations/xxx_add_is_owner_to_tenant_user_table.php:
Schema::table('tenant_user', function (Blueprint $table) { $table->boolean('is_owner')->default(false);});
app/Models/Tenant.php:
class Tenant extends Model{ // ... public function users(): BelongsToMany { return $this->belongsToMany(User::class); return $this->belongsToMany(User::class)->withPivot('is_owner'); }}
app/Models/User.php:
class User extends Authenticatable{ // ... public function tenants(): BelongsToMany { return $this->belongsToMany(Tenant::class); return $this->belongsToMany(Tenant::class)->withPivot('is_owner'); }}
Registration: Setting the Owner
Next, when we attach a user to a tenant in the registration Controller, we must set is_owner
to true.
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], 'password' => ['required', 'confirmed', Rules\Password::defaults()], 'subdomain' => ['required', 'alpha:ascii', 'unique:'.Tenant::class], ]); $user = User::create([ 'name' => $request->name, 'email' => $request->email, 'password' => Hash::make($request->password), ]); $tenant = Tenant::create([ 'name' => $request->name . ' Team', 'subdomain' => $request->subdomain, ]); $tenant->users()->attach($user); $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')); return redirect($tenantDomain . route('dashboard', absolute: false)); }}
After registering in the database, the is_owner
is true.
Manage Tenant Users
Now, with this user who is the owner of a tenant, we should see the page to manage tenant users.
php artisan make:controller UserController
app/Http/Controllers/UserController.php:
use Illuminate\Contracts\View\View; class UserController extends Controller{ public function index(): View { return view('users.index'); }}
resources/views/users/index.php:
<x-app-layout> <x-slot name="header"> <h2 class="font-semibold text-xl text-gray-800 leading-tight"> {{ __('Users') }} </h2> </x-slot> <div class="py-12"> <div class="max-w-7xl mx-auto sm:px-6 lg:px-8"> <div class="bg-white overflow-hidden shadow-sm sm:rounded-lg"> <div class="p-6 bg-white border-b border-gray-200"> Coming soon. </div> </div> </div> </div></x-app-layout>
routes/web.php:
// ... Route::middleware('auth')->group(function () { Route::get('tenants/change/{tenantId}', \App\Http\Controllers\TenantController::class)->name('tenants.change'); Route::resource('tasks', \App\Http\Controllers\TaskController::class); Route::resource('projects', \App\Http\Controllers\ProjectController::class); Route::resource('users', \App\Http\Controllers\UserController::class)->only('index', 'store'); Route::get('/profile', [ProfileController::class, 'edit'])->name('profile.edit'); Route::patch('/profile', [ProfileController::class, 'update'])->name('profile.update'); Route::delete('/profile', [ProfileController::class, 'destroy'])->name('profile.destroy');}); require __DIR__.'/auth.php';
resources/views/layouts/navigation.blade.php:
// ... <!-- Navigation Links --><div class="hidden space-x-8 sm:-my-px sm:ms-10 sm:flex"> <x-nav-link :href="route('dashboard')" :active="request()->routeIs('dashboard')"> {{ __('Dashboard') }} </x-nav-link> <x-nav-link :href="route('tasks.index')" :active="request()->routeIs('tasks.*')"> {{ __('Tasks') }} </x-nav-link> <x-nav-link :href="route('projects.index')" :active="request()->routeIs('projects.*')"> {{ __('Projects') }} </x-nav-link> <x-nav-link :href="route('users.index')" :active="request()->routeIs('users.*')"> {{ __('Users') }} </x-nav-link></div> // ...
In the navigation, we can see the Users
menu item and visit this page.
Protecting Page Access
Finally, we must restrict access to this Users
page. For that, we will define a Gate. In the Gate, we will check if the user of the current tenant is the owner.
app/Providers/AppServiceProviders.php:
use App\Models\User;use Illuminate\Support\Facades\Gate; class AppServiceProvider extends ServiceProvider{ // ... public function boot(): void { Gate::define('manage-users', function (User $user) { return $user->tenants() ->wherePivot('tenant_id', $user->current_tenant_id) ->wherePivot('is_owner', true) ->exists(); }); }}
Next, we must use this Gate on the Route and navigation.
routes/web.php:
// ... Route::middleware('auth')->group(function () { Route::get('tenants/change/{tenantId}', \App\Http\Controllers\TenantController::class)->name('tenants.change'); Route::resource('tasks', \App\Http\Controllers\TaskController::class); Route::resource('projects', \App\Http\Controllers\ProjectController::class); Route::resource('users', \App\Http\Controllers\UserController::class) ->only('index', 'store') ->middleware('can:manage-users'); Route::get('/profile', [ProfileController::class, 'edit'])->name('profile.edit'); Route::patch('/profile', [ProfileController::class, 'update'])->name('profile.update'); Route::delete('/profile', [ProfileController::class, 'destroy'])->name('profile.destroy');}); require __DIR__.'/auth.php';
resources/views/layouts/navigation.blade.php:
// ... <!-- Navigation Links --><div class="hidden space-x-8 sm:-my-px sm:ms-10 sm:flex"> <x-nav-link :href="route('dashboard')" :active="request()->routeIs('dashboard')"> {{ __('Dashboard') }} </x-nav-link> <x-nav-link :href="route('tasks.index')" :active="request()->routeIs('tasks.*')"> {{ __('Tasks') }} </x-nav-link> <x-nav-link :href="route('projects.index')" :active="request()->routeIs('projects.*')"> {{ __('Projects') }} </x-nav-link> @can('manage-users') <x-nav-link :href="route('users.index')" :active="request()->routeIs('users.*')"> {{ __('Users') }} </x-nav-link> @endcan </div> // ...
If you tried to access the Users
page with a different user, you would get a "403 This Action is Unauthorized" error message.
I have run complete repository but showing 419 Page Expired. Is there anything missing from my side?
419 page expired usually means something is wrong with your session storage so it doesn't save session cookies as usual. Sorry hard to "blindly" debug it to tell you what can be wrong.
How Can I implement this concept of Multitenancy on the Login and Register Routes where the user is not yet exisiting.
If you apply globalscope and let model self::creating populate the db with user()->id, when accessing the login and register routes Laravel seems to fire the Memory overflow error.
PHP Fatal error : Allowed memory size of xxxxxx bytes executed (tried to allocated xxxx bytes) in vendor/laravel/framework/src/Illuminate/Database/Eloquent/builder.php on line 288
PHP Fatal error : Allowed memory size of xxxxxx bytes executed (tried to allocated xxxx bytes) in vendor/laravel/framework/src/Illuminate/Database/Eloquent/DatabaseManager.php on line 91
In the global scope, the best way is to probably check
if (auth()->check())
before applying the scope.Hi Povilas, I think you should add current_tenant_id when define Gate 'manage_users'?
Gate::define('manage_users', function(User $user) { $tenantId = $user->current_tenant_id; return $user->tenants()->where('id', $tenantId)->wherePivot('is_owner', true)->exists(); });
You're probably right, good catch!