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