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

Tenant Owner: Manage Users - Menu and Permission

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.