Back to Course |
React.js + Inertia in Laravel 11: From Scratch

User Roles and Permissions Protection

In this lesson, permissions will be managed on the front and back end. We will again pass the permissions to React using the shared data Middleware.


Laravel Inertia Middleware

So, in the HandleInertiaRequests Middleware, you pass the permissions. Those permissions can come from some package or your own custom implementation. For example, let's add two permissions.

app/Http/Middleware/HandleInertiaRequests.php:

class HandleInertiaRequests extends Middleware
{
// ...
 
public function share(Request $request): array
{
return array_merge(parent::share($request), [
'flash' => [
'message' => fn () => $request->session()->get('message')
],
'user' => [
'name' => $request->user()?->name,
'email' => $request->user()?->email,
],
'permissions' => [
'posts_view' => true,
'posts_manage' => true,
],
]);
}
}

React Component Props

Next, we can add permissions as props instead of using the usePage().

resources/js/Pages/Posts/Index.jsx:

import AppLayout from '../../Layouts/AppLayout.jsx';
import { Head, Link, router, usePage } from '@inertiajs/react';
 
export default function PostsIndex({ posts }) {
export default function PostsIndex({ posts, permissions }) {
// ...
};

Using Permissions in React

Now, we can add a check for the Edit button.

resources/js/Pages/Posts/Index.jsx:

import AppLayout from '../../Layouts/AppLayout.jsx';
import { Head, Link, router, usePage } from '@inertiajs/react';
 
export default function PostsIndex({ posts, permissions }) {
const destroy = (id) => {
if (confirm('Are you sure?')) {
router.delete(route('posts.destroy', { id }));
}
}
 
return (
// ...
<td className="px-6 py-4 whitespace-no-wrap text-sm leading-5 text-gray-900">
{ permissions.posts_manage && (
<Link href={route('posts.edit', post.id)} className="mr-2 inline-block rounded-md bg-blue-500 px-3 py-2 text-xs font-semibold uppercase tracking-widest text-white shadow-sm">
Edit
</Link>
)}
<button onClick={() => destroy(post.id)} type="button" className="rounded-md bg-red-600 px-3 py-2 text-xs font-semibold uppercase tracking-widest text-white shadow-sm">
Delete
</button>
</td>
// ...
);
};

On the posts page, everything stayed the same.

But, if we set posts_manage to false, the edit button will be gone.

app/Http/Middleware/HandleInertiaRequests.php:

class HandleInertiaRequests extends Middleware
{
// ...
 
public function share(Request $request): array
{
return array_merge(parent::share($request), [
'flash' => [
'message' => fn () => $request->session()->get('message')
],
'user' => [
'name' => $request->user()?->name,
'email' => $request->user()?->email,
],
'permissions' => [
'posts_view' => true,
'posts_manage' => false,
],
]);
}
}

This is the principle. You pass the permissions from HandleInertiaRequests, and add the if-statements where you need to show/hide based on the permissions.

But this is only the front-end part. We also must protect the backend because what if someone guesses the URL?


Back-End Permission Protection

Let's add a simple hard-coded rule to the permissions: like, users with the ID of 1 and 2 can view the post but only users with the ID of 1 can manage the posts.

app/Http/Middleware/HandleInertiaRequests.php:

class HandleInertiaRequests extends Middleware
{
// ...
 
public function share(Request $request): array
{
return array_merge(parent::share($request), [
'flash' => [
'message' => fn () => $request->session()->get('message')
],
'user' => [
'name' => $request->user()?->name,
'email' => $request->user()?->email,
],
'permissions' => [
'posts_view' => true,
'posts_manage' => false,
'posts_view' => in_array(auth()->id(), [1, 2]),
'posts_manage' => auth()->id() === 1,
],
]);
}
}

And let's add checks for other buttons.

resources/js/Pages/Posts/Index.jsx:

import AppLayout from '../../Layouts/AppLayout.jsx';
import { Head, Link, router, usePage } from '@inertiajs/react';
 
export default function PostsIndex({ posts, permissions }) {
const destroy = (id) => {
if (confirm('Are you sure?')) {
router.delete(route('posts.destroy', { id }));
}
}
 
return (
<AppLayout>
<Head>
<title>Posts</title>
</Head>
 
<div>
{ permissions.posts_manage && (
<Link href={route('posts.create')} className="mb-4 inline-block rounded-md bg-blue-500 px-4 py-3 text-xs font-semibold uppercase tracking-widest text-white shadow-sm">
Add new post
</Link>
)}
 
<table className="min-w-full divide-y divide-gray-200 border">
<thead>
<tr>
<th className="px-6 py-3 bg-gray-50 text-left">
<span className="text-xs leading-4 font-medium text-gray-500 uppercase tracking-wider">ID</span>
</th>
<th className="px-6 py-3 bg-gray-50 text-left">
<span className="text-xs leading-4 font-medium text-gray-500 uppercase tracking-wider">Title</span>
</th>
<th className="px-6 py-3 bg-gray-50 text-left">
<span className="text-xs leading-4 font-medium text-gray-500 uppercase tracking-wider">Content</span>
</th>
<th className="px-6 py-3 bg-gray-50 text-left">
<span className="text-xs leading-4 font-medium text-gray-500 uppercase tracking-wider">Created At</span>
</th>
<th className="px-6 py-3 bg-gray-50 text-left">
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200 divide-solid">
{posts && posts && posts.map((post) => (
<tr key={post.id}>
<td className="px-6 py-4 whitespace-no-wrap text-sm leading-5 text-gray-900">
{post.id}
</td>
<td className="px-6 py-4 whitespace-no-wrap text-sm leading-5 text-gray-900">
{post.title}
</td>
<td className="px-6 py-4 whitespace-no-wrap text-sm leading-5 text-gray-900">
{post.content}
</td>
<td className="px-6 py-4 whitespace-no-wrap text-sm leading-5 text-gray-900">
{post.created_at}
</td>
<td className="px-6 py-4 whitespace-no-wrap text-sm leading-5 text-gray-900">
{ permissions.posts_manage && (
<Link href={route('posts.edit', post.id)} className="mr-2 inline-block rounded-md bg-blue-500 px-3 py-2 text-xs font-semibold uppercase tracking-widest text-white shadow-sm">
Edit
</Link>
)}
{ permissions.posts_manage && (
<button onClick={() => destroy(post.id)} type="button" className="rounded-md bg-red-600 px-3 py-2 text-xs font-semibold uppercase tracking-widest text-white shadow-sm">
Delete
</button>
)} /* [tl! ++] */
</td>
</tr>
))}In this lesson, permissions will be managed on the front and back end. We will again pass the permissions to React using the [shared data](https://inertiajs.com/shared-data) Middleware.
 
---
 
## Laravel Inertia Middleware
 
So, in the `HandleInertiaRequests` Middleware, you pass the permissions. Those permissions can come from some package or your own custom implementation. For example, let's add two permissions.
 
**app/Http/Middleware/HandleInertiaRequests.php**:
```php
class HandleInertiaRequests extends Middleware
{
// ...
 
public function share(Request $request): array
{
return array_merge(parent::share($request), [
'flash' => [
'message' => fn () => $request->session()->get('message')
],
'user' => [
'name' => $request->user()?->name,
'email' => $request->user()?->email,
],
'permissions' => [
'posts_view' => true,
'posts_manage' => true,
],
]);
}
}

React Component Props

Next, we can add permissions as props instead of using the usePage().

resources/js/Pages/Posts/Index.jsx:

import AppLayout from '../../Layouts/AppLayout.jsx';
import { Head, Link, router, usePage } from '@inertiajs/react';
 
export default function PostsIndex({ posts }) {
export default function PostsIndex({ posts, permissions }) {
// ...
};

Using Permissions in React

Now, we can add a check for the Edit button.

resources/js/Pages/Posts/Index.jsx:

import AppLayout from '../../Layouts/AppLayout.jsx';
import { Head, Link, router, usePage } from '@inertiajs/react';
 
export default function PostsIndex({ posts, permissions }) {
const destroy = (id) => {
if (confirm('Are you sure?')) {
router.delete(route('posts.destroy', { id }));
}
}
 
return (
// ...
<td className="px-6 py-4 whitespace-no-wrap text-sm leading-5 text-gray-900">
{ permissions.posts_manage && (
<Link href={route('posts.edit', post.id)} className="mr-2 inline-block rounded-md bg-blue-500 px-3 py-2 text-xs font-semibold uppercase tracking-widest text-white shadow-sm">
Edit
</Link>
)}
<button onClick={() => destroy(post.id)} type="button" className="rounded-md bg-red-600 px-3 py-2 text-xs font-semibold uppercase tracking-widest text-white shadow-sm">
Delete
</button>
</td>
// ...
);
};

On the posts page, everything stayed the same.

But, if we set posts_manage to false, the edit button will be gone.

app/Http/Middleware/HandleInertiaRequests.php:

class HandleInertiaRequests extends Middleware
{
// ...
 
public function share(Request $request): array
{
return array_merge(parent::share($request), [
'flash' => [
'message' => fn () => $request->session()->get('message')
],
'user' => [
'name' => $request->user()?->name,
'email' => $request->user()?->email,
],
'permissions' => [
'posts_view' => true,
'posts_manage' => false,
],
]);
}
}

This is the principle. You pass the permissions from HandleInertiaRequests, and add the if-statements where you need to show/hide based on the permissions.

But this is only the front-end part. We also must protect the backend because what if someone guesses the URL?


Back-End Permission Protection

Let's add a simple hard-coded rule to the permissions: like, users with the ID of 1 and 2 can view the post but only users with the ID of 1 can manage the posts.

app/Http/Middleware/HandleInertiaRequests.php:

class HandleInertiaRequests extends Middleware
{
// ...
 
public function share(Request $request): array
{
return array_merge(parent::share($request), [
'flash' => [
'message' => fn () => $request->session()->get('message')
],
'user' => [
'name' => $request->user()?->name,
'email' => $request->user()?->email,
],
'permissions' => [
'posts_view' => true,
'posts_manage' => false,
'posts_view' => in_array(auth()->id(), [1, 2]),
'posts_manage' => auth()->id() === 1,
],
]);
}
}

And let's add checks for other buttons.

resources/js/Pages/Posts/Index.jsx:

import AppLayout from '../../Layouts/AppLayout.jsx';
import { Head, Link, router, usePage } from '@inertiajs/react';
 
export default function PostsIndex({ posts, permissions }) {
const destroy = (id) => {
if (confirm('Are you sure?')) {
router.delete(route('posts.destroy', { id }));
}
}
 
return (
<AppLayout>
<Head>
<title>Posts</title>
</Head>
 
<div>
{ permissions.posts_manage && (
<Link href={route('posts.create')} className="mb-4 inline-block rounded-md bg-blue-500 px-4 py-3 text-xs font-semibold uppercase tracking-widest text-white shadow-sm">
Add new post
</Link>
)}
 
<table className="min-w-full divide-y divide-gray-200 border">
<thead>
<tr>
<th className="px-6 py-3 bg-gray-50 text-left">
<span className="text-xs leading-4 font-medium text-gray-500 uppercase tracking-wider">ID</span>
</th>
<th className="px-6 py-3 bg-gray-50 text-left">
<span className="text-xs leading-4 font-medium text-gray-500 uppercase tracking-wider">Title</span>
</th>
<th className="px-6 py-3 bg-gray-50 text-left">
<span className="text-xs leading-4 font-medium text-gray-500 uppercase tracking-wider">Content</span>
</th>
<th className="px-6 py-3 bg-gray-50 text-left">
<span className="text-xs leading-4 font-medium text-gray-500 uppercase tracking-wider">Created At</span>
</th>
<th className="px-6 py-3 bg-gray-50 text-left">
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200 divide-solid">
{posts && posts && posts.map((post) => (
<tr key={post.id}>
<td className="px-6 py-4 whitespace-no-wrap text-sm leading-5 text-gray-900">
{post.id}
</td>
<td className="px-6 py-4 whitespace-no-wrap text-sm leading-5 text-gray-900">
{post.title}
</td>
<td className="px-6 py-4 whitespace-no-wrap text-sm leading-5 text-gray-900">
{post.content}
</td>
<td className="px-6 py-4 whitespace-no-wrap text-sm leading-5 text-gray-900">
{post.created_at}
</td>
<td className="px-6 py-4 whitespace-no-wrap text-sm leading-5 text-gray-900">
{ permissions.posts_manage && (
<Link href={route('posts.edit', post.id)} className="mr-2 inline-block rounded-md bg-blue-500 px-3 py-2 text-xs font-semibold uppercase tracking-widest text-white shadow-sm">
Edit
</Link>
)}
{ permissions.posts_manage && (
<button onClick={() => destroy(post.id)} type="button" className="rounded-md bg-red-600 px-3 py-2 text-xs font-semibold uppercase tracking-widest text-white shadow-sm">
Delete
</button>
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
</AppLayout>
);
};

For the back-end, we need to implement, for example, Gates. But so that we don't have code duplication, let's move those permissions to the User Model as an Attribute.

app/Models/User.php:

class User extends Authenticatable
{
// ...
 
protected function permissions(): Attribute
{
return Attribute::make(
get: function () {
return [
'posts_view' => in_array($this->id, [1, 2]),
'posts_manage' => $this->id == 1,
];
}
);
}
}

app/Http/Middleware/HandleInertiaRequests.php:

class HandleInertiaRequests extends Middleware
{
// ...
 
public function share(Request $request): array
{
return array_merge(parent::share($request), [
'flash' => [
'message' => fn () => $request->session()->get('message')
],
'user' => [
'name' => $request->user()?->name,
'email' => $request->user()?->email,
],
'permissions' => [
'posts_view' => in_array(auth()->id(), [1, 2]),
'posts_manage' => auth()->id() === 1,
],
'permissions' => $request->user()?->permissions,
]);
}
}

In the Controller, we can use Policies.

php artisan make:policy PostPolicy --model=Post

In this example, we will use only the viewAny() and create() methods from the Policy. The Policy is registered automatically, so we don't need to register it manually.

app/Policies/PostPolicy.php:

class PostPolicy
{
public function viewAny(User $user): bool
{
return $user->permissions['posts_view'];
}
 
public function create(User $user): bool
{
return $user->permissions['posts_manage'];
}
}

Now, we can use this Policy in the Controller.

app/Http/Controllers/PostController.php:

use Illuminate\Support\Facades\Gate;
 
class PostController extends Controller
{
public function index(): Response
{
Gate::authorize('viewAny', Post::class);
 
$posts = PostResource::collection(Post::all());
 
return Inertia::render('Posts/Index', compact('posts'));
}
 
public function create(): Response
{
Gate::authorize('create', Post::class);
 
return Inertia::render('Posts/Create');
}
 
public function store(StorePostRequest $request): RedirectResponse
{
Gate::authorize('create', Post::class);
 
Post::create($request->validated());
 
return redirect()->route('posts.index')
->with('message', 'Post created successfully.');
}
 
public function edit(Post $post)
{
Gate::authorize('create', Post::class);
 
return Inertia::render('Posts/Edit', compact('post'));
}
 
public function update(Post $post, StorePostRequest $request)
{
Gate::authorize('create', Post::class);
 
$post->update($request->validated());
 
return redirect()->route('posts.index')
->with('message', 'Post updated successfully');
}
 
public function destroy(Post $post)
{
Gate::authorize('create', Post::class);
 
$post->delete();
 
return redirect()->route('posts.index')
->with('message', 'Post deleted successfully');
}
}

If the user even guesses the URL now, they won't be able to access those protected pages.


This is one way how to add permissions to your Inertia application.


You can find the source code for this course on GitHub.