Backend Development

Laravel Inertia React Roles Permissions CRUD

Laravel Inertia React Roles Permissions CRUD

In this post we will build CRUD pages for roles and permissions using Laravel framework and Inertia library.

 

 

Roles and permissions are essential parts on many web application dashboards. In this tutorial we will see how to build CRUD pages for roles and permissions in laravel. We will be using Inertia-js and React as our frontend along with laravel, however you can build this using Vue as well or normal blade views.

For css styling we will use tailwindcss.

 

Preparing Laravel Project

Let’s begin by creating a new laravel project using composer:

composer create-project laravel/laravel laravel_roles_permissions_crud

Launch the project:

php artisan serve

When opening the app url in the browser, you may encounter this error:

could not find driver

This error because laravel default db connection set to sqlite. So to resolve this error open .env and change the DB_CONNECTION to mysql and uncomment the db related env variables like so:

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=role_permission_crud
DB_USERNAME=root
DB_PASSWORD=root

Also in latest laravel versions the session_driver and cache_store is set to database, you may need to use file instead:

SESSION_DRIVER=file
CACHE_STORE=file

Be sure to create the db provided above in phpmyadmin.

 

Preparing and Installing Inertia

To use inertia you have to install the server and client side dependencies. First install inertia laravel by executing this command:

composer require inertiajs/inertia-laravel

Once installed publish the inertia middleware:

php artisan inertia:middleware

This command publish the HandleInertiaRequests middleware into the app/Http/Middleware/ directory. This middleware used for data sharing globally and contains version() method for versioning the assets.

Open bootstrap/app.php and append the HandleInertiaRequests middleware:

->withMiddleware(function (Middleware $middleware) {
        $middleware->web(append: [
           \App\Http\Middleware\HandleInertiaRequests::class
        ]);
    })

Next install the client side dependencies. As we will be using React with Inertia we will need the following dependencies:

npm install @inertiajs/react react react-dom @vitejs/plugin-react

Also install tailwindcss:

npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p

Open the tailwind.config.js and configure the template paths:

/** @type {import('tailwindcss').Config} */
export default {
  content: [
      "./resources/**/*.blade.php",
      "./resources/**/*.js",
      "./resources/**/*.vue",
      "./resources/**/*.jsx"
  ],
  theme: {
    extend: {},
  },
  plugins: [],
}

Include tailwindcss directives in resources/css/app.css

@tailwind base;
@tailwind components;
@tailwind utilities;

Next update vite.config.js to include the vite-react plugin:

import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin';
import react from "@vitejs/plugin-react";

export default defineConfig({
    plugins: [
        react(),
        laravel({
            input: ['resources/css/app.css', 'resources/js/app.jsx'],
            refresh: true,
        }),
    ],
});

 

Booting Inertia App

To boot the inertia app, create new file resources/js/app.jsx and add this code:

import './bootstrap';

import { createInertiaApp } from '@inertiajs/react'
import { createRoot } from 'react-dom/client'

createInertiaApp({
    resolve: name => {
        const pages = import.meta.glob('./Pages/**/*.jsx', { eager: true })
        return pages[`./Pages/${name}.jsx`]
    },
    setup({ el, App, props }) {
        createRoot(el).render(<App {...props} />)
    },
})

The resolve() callback suppose that we have a Pages/ directory where all pages will be located. The setup() callback mount the app to the dom.

Now compile the assets:

npm run dev

 

Root Template

If you opened HandleInertiaRequests middleware you will see this property:

protected $rootView = 'app';

This tells the root layout that will be used by inertia.Update it like so:

protected $rootView = 'layouts/frontend';

Then create these two layouts:

  • resources/views/layouts/app.blade.php
  • resources/views/layouts/frontend.blade.php

The app.blade.php will be used for normal blade pages like login, register etc. The frontend.blade.php layout will be used by inertia.

resources/views/app.blade.php

<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">

        <title>Roles Permissions CRUD</title>

        <!-- Fonts -->
        <link rel="preconnect" href="https://fonts.bunny.net">
        <link href="https://fonts.bunny.net/css?family=figtree:400,600&display=swap" rel="stylesheet" />
        
        @vite(['resources/css/app.css'])

    </head>
    <body class="font-sans antialiased dark:bg-black dark:text-white/50">
        <div class="bg-gray-50 text-black/50 dark:bg-black dark:text-white/50">
                    <header class="grid grid-cols-1 gap-2 py-5 px-4">

                        @if (Route::has('login'))
                            <nav class="-mx-3 flex flex-1 justify-end">
                                @auth
                                    <a
                                        href="{{ url('/roles') }}"
                                        class="rounded-md px-3 py-2 text-black ring-1 ring-transparent transition hover:text-black/70 focus:outline-none focus-visible:ring-[#FF2D20] dark:text-white dark:hover:text-white/80 dark:focus-visible:ring-white"
                                    >
                                        Roles
                                    </a>

                                    <a
                                        href="{{ url('/permissions') }}"
                                        class="rounded-md px-3 py-2 text-black ring-1 ring-transparent transition hover:text-black/70 focus:outline-none focus-visible:ring-[#FF2D20] dark:text-white dark:hover:text-white/80 dark:focus-visible:ring-white"
                                    >
                                        Permissions
                                    </a>
                                @else
                                    <a
                                        href="{{ route('login') }}"
                                        class="rounded-md px-3 py-2 text-black ring-1 ring-transparent transition hover:text-black/70 focus:outline-none focus-visible:ring-[#FF2D20] dark:text-white dark:hover:text-white/80 dark:focus-visible:ring-white"
                                    >
                                        Log in
                                    </a>

                                    @if (Route::has('register'))
                                        <a
                                            href="{{ route('register') }}"
                                            class="rounded-md px-3 py-2 text-black ring-1 ring-transparent transition hover:text-black/70 focus:outline-none focus-visible:ring-[#FF2D20] dark:text-white dark:hover:text-white/80 dark:focus-visible:ring-white"
                                        >
                                            Register
                                        </a>
                                    @endif
                                @endauth
                            </nav>
                        @endif
                    </header>

                    <main class="mt-6">
                        @yield('content')
                    </main>

                </div>
    </body>
</html>

resources/views/frontend.blade.php

<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">

    <title>Roles Permissions CRUD</title>

    <!-- Fonts -->
    <link rel="preconnect" href="https://fonts.bunny.net">
    <link href="https://fonts.bunny.net/css?family=figtree:400,600&display=swap" rel="stylesheet" />
    @vitereactrefresh
    @vite(['resources/css/app.css', 'resources/js/app.jsx'])
    @inertiaHead

</head>
<body class="font-sans antialiased dark:bg-black dark:text-white/50">
<div class="bg-gray-50 text-black/50 dark:bg-black dark:text-white/50">
            <header class="grid grid-cols-1 gap-2 py-10">

                @if (Route::has('login'))
                    <nav class="-mx-3 flex flex-1 justify-end">
                        @auth
                            <a
                                href="{{ url('/roles') }}"
                                class="rounded-md px-3 py-2 text-black ring-1 ring-transparent transition hover:text-black/70 focus:outline-none focus-visible:ring-[#FF2D20] dark:text-white dark:hover:text-white/80 dark:focus-visible:ring-white"
                            >
                                Roles
                            </a>

                            <a
                                href="{{ url('/permissions') }}"
                                class="rounded-md px-3 py-2 text-black ring-1 ring-transparent transition hover:text-black/70 focus:outline-none focus-visible:ring-[#FF2D20] dark:text-white dark:hover:text-white/80 dark:focus-visible:ring-white"
                            >
                                Permissions
                            </a>
                        @else
                            <a
                                href="{{ route('login') }}"
                                class="rounded-md px-3 py-2 text-black ring-1 ring-transparent transition hover:text-black/70 focus:outline-none focus-visible:ring-[#FF2D20] dark:text-white dark:hover:text-white/80 dark:focus-visible:ring-white"
                            >
                                Log in
                            </a>

                            @if (Route::has('register'))
                                <a
                                    href="{{ route('register') }}"
                                    class="rounded-md px-3 py-2 text-black ring-1 ring-transparent transition hover:text-black/70 focus:outline-none focus-visible:ring-[#FF2D20] dark:text-white dark:hover:text-white/80 dark:focus-visible:ring-white"
                                >
                                    Register
                                </a>
                            @endif
                        @endauth
                    </nav>
                @endif
            </header>

            <main class="mt-6">
                @inertia
            </main>

</div>
</body>
</html>

In the layouts i added links for navigating user to login and register pages in case user not authenticated otherwise we show two links to manage roles and permissions. These modules will be created later.

Also in the layouts you notice that i added the @vite directives that will load the js and css assets. Also for the second layout the inertia related directives @inertiaHead and @inertia directives.

 

Authentication

Before implementing the CRUD operations we have to authenticate the users first. For this we will laravel fortify package to handle authentication.

Install laravel fortify:

composer require laravel/fortify

Then publish fortify resources:

php artisan fortify:install

After running this command you will see FortifyServiceProvider.php in app/Providers/ directory and app/Actions/Fortify/ directory created.

Migrate the database:

php artisan migrate

Laravel fortify gives us the routes for authentication such as login but it doesn’t provide the views, for this we need to create the auth views.

So let’s create directory resources/views/auth. I created two views for login and register just for the purpose of this tutorial.

resources/views/auth/register.blade.php

@extends('layouts.app')

@section('content')

    <div class="mt-10 sm:mx-auto sm:w-full sm:max-w-sm">

        <h2 class="mt-10 text-center text-2xl font-bold leading-9 tracking-tight text-gray-900">Create Account</h2>

        @if ($errors->any())
            <div class="bg-red-400 text-red-800 rounded px-4 py-5">
                <ul>
                    @foreach ($errors->all() as $error)
                        <li>{{ $error }}</li>
                    @endforeach
                </ul>
            </div>
        @endif

        <form method="POST" class="space-y-6" action="{{ route('register') }}">
            @csrf

            <div>
                <label class="block text-sm font-medium leading-6 text-gray-900">{{ __('Name') }}</label>
                <input type="text" name="name" value="{{ old('name') }}" required autofocus class="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"/>
            </div>

            <div>
                <label class="block text-sm font-medium leading-6 text-gray-900">{{ __('Email') }}</label>
                <input type="text" name="email" value="{{ old('email') }}" required class="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"/>
            </div>

            <div>
                <label class="block text-sm font-medium leading-6 text-gray-900">{{ __('Password') }}</label>
                <input type="password" name="password" required autocomplete="current-password" class="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"/>
            </div>

            <div>
                <label class="block text-sm font-medium leading-6 text-gray-900">{{ __('Password Confirm') }}</label>
                <input type="password" name="password_confirmation" required class="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"/>
            </div>

                <a href="{{ route('login') }}" class="mt-8 text-sm underline">
                    {{ __('Already have account? Login') }}
                </a>

            <div>
                <button type="submit" class="flex w-full justify-center rounded-md bg-indigo-600 px-3 py-1.5 text-sm font-semibold leading-6 text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600">
                    {{ __('Register') }}
                </button>
            </div>
        </form>
    </div>
@endsection

resources/views/auth/login.blade.php

@extends('layouts.app')

@section('content')

    <div class="mt-10 sm:mx-auto sm:w-full sm:max-w-sm">

        <h2 class="mt-10 text-center text-2xl font-bold leading-9 tracking-tight text-gray-900">Sign in</h2>

        @if ($errors->any())
            <div class="bg-red-400 text-red-800 rounded px-4 py-5">
                <ul>
                    @foreach ($errors->all() as $error)
                        <li>{{ $error }}</li>
                    @endforeach
                </ul>
            </div>
        @endif

    <form method="POST" class="space-y-6" action="{{ route('login') }}">
        @csrf

        <div>
            <label class="block text-sm font-medium leading-6 text-gray-900">{{ __('Email') }}</label>
            <input type="email" name="email" value="{{ old('email') }}" required autofocus class="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"/>
        </div>

        <div>
            <label class="block text-sm font-medium leading-6 text-gray-900">{{ __('Password') }}</label>
            <input type="password" name="password" required autocomplete="current-password" class="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"/>
        </div>

        <div>
            <label class="block text-sm font-medium leading-6 text-gray-900">{{ __('Remember me') }}</label>
            <input type="checkbox" name="remember" />
        </div>

        <a href="{{ route('register') }}" class="mt-8 text-sm underline">
            {{ __('Create Account') }}
        </a>

        <div>
            <button type="submit" class="flex w-full justify-center rounded-md bg-indigo-600 px-3 py-1.5 text-sm font-semibold leading-6 text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600">
                {{ __('Login') }}
            </button>
        </div>
    </form>
    </div>
@endsection

I styled the forms using tailwindcss classes. In the register view i added the form to register user, the form action is set route(‘register’). This router is provided by fortify package.

In the same way the login view contains the login form which have an action provided by fortify which is set route(‘login’).

Let’s tell laravel fortify to use these views for login and register. This can be done in the FortifyServiceProvider::boot() method. add the below code at the beginning of the boot method.

app/Providers/FortifyServiceProvider.php

<?php

namespace App\Providers;

....
....
....

class FortifyServiceProvider extends ServiceProvider
{
    
    public function boot(): void
    {
        Fortify::loginView(function () {
            return view('auth.login');
        });

        Fortify::registerView(function() {
           return view('auth.register');
        });

        ....
        ....
        ....
    }
}

If you run the application and trying the register and login functionality you will see it work properly. 

You may need to configure the redirect url after successful login or register which can be done in fortify.php config file

config/fortify.php

'home' => '/',

Create the home view in resources/views/home.blade.php

@extends('layouts.app')

@section('content')
    <div class="mt-10 sm:mx-auto sm:w-full sm:max-w-sm">

        @if (session('status'))
            <div>
                {{ session('status') }}
            </div>
        @endif

        @guest
            <h4 class="text-2xl">Hello, <a href="{{route('login')}}">Sign in</a> </h4>
        @endguest

        @auth
                <h4 class="text-2xl">Hello {{auth()->user()->name}}, <a href="{{ route('logout') }}" onclick="event.preventDefault(); document.getElementById('frm-logout').submit();" class="underline text-blue-500">
                        Logout
                    </a>   </h4>

                <form id="frm-logout" action="{{ route('logout') }}" method="POST" style="display: none;">
                    {{ csrf_field() }}
                </form>
        @endauth
    </div>
@endsection

Add a route for this view

routes/web.php

Route::get('/', function () {
    return view('home');
});

 

Installing Laravel Permissions Package

After making the authentication functionality let’s move to implement the roles and permissions. We will use spatie/laravel-permission package as it provides the necessary Api for dealing with permissions.

Let’s install it first with composer:

composer require spatie/laravel-permission

Publish the migration and config file:

php artisan vendor:publish --provider="Spatie\Permission\PermissionServiceProvider"

Now you will see the migration is copied into database/migrations/ directory and config file config/permissions.php

Next clear the cache and run the migrations:

php artisan cache:clear
php artisan config:clear
php artisan migrate

Update the User model to include the HasRoles trait like so:

<?php

namespace App\Models;

....
....
use Spatie\Permission\Traits\HasRoles;

class User extends Authenticatable
{
    use HasFactory, Notifiable, HasRoles;

    ...
    ...
}

 

Laravel Inertia React Roles Permissions CRUD Part2

0 0 votes
Article Rating

What's your reaction?

Excited
0
Happy
0
Not Sure
0
Confused
0

You may also like

Subscribe
Notify of
guest

0 Comments
Inline Feedbacks
View all comments