DEV Community

Daud A. Mabena
Daud A. Mabena

Posted on

Integrating Spatie Laravel Permission with React for Role-Based Access Control

This tutorial will guide you through the process of setting up a robust role-based access control (RBAC) system using the Spatie Laravel Permission package for your Laravel backend and a React frontend. We will cover everything from initial setup to advanced features like real-time permission updates.

Introduction

In modern web applications, it is crucial to control what actions a user can perform. Role-based access control is a common and effective way to manage user permissions. The Spatie Laravel Permission package is a powerful and flexible tool for implementing RBAC in Laravel applications. When combined with a React frontend, it allows for a dynamic and secure user experience.

This tutorial will cover:

  • Backend Setup: Installing and configuring the Spatie Laravel Permission package.
  • API Endpoints: Creating secure API endpoints to manage roles and permissions.
  • Frontend Setup: Integrating React with your Laravel backend.
  • Role-Based Rendering: Conditionally rendering UI components in React based on user roles and permissions.
  • Real-Time Updates: Using WebSockets to instantly reflect permission changes in the frontend.

By the end of this tutorial, you will have a solid understanding of how to build a secure and scalable application with role-based access control using Laravel and React.

Part 1: Backend Setup with Laravel and Spatie Permission

In this section, we will set up our Laravel backend, install the Spatie Laravel Permission package, and configure it for our application.

1.1. Create a New Laravel Project

First, let's create a new Laravel project. Open your terminal and run the following command:

composer create-project --prefer-dist laravel/laravel laravel-react-permissions
cd laravel-react-permissions
Enter fullscreen mode Exit fullscreen mode

1.2. Set Up the Database

Next, you need to configure your database. Open the .env file and update the database credentials:

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=laravel_react_permissions
DB_USERNAME=root
DB_PASSWORD=
Enter fullscreen mode Exit fullscreen mode

After configuring the database, run the migrations:

php artisan migrate
Enter fullscreen mode Exit fullscreen mode

1.3. Install and Configure Spatie Laravel Permission

Now, let's install the Spatie Laravel Permission package via Composer:

composer require spatie/laravel-permission
Enter fullscreen mode Exit fullscreen mode

Publish the package's configuration and migration files:

php artisan vendor:publish --provider="Spatie\Permission\PermissionServiceProvider"
Enter fullscreen mode Exit fullscreen mode

This will publish the config/permission.php file and the necessary migration files. Now, run the migrations to create the roles and permissions tables:

php artisan migrate
Enter fullscreen mode Exit fullscreen mode

1.4. Set Up the User Model

To enable role and permission management for your User model, you need to add the HasRoles trait to it. Open app/Models/User.php and add the trait:

<?php

namespace App\Models;

use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens;
use Spatie\Permission\Traits\HasRoles;

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

    /**
     * The attributes that are mass assignable.
     *
     * @var array<int, string>
     */
    protected $fillable = [
        'name',
        'email',
        'password',
    ];

    /**
     * The attributes that should be hidden for serialization.
     *
     * @var array<int, string>
     */
    protected $hidden = [
        'password',
        'remember_token',
    ];

    /**
     * The attributes that should be cast.
     *
     * @var array<string, string>
     */
    protected $casts = [
        'email_verified_at' => 'datetime',
    ];
}
Enter fullscreen mode Exit fullscreen mode

Part 2: Creating API Endpoints for Roles and Permissions

In this section, we will create the necessary API endpoints to manage roles and permissions, and to get user data with their assigned roles and permissions.

2.1. Create Roles and Permissions

Let's create some roles and permissions for our application. You can do this in a seeder or directly in your code. For this tutorial, we will create a seeder.

First, create a new seeder:

php artisan make:seeder RolesAndPermissionsSeeder
Enter fullscreen mode Exit fullscreen mode

Now, open the seeder file in database/seeders/RolesAndPermissionsSeeder.php and add the following code:

<?php

namespace Database\Seeders;

use Illuminate\Database\Seeder;
use Spatie\Permission\Models\Role;
use Spatie\Permission\Models\Permission;

class RolesAndPermissionsSeeder extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        // Reset cached roles and permissions
        app()[
            \Spatie\Permission\PermissionRegistrar::class
        ]->forgetCachedPermissions();

        // create permissions
        Permission::create(["name" => "edit articles"]);
        Permission::create(["name" => "delete articles"]);
        Permission::create(["name" => "publish articles"]);
        Permission::create(["name" => "unpublish articles"]);

        // create roles and assign created permissions

        // this can be done as separate statements
        $role = Role::create(["name" => "writer"]);
        $role->givePermissionTo("edit articles");

        // or may be done by chaining
        $role = Role::create(["name" => "moderator"])->givePermissionTo([
            "publish articles",
            "unpublish articles",
        ]);

        $role = Role::create(["name" => "super-admin"]);
        $role->givePermissionTo(Permission::all());
    }
}

Enter fullscreen mode Exit fullscreen mode

Now, run the seeder:

php artisan db:seed --class=RolesAndPermissionsSeeder
Enter fullscreen mode Exit fullscreen mode

2.2. Create API Routes

Now, let's create the API routes for our application. Open routes/api.php and add the following code:

<?php

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\AuthController;

/*
|--------------------------------------------------------------------------
| API Routes
|--------------------------------------------------------------------------
|
| Here is where you can register API routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| is assigned the "api" middleware group. Enjoy building your API!
|
*/

Route::post("/register", [AuthController::class, "register"]);
Route::post("/login", [AuthController::class, "login"]);

Route::middleware("auth:sanctum")->get("/user", function (Request $request) {
    return $request->user();
});

Route::middleware("auth:sanctum")->group(function () {
    Route::get("/user-permissions", [AuthController::class, "userPermissions"]);
});
Enter fullscreen mode Exit fullscreen mode

2.3. Create AuthController

Now, let's create the AuthController to handle user registration, login, and getting user permissions. Create a new controller:

php artisan make:controller AuthController
Enter fullscreen mode Exit fullscreen mode

Open app/Http/Controllers/AuthController.php and add the following code:

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use App\Models\User;

class AuthController extends Controller
{
    public function register(Request $request)
    {
        $validatedData = $request->validate([
            "name" => "required|string|max:255",
            "email" => "required|string|email|max:255|unique:users",
            "password" => "required|string|min:8",
        ]);

        $user = User::create([
            "name" => $validatedData["name"],
            "email" => $validatedData["email"],
            "password" => Hash::make($validatedData["password"]),
        ]);

        $token = $user->createToken("auth_token")->plainTextToken;

        return response()->json([
            "access_token" => $token,
            "token_type" => "Bearer",
        ]);
    }

    public function login(Request $request)
    {
        if (!Auth::attempt($request->only("email", "password"))) {
            return response()->json([
                "message" => "Invalid login details",
            ], 401);
        }

        $user = User::where("email", $request["email"])->firstOrFail();

        $token = $user->createToken("auth_token")->plainTextToken;

        return response()->json([
            "access_token" => $token,
            "token_type" => "Bearer",
        ]);
    }

    public function userPermissions(Request $request)
    {
        return response()->json([
            "roles" => $request->user()->getRoleNames(),
            "permissions" => $request->user()->getAllPermissions()->pluck("name"),
        ]);
    }
}
Enter fullscreen mode Exit fullscreen mode

Part 3: Frontend Setup with React

In this section, we will set up our React frontend, connect it to the Laravel backend, and handle user authentication.

3.1. Create a New React App

First, let's create a new React app. Open a new terminal window and run the following command:

npx create-react-app react-permissions-frontend
cd react-permissions-frontend
Enter fullscreen mode Exit fullscreen mode

3.2. Install Dependencies

Next, we need to install some dependencies for our React app. We will use axios for making API requests and react-router-dom for routing.

npm install axios react-router-dom
Enter fullscreen mode Exit fullscreen mode

3.3. Create API Service

Let's create a simple API service to handle communication with our Laravel backend. Create a new file src/api.js and add the following code:

import axios from "axios";

const api = axios.create({
    baseURL: "http://localhost:8000/api",
    headers: {
        "Content-Type": "application/json",
    },
});

api.interceptors.request.use(
    (config) => {
        const token = localStorage.getItem("token");
        if (token) {
            config.headers["Authorization"] = `Bearer ${token}`;
        }
        return config;
    },
    (error) => {
        return Promise.reject(error);
    }
);

export default api;
Enter fullscreen mode Exit fullscreen mode

3.4. Create Auth Context

Now, let's create a React context to manage user authentication state and permissions. Create a new file src/AuthContext.js and add the following code:

import React, { createContext, useState, useContext } from "react";
import api from "./api";

const AuthContext = createContext();

export const AuthProvider = ({ children }) => {
    const [user, setUser] = useState(null);
    const [roles, setRoles] = useState([]);
    const [permissions, setPermissions] = useState([]);

    const login = async (email, password) => {
        const response = await api.post("/login", { email, password });
        localStorage.setItem("token", response.data.access_token);
        await getUser();
    };

    const register = async (name, email, password) => {
        await api.post("/register", { name, email, password });
    };

    const logout = () => {
        localStorage.removeItem("token");
        setUser(null);
        setRoles([]);
        setPermissions([]);
    };

    const getUser = async () => {
        const response = await api.get("/user");
        setUser(response.data);
        const permissionsResponse = await api.get("/user-permissions");
        setRoles(permissionsResponse.data.roles);
        setPermissions(permissionsResponse.data.permissions);
    };

    return (
        <AuthContext.Provider value={{ user, roles, permissions, login, register, logout, getUser }}>
            {children}
        </AuthContext.Provider>
    );
};

export const useAuth = () => useContext(AuthContext);
Enter fullscreen mode Exit fullscreen mode

3.5. Update App.js

Now, let's update our App.js to use the AuthProvider and set up our routes.

import React from "react";
import { BrowserRouter as Router, Routes, Route, Link } from "react-router-dom";
import { AuthProvider, useAuth } from "./AuthContext";
import Login from "./Login";
import Register from "./Register";

const Home = () => {
    const { user, roles, permissions, logout } = useAuth();

    return (
        <div>
            <h1>Welcome, {user ? user.name : "Guest"}</h1>
            {user ? (
                <div>
                    <p>Email: {user.email}</p>
                    <p>Roles: {roles.join(", ")}</p>
                    <p>Permissions: {permissions.join(", ")}</p>
                    <button onClick={logout}>Logout</button>
                </div>
            ) : (
                <div>
                    <Link to="/login">Login</Link> | <Link to="/register">Register</Link>
                </div>
            )}
        </div>
    );
};

const App = () => {
    return (
        <Router>
            <AuthProvider>
                <Routes>
                    <Route path="/" element={<Home />} />
                    <Route path="/login" element={<Login />} />
                    <Route path="/register" element={<Register />} />
                </Routes>
            </AuthProvider>
        </Router>
    );
};

export default App;
Enter fullscreen mode Exit fullscreen mode

3.6. Create Login and Register Components

Finally, let's create the Login and Register components. Create two new files, src/Login.js and src/Register.js.

src/Login.js

import React, { useState } from "react";
import { useAuth } from "./AuthContext";
import { useNavigate } from "react-router-dom";

const Login = () => {
    const [email, setEmail] = useState("");
    const [password, setPassword] = useState("");
    const { login } = useAuth();
    const navigate = useNavigate();

    const handleSubmit = async (e) => {
        e.preventDefault();
        await login(email, password);
        navigate("/");
    };

    return (
        <form onSubmit={handleSubmit}>
            <h2>Login</h2>
            <input type="email" value={email} onChange={(e) => setEmail(e.target.value)} placeholder="Email" />
            <input type="password" value={password} onChange={(e) => setPassword(e.target.value)} placeholder="Password" />
            <button type="submit">Login</button>
        </form>
    );
};

export default Login;
Enter fullscreen mode Exit fullscreen mode

src/Register.js

import React, { useState } from "react";
import { useAuth } from "./AuthContext";
import { useNavigate } from "react-router-dom";

const Register = () => {
    const [name, setName] = useState("");
    const [email, setEmail] = useState("");
    const [password, setPassword] = useState("");
    const { register } = useAuth();
    const navigate = useNavigate();

    const handleSubmit = async (e) => {
        e.preventDefault();
        await register(name, email, password);
        navigate("/login");
    };

    return (
        <form onSubmit={handleSubmit}>
            <h2>Register</h2>
            <input type="text" value={name} onChange={(e) => setName(e.target.value)} placeholder="Name" />
            <input type="email" value={email} onChange={(e) => setEmail(e.target.value)} placeholder="Email" />
            <input type="password" value={password} onChange={(e) => setPassword(e.target.value)} placeholder="Password" />
            <button type="submit">Register</button>
        </form>
    );
};

export default Register;
Enter fullscreen mode Exit fullscreen mode

Part 4: Role-Based Rendering in React

In this section, we will implement role-based rendering in our React frontend to show or hide UI components based on the user's roles and permissions.

4.1. Create a Protected Route Component

First, let's create a protected route component that checks if a user has the required permission to access a route. Create a new file src/ProtectedRoute.js and add the following code:

import React from "react";
import { Navigate } from "react-router-dom";
import { useAuth } from "./AuthContext";

const ProtectedRoute = ({ children, permission }) => {
    const { permissions } = useAuth();

    if (!permissions.includes(permission)) {
        return <Navigate to="/" />;
    }

    return children;
};

export default ProtectedRoute;
Enter fullscreen mode Exit fullscreen mode

4.2. Create a Can Component

Next, let's create a Can component that conditionally renders its children based on the user's permissions. Create a new file src/Can.js and add the following code:

import { useAuth } from "./AuthContext";

const Can = ({ children, permission }) => {
    const { permissions } = useAuth();

    if (!permissions.includes(permission)) {
        return null;
    }

    return children;
};

export default Can;
Enter fullscreen mode Exit fullscreen mode

4.3. Update App.js with Protected Routes and Components

Now, let's update our App.js to use the ProtectedRoute and Can components.

import React from "react";
import { BrowserRouter as Router, Routes, Route, Link } from "react-router-dom";
import { AuthProvider, useAuth } from "./AuthContext";
import Login from "./Login";
import Register from "./Register";
import ProtectedRoute from "./ProtectedRoute";
import Can from "./Can";

const Dashboard = () => <h2>Dashboard</h2>;
const Articles = () => <h2>Articles</h2>;

const Home = () => {
    const { user, roles, permissions, logout } = useAuth();

    return (
        <div>
            <h1>Welcome, {user ? user.name : "Guest"}</h1>
            {user ? (
                <div>
                    <p>Email: {user.email}</p>
                    <p>Roles: {roles.join(", ")}</p>
                    <p>Permissions: {permissions.join(", ")}</p>
                    <nav>
                        <Link to="/">Home</Link> | <Link to="/dashboard">Dashboard</Link> | <Link to="/articles">Articles</Link>
                    </nav>
                    <Can permission="edit articles">
                        <button>Edit Articles</button>
                    </Can>
                    <Can permission="delete articles">
                        <button>Delete Articles</button>
                    </Can>
                    <button onClick={logout}>Logout</button>
                </div>
            ) : (
                <div>
                    <Link to="/login">Login</Link> | <Link to="/register">Register</Link>
                </div>
            )}
        </div>
    );
};

const App = () => {
    return (
        <Router>
            <AuthProvider>
                <Routes>
                    <Route path="/" element={<Home />} />
                    <Route path="/login" element={<Login />} />
                    <Route path="/register" element={<Register />} />
                    <Route
                        path="/dashboard"
                        element={
                            <ProtectedRoute permission="view dashboard">
                                <Dashboard />
                            </ProtectedRoute>
                        }
                    />
                    <Route
                        path="/articles"
                        element={
                            <ProtectedRoute permission="view articles">
                                <Articles />
                            </ProtectedRoute>
                        }
                    />
                </Routes>
            </AuthProvider>
        </Router>
    );
};

export default App;
Enter fullscreen mode Exit fullscreen mode

Now, you need to add the view dashboard and view articles permissions to your seeder and assign them to the appropriate roles.

Part 5: Real-Time Permission Updates with WebSockets

In this section, we will implement real-time permission updates using WebSockets to instantly reflect any changes in user permissions on the frontend.

5.1. Install Pusher and Laravel Echo

First, let's install the necessary packages for broadcasting events from our Laravel backend.

composer require pusher/pusher-php-server
npm install --save-dev laravel-echo pusher-js
Enter fullscreen mode Exit fullscreen mode

5.2. Configure Broadcasting

Next, you need to configure broadcasting in your Laravel application. Open config/app.php and uncomment the App\Providers\BroadcastServiceProvider.

Then, open your .env file and configure the broadcast driver and Pusher credentials:

BROADCAST_DRIVER=pusher
PUSHER_APP_ID=your-pusher-app-id
PUSHER_APP_KEY=your-pusher-app-key
PUSHER_APP_SECRET=your-pusher-app-secret
PUSHER_APP_CLUSTER=mt1
Enter fullscreen mode Exit fullscreen mode

5.3. Create a Permission Changed Event

Now, let's create an event that will be broadcasted whenever a user's permissions change. Create a new event:

php artisan make:event UserPermissionsChanged
Enter fullscreen mode Exit fullscreen mode

Open app/Events/UserPermissionsChanged.php and add the following code:

<?php

namespace App\Events;

use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PresenceChannel;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
use App\Models\User;

class UserPermissionsChanged implements ShouldBroadcast
{
    use Dispatchable, InteractsWithSockets, SerializesModels;

    public $user;

    /**
     * Create a new event instance.
     *
     * @return void
     */
    public function __construct(User $user)
    {
        $this->user = $user;
    }

    /**
     * Get the channels the event should broadcast on.
     *
     * @return \Illuminate\Broadcasting\Channel|array
     */
    public function broadcastOn()
    {
        return new PrivateChannel("user." . $this->user->id);
    }
}
Enter fullscreen mode Exit fullscreen mode

5.4. Broadcast the Event

Now, you need to broadcast the UserPermissionsChanged event whenever a user's permissions are updated. You can do this in your AuthController or a dedicated service. For this tutorial, we will add a new method to our AuthController to update user permissions.

// app/Http/Controllers/AuthController.php

public function updateUserPermissions(Request $request, User $user)
{
    $validatedData = $request->validate([
        "permissions" => "required|array",
    ]);

    $user->syncPermissions($validatedData["permissions"]);

    event(new \App\Events\UserPermissionsChanged($user));

    return response()->json(["message" => "Permissions updated successfully"]);
}
Enter fullscreen mode Exit fullscreen mode

5.5. Listen for the Event in React

Finally, let's update our AuthContext.js to listen for the UserPermissionsChanged event and update the user's permissions in real-time.

// src/AuthContext.js

import Echo from "laravel-echo";
import Pusher from "pusher-js";

// ...

export const AuthProvider = ({ children }) => {
    // ...

    useEffect(() => {
        if (user) {
            const echo = new Echo({
                broadcaster: "pusher",
                key: "your-pusher-app-key",
                cluster: "mt1",
                encrypted: true,
            });

            echo.private(`user.${user.id}`).listen("UserPermissionsChanged", (e) => {
                getUser();
            });
        }
    }, [user]);

    // ...
};
Enter fullscreen mode Exit fullscreen mode

Now, whenever a user's permissions are updated in the backend, the UserPermissionsChanged event will be broadcasted, and the frontend will automatically fetch the updated permissions.

Conclusion

In this tutorial, you have learned how to integrate the Spatie Laravel Permission package with a React frontend to create a robust role-based access control system. You have also learned how to implement real-time permission updates using WebSockets.

By following this tutorial, you can build secure and scalable applications with granular control over user permissions. Now I have finished writing the tutorial. I will deliver it to the user.

Top comments (1)

Collapse
 
philbert_malulu_786f7595f profile image
philbert malulu

1🔥🔥🔥🔥🔥. best article of the year