Implementing Roles & Permissions in Laravel Using Inertia: A Guide with Breeze/Jetstream

Laravel Pro Tips
9 min readDec 18, 2023

--

Laravel offers beginner-friendly tools like Breeze and Jetstream. However, they don’t come with built-in features for roles and permissions. So, how do you add these features when you’re using the Vue Inertia versions? Let’s dive in!

But, before we add roles and permissions, we should set up our project. This includes getting our data structures in place and setting up Inertia for creating, reading, updating, and deleting data. Let’s take it one step at a time.

Initial Step: Setting Up Breeze with Inertia

Beginning with Laravel Breeze is a great way to set up your Laravel project. While we’ll initially focus on the Breeze setup, we’ll also give a nod to Jetstream towards the article’s end, as their implementation processes share similarities.

Before we delve into Breeze, ensure you have a fresh Laravel project ready to go.

Next, add Breeze to your project using the following command:

composer require laravel/breeze --dev

Once Breeze is installed, it’s time to scaffold it using the Vue version. Run these commands to achieve that:

php artisan breeze:install vue
npm run dev

Setup Phase 2: Creating a Basic Tasks Management System

To effectively manage permissions, we’ll first set up a basic Tasks CRUD (Create, Read, Update, Delete) system. Here’s how you can get this up and running smoothly in your Laravel project with Inertia.

Kickstart the CRUD setup by generating a model named Task alongside its corresponding migrations and controller using this command:

php artisan make:model Task -mc

Next, design the structure of our tasks table. Here, we're adding just a 'description' field:

public function up()
{
Schema::create('tasks', function (Blueprint $table) {
$table->id();
$table->text('description');
$table->timestamps();
});
}

Ensure that you can assign a value to the description field by updating the $fillable property in the Task model:

// Located at: app/Models/Task.php
protected $fillable = ['description'];

In our controller, we’ll cover all standard CRUD operations. What distinguishes this from traditional Laravel setup is our use of Inertia for rendering:

// Located at: app/Http/Controllers/TasksController.php
class TasksController extends Controller
{
public function index()
{
$tasks = Task::all();
return Inertia::render('Tasks/Index', ['tasks' => $tasks]);
}
public function create()
{
return Inertia::render('Tasks/Create');
}
public function store(StoreTaskRequest $request)
{
Task::create($request->validated());
return redirect()->route('tasks.index');
}
public function edit(Task $task)
{
return Inertia::render('Tasks/Edit', ['task' => $task]);
}
public function update(UpdateTaskRequest $request, Task $task)
{
$task->update($request->validated());
return redirect()->route('tasks.index');
}
public function destroy(Task $task)
{
$task->delete();
return redirect()->route('tasks.index');
}
}

To handle data validation and create a functional user interface for our task management system, we’ll leverage Laravel’s Form Requests and Vue components. Let’s break down the steps for setting this up.

To ensure data integrity, set up a simple rule for validating task descriptions:

public function rules(): array
{
return [
'description' => ['required', 'string'],
];
}

Proceed by structuring the frontend. Create a Tasks directory within resources/js/Pages and add three Vue files to manage the task's CRUD operations:

  • Index.vue
  • Create.vue
  • Edit.vue

Let’s first define the file to display all tasks:

resources/js/Pages/Tasks/Index.vue

<script setup>
import { Head, Link, Inertia } from '@inertiajs/inertia-vue3';
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout.vue';
import BreezeButton from '@/Components/PrimaryButton.vue';const props = defineProps({
tasks: Object
})
function destroy(id) {
Inertia.delete(route('tasks.destroy', id), {
onBefore: () => confirm('Are you sure?')
});
}
</script>
<template>
<!-- (Layout code for displaying tasks is here) -->
</template>

Here’s how to define the Vue file for creating a new task:

resources/js/Pages/Tasks/Create.vue

<script setup>
import { Head, useForm } from '@inertiajs/inertia-vue3';
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout.vue';
import TextInput from '@/Components/TextInput.vue';
import InputLabel from '@/Components/InputLabel.vue';
import PrimaryButton from '@/Components/PrimaryButton.vue';const form = useForm({
description: '',
});
const submit = () => {
form.post(route('tasks.store'));
};
</script>
<template>
<!-- (Layout code for adding a task is here) -->
</template>

Finally, set up the Vue file to edit tasks:

resources/js/Pages/Tasks/Edit.vue

<script setup>
import { Head, useForm } from '@inertiajs/inertia-vue3';
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout.vue';
import TextInput from '@/Components/TextInput.vue';
import InputLabel from '@/Components/InputLabel.vue';
import PrimaryButton from '@/Components/PrimaryButton.vue';const props = defineProps({
task: {
type: Object,
}
})
const form = useForm({
id: props.task.id,
description: props.task.description,
});
const submit = () => {
form.patch(route('tasks.update', form.id));
};
</script>
<template>
<!-- (Layout code for editing a task is here) -->
</template>

Building Role and Permission Management in Laravel: Models and Migrations

Now that we have our CRUD operations in place, let’s dive into roles and permissions. In this guide, we’ll develop the backend logic for roles and permissions from scratch, without relying on external packages like Spatie Permission. We’ll create the necessary data structures and relationships to efficiently manage permissions.

To start, we’ll generate models and migrations for roles and permissions. Both models will have a single ‘title’ field to store the name of the role or permission.

php artisan make:model Role -m
php artisan make:model Permission -m

In the Role model (app/Models/Role.php), specify the fillable property:

// app/Models/Role.php
protected $fillable = ['title'];

In the corresponding migration file, include the ‘title’ field:

// database/migrations/xxxx_xx_xx_create_roles_table.php
public function up()
{
Schema::create('roles', function (Blueprint $table) {
$table->id();
$table->string('title');
$table->timestamps();
});
}

Roles can possess multiple permissions. Set up this relationship in the Role model:

// app/Models/Role.php
public function permissions(): BelongsToMany
{
return $this->belongsToMany(Permission::class);
}

To manage the many-to-many relationship between roles and permissions, create a pivot table:

php artisan make:migration create_permission_role_table

In the migration file, define the table schema:

// database/migrations/xxxx_xx_xx_create_permission_role_table.php
public function up()
{
Schema::create('permission_role', function (Blueprint $table) {
$table->foreignId('permission_id')->constrained();
$table->foreignId('role_id')->constrained();
});
}

Users can hold multiple roles. Create another pivot table to manage this many-to-many relationship:

php artisan make:migration create_role_user_table

Define the table schema in the migration:

// database/migrations/xxxx_xx_xx_create_role_user_table.php
public function up()
{
Schema::create('role_user', function (Blueprint $table) {
$table->foreignId('role_id')->constrained();
$table->foreignId('user_id')->constrained();
});
}

In the User model (app/Models/User.php), establish the relationship:

// app/Models/User.php
public function roles(): BelongsToMany
{
return $this->belongsToMany(Role::class);
}

Seeding Data for User Roles and Permissions in Laravel: Backend Setup

Now, we’ll learn how to seed data for testing user roles and permissions in Laravel. We’ll create two users with different roles (admin and user), define roles, assign roles to users, define permissions, and link permissions to the respective roles.

Let’s begin by creating two users, one with an admin role and the other with a user role.

// database/seeders/UsersTableSeeder.php
public function run()
{
$users = [
[
'id' => 1,
'name' => 'Admin',
'email' => 'admin@admin.com',
'password' => bcrypt('password'),
'remember_token' => null,
],
[
'id' => 2,
'name' => 'User',
'email' => 'user@user.com',
'password' => bcrypt('password'),
'remember_token' => null,
],
];
User::insert($users);
}

Now, let’s create roles and associate them with users.

// database/seeders/RolesTableSeeder.php
public function run()
{
$roles = [
[
'id' => 1,
'title' => 'Admin',
],
[
'id' => 2,
'title' => 'User',
],
];
Role::insert($roles);
}

We’ll set up the relationship between users and roles.

// database/seeders/RoleUserTableSeeder.php
User::findOrFail(1)->roles()->sync(1);
User::findOrFail(2)->roles()->sync(2);

Let’s define the permissions we want to assign to roles.

// database/seeders/PermissionsTableSeeder.php
public function run()
{
$permissions = [
[
'id' => 1,
'title' => 'task_create',
],
[
'id' => 2,
'title' => 'task_edit',
],
[
'id' => 3,
'title' => 'task_destroy',
],
];
Permission::insert($permissions);
}

Finally, we’ll associate permissions with roles. Admin will have all three permissions, while the user role will only have ‘task_create’ permission.

// database/seeders/PermissionRoleTableSeeder.php
Role::findOrFail(1)->permissions()->sync([1, 2, 3]);
Role::findOrFail(2)->permissions()->sync([1]);

Defining Laravel Gates Based on Permissions: Backend Implementation

Now, we’ll learn how to register permissions and define Laravel Gates based on these permissions. We’ll create a method called registerUserAccessToGates() in the app/Providers/AuthServiceProvider.php file and call it during bootstrapping. This method will dynamically define Gates for each permission, providing fine-grained access control.

In your app/Providers/AuthServiceProvider.php file, add a new method named registerUserAccessToGates(). This method will be called during the bootstrapping process.

// app/Providers/AuthServiceProvider.php
protected function registerUserAccessToGates()
{
try {
foreach (Permission::pluck('title') as $permission) {
Gate::define($permission, function ($user) use ($permission) {
return $user->roles()->whereHas('permissions', function ($q) use ($permission) {
$q->where('title', $permission);
})->count() > 0;
});
}
} catch (\Exception $e) {
info('registerUserAccessToGates: Database not found or not yet migrated. Ignoring user permissions while booting app.');
}
}

The code above dynamically defines Gates for all available permissions. It iterates through each permission in your database and associates it with a Gate. Here’s how it works:

  • For each permission in the database, it defines a Gate.
  • The Gate checks if the user has a role that is linked to the permission.
  • If the user has a role with the required permission, the Gate grants access.

Implementing Roles and Permissions in Laravel with Breeze and Inertia: Frontend Integration

Before using permissions in your Vue components, you need to pass permission data to the front-end. Inertia provides shared data through Laravel middleware called HandleInertiaRequests. Here's how to add a new 'can' key to 'auth' and pass all user permissions:

'auth' => [
'user' => $request->user(),
'can' => $request->user()?->loadMissing('roles.permissions')
->roles->flatMap(function ($role) {
return $role->permissions;
})->map(function ($permission) {
return [$permission['title'] => auth()->user()->can($permission['title'])];
})->collapse()->all(),
],

This code dynamically fetches the user’s permissions and structures them into a JSON format like this:

{
"task_create": true,
"task_edit": true,
"task_destroy": true
}

With the permissions data available in the frontend, we can now prevent certain buttons from showing if the user doesn’t have the required permission. Let’s take an example of a “Create Task” button and conditionally render it based on the ‘task_create’ permission.

In your resources/js/Pages/Tasks/Index.vue file:

<div class="mb-4" v-if="$page.props.auth.can.task_create">
<Link :href="route('tasks.create')" class="bg-green-500 hover:bg-green-700 text-white border border-transparent font-bold px-4 py-2 text-xs uppercase tracking-widest rounded-md">
Create
</Link>
</div>

Similarly, you can apply the same approach to edit and delete buttons. Here’s an example for the edit and delete buttons:

<Link v-if="$page.props.auth.can.task_edit" :href="route('tasks.edit', task)" class="bg-green-500 hover:bg-green-700 text-white border border-transparent font-bold px-4 py-2 text-xs uppercase tracking-widest rounded-md">
Edit
</Link>
<PrimaryButton v-if="$page.props.auth.can.task_destroy" @click="destroy(task.id)">
Delete
</PrimaryButton>

With these conditional checks in place, if a user doesn’t have permission for a specific action, the corresponding button won’t be displayed on the page.

Securing Backend Controllers with Roles and Permissions in Laravel

Now, In your app/Http/Controllers/TasksController.php, we'll add authorization checks to each method using the $this->authorize() method. This will restrict access based on the user's permissions.

// app/Http/Controllers/TasksController.php
public function index()
{
$tasks = Task::all();
return Inertia::render('Tasks/Index', [
'tasks' => $tasks,
'can' => [
'createTask' => auth()->user()->can('task_create'),
'editTask' => auth()->user()->can('task_edit'),
'destroyTask' => auth()->user()->can('task_destroy'),
],
]);
}
public function create()
{
$this->authorize('task_create');
return Inertia::render('Tasks/Create');
}
public function store(StoreTaskRequest $request)
{
$this->authorize('task_create');
Task::create($request->validated());
return redirect()->route('tasks.index');
}
public function edit(Task $task)
{
$this->authorize('task_edit');
return Inertia::render('Tasks/Edit', compact('task'));
}
public function update(UpdateTaskRequest $request, Task $task)
{
$this->authorize('task_edit');
$task->update($request->validated());
return redirect()->route('tasks.index');
}
public function destroy(Task $task)
{
$this->authorize('task_destroy');
$task->delete();
return redirect()->route('tasks.index');
}

Implementing Roles and Permissions with Laravel Jetstream

Just like with Breeze, after creating a new Laravel project, we’ll start by installing Jetstream:

composer require laravel/jetstream

Then, we’ll use Jetstream scaffolding with the Vue version:

php artisan jetstream:install inertia
npm run dev

For Jetstream, all the backend models, migrations, and controllers are set up similarly to what we did with Breeze. The main difference lies in the JavaScript part and how we handle permissions.

In the app/Http/Middleware/HandleInertiaRequests.php file, you won't find the user array like you did with Breeze. Instead, we'll directly add the can variable to the share() method:

public function share(Request $request): array
{
return array_merge(parent::share($request), [
'can' => auth()->user()->loadMissing('roles.permissions')
->roles->flatMap(function ($role) {
return $role->permissions;
})->map(function ($permission) {
return [$permission['title'] => auth()->user()->can($permission['title'])];
})->collapse()->all(),
]);
}

In the JavaScript part, the only difference is that instead of $page.props.auth.can, we'll use $page.props.can. The auth part is no longer present.

So, in your Vue files, you can check permissions like this:

<!-- resources/js/Pages/Tasks/Index.vue -->
<div class="mb-4" v-if="$page.props.can.task_create">
<Link :href="route('tasks.create')" class="bg-green-500 hover:bg-green-700 text-white border border-transparent font-bold px-4 py-2 text-xs uppercase tracking-widest rounded-md">
Create
</Link>
</div>

You can apply the same approach to the edit and delete buttons:

<!-- resources/js/Pages/Tasks/Index.vue -->
<Link v-if="$page.props.can.task_edit" :href="route('tasks.edit', task)" class="bg-green-500 hover:bg-green-700 text-white border border-transparent font-bold px-4 py-2 text-xs uppercase tracking-widest rounded-md">
Edit
</Link>
<PrimaryButton v-if="$page.props.can.task_destroy" @click="destroy(task.id)">
Delete
</PrimaryButton>

Unlock the secrets to mastering Laravel and supercharge your development skills! This guide, crafted by seasoned experts, is your golden ticket to bypass common roadblocks and accelerate your career growth. With this treasure trove of Laravel wisdom, you’re not just buying an eBook; you’re investing in a leap forward. Two years of professional advancement await at your fingertips. Make the smart move — transform your Laravel expertise with just one click. Get Your Copy Now!

--

--