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
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=
After configuring the database, run the migrations:
php artisan migrate
1.3. Install and Configure Spatie Laravel Permission
Now, let's install the Spatie Laravel Permission package via Composer:
composer require spatie/laravel-permission
Publish the package's configuration and migration files:
php artisan vendor:publish --provider="Spatie\Permission\PermissionServiceProvider"
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
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',
];
}
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
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());
}
}
Now, run the seeder:
php artisan db:seed --class=RolesAndPermissionsSeeder
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"]);
});
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
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"),
]);
}
}
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
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
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;
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);
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;
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;
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;
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;
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;
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;
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
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
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
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);
}
}
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"]);
}
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]);
// ...
};
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)
1🔥🔥🔥🔥🔥. best article of the year