Kritim Yantra
Jun 08, 2025
In this comprehensive tutorial, we'll build a role-based access control (RBAC) system using Laravel 12 and React.js. This powerful combination allows you to create enterprise-grade applications with sophisticated permission systems.
In modern applications, different users need different levels of access:
Implementing this properly ensures security and provides a better user experience.
Before we begin, ensure you have:
composer create-project laravel/laravel laravel-roles-react
)composer create-project laravel/laravel laravel-roles-react
cd laravel-roles-react
php artisan api:install
This creates:
routes/api.php
for API routesConfigure your .env
file:
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=laravel_roles
DB_USERNAME=root
DB_PASSWORD=
php artisan migrate
We'll use the popular spatie/laravel-permission package.
composer require spatie/laravel-permission
php artisan vendor:publish --provider="Spatie\Permission\PermissionServiceProvider"
app/Models/User.php
use Spatie\Permission\Traits\HasRoles;
class User extends Authenticatable
{
use HasApiTokens, HasFactory, Notifiable, HasRoles;
}
php artisan make:seeder RolePermissionSeeder
database/seeders/RolePermissionSeeder.php
use Spatie\Permission\Models\Role;
use Spatie\Permission\Models\Permission;
public function run()
{
// Reset cached roles and permissions
app()[\Spatie\Permission\PermissionRegistrar::class]->forgetCachedPermissions();
// Create permissions
$permissions = [
'view posts',
'create posts',
'edit posts',
'delete posts',
'manage users',
'manage settings'
];
foreach ($permissions as $permission) {
Permission::create(['name' => $permission]);
}
// Create roles and assign permissions
$admin = Role::create(['name' => 'admin']);
$admin->givePermissionTo(Permission::all());
$editor = Role::create(['name' => 'editor']);
$editor->givePermissionTo(['view posts', 'create posts', 'edit posts']);
$viewer = Role::create(['name' => 'viewer']);
$viewer->givePermissionTo(['view posts']);
}
php artisan migrate:fresh --seed
app/Http/Controllers/AuthController.php
public function register(Request $request)
{
$request->validate([
'name' => 'required|string',
'email' => 'required|email|unique:users',
'password' => 'required|min:8|confirmed',
'role' => 'required|in:admin,editor,viewer' // Add role validation
]);
$user = User::create([
'name' => $request->name,
'email' => $request->email,
'password' => Hash::make($request->password),
]);
// Assign role to user
$user->assignRole($request->role);
$token = $user->createToken('authToken')->plainTextToken;
return response()->json(['token' => $token, 'user' => $user], 201);
}
public function user(Request $request)
{
$user = $request->user();
// Load roles and permissions
$user->load('roles', 'permissions');
return response()->json([
'user' => $user,
'permissions' => $user->getAllPermissions()->pluck('name'),
'roles' => $user->getRoleNames()
]);
}
php artisan make:middleware CheckPermission
app/Http/Middleware/CheckPermission.php
public function handle(Request $request, Closure $next, $permission)
{
if (!$request->user()->hasPermissionTo($permission)) {
return response()->json(['error' => 'Unauthorized'], 403);
}
return $next($request);
}
bootstrap/app.php
->withMiddleware(function ($middleware) {
$middleware->alias([
'permission' => \App\Http\Middleware\CheckPermission::class,
]);
})
routes/api.php
Route::middleware('auth:sanctum')->group(function () {
// User info with permissions
Route::get('/user', [AuthController::class, 'user']);
// Protected routes with permissions
Route::get('/posts', [PostController::class, 'index'])->middleware('permission:view posts');
Route::post('/posts', [PostController::class, 'store'])->middleware('permission:create posts');
Route::put('/posts/{id}', [PostController::class, 'update'])->middleware('permission:edit posts');
Route::delete('/posts/{id}', [PostController::class, 'destroy'])->middleware('permission:delete posts');
// Admin-only route
Route::get('/users', [UserController::class, 'index'])->middleware('permission:manage users');
});
npm install react react-dom @vitejs/plugin-react axios react-router-dom
npm install @heroicons/react
vite.config.js
import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [
laravel(['resources/js/app.jsx']),
react(),
],
});
resources/js/
├── components/
│ ├── Dashboard.jsx
│ ├── Login.jsx
│ ├── Register.jsx
│ ├── Navbar.jsx
│ ├── PostList.jsx
│ └── PermissionGate.jsx
├── context/
│ └── AuthContext.jsx
├── App.jsx
└── main.jsx
resources/js/context/AuthContext.jsx
import { createContext, useContext, useState, useEffect } from 'react';
import axios from 'axios';
const AuthContext = createContext();
export const AuthProvider = ({ children }) => {
const [user, setUser] = useState(null);
const [permissions, setPermissions] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchUser = async () => {
try {
const token = localStorage.getItem('token');
if (token) {
axios.defaults.headers.common['Authorization'] = `Bearer ${token}`;
const { data } = await axios.get('/api/user');
setUser(data.user);
setPermissions(data.permissions);
}
} catch (error) {
console.error('Session check failed:', error);
logout();
} finally {
setLoading(false);
}
};
fetchUser();
}, []);
const login = async (credentials) => {
const { data } = await axios.post('/api/login', credentials);
localStorage.setItem('token', data.token);
axios.defaults.headers.common['Authorization'] = `Bearer ${data.token}`;
setUser(data.user);
setPermissions(data.permissions);
return data.user;
};
const register = async (userData) => {
const { data } = await axios.post('/api/register', userData);
localStorage.setItem('token', data.token);
axios.defaults.headers.common['Authorization'] = `Bearer ${data.token}`;
setUser(data.user);
setPermissions(data.permissions);
return data.user;
};
const logout = () => {
localStorage.removeItem('token');
delete axios.defaults.headers.common['Authorization'];
setUser(null);
setPermissions([]);
};
const hasPermission = (requiredPermission) => {
return permissions.includes(requiredPermission);
};
return (
<AuthContext.Provider value={{
user,
permissions,
loading,
login,
register,
logout,
hasPermission
}}>
{children}
</AuthContext.Provider>
);
};
export const useAuth = () => useContext(AuthContext);
resources/js/components/PermissionGate.jsx
import { useAuth } from '../context/AuthContext';
const PermissionGate = ({ children, permissions }) => {
const { hasPermission } = useAuth();
const hasRequiredPermissions = permissions.every(perm => hasPermission(perm));
return hasRequiredPermissions ? children : null;
};
export default PermissionGate;
resources/js/components/Dashboard.jsx
import { useAuth } from '../context/AuthContext';
import PermissionGate from './PermissionGate';
const Dashboard = () => {
const { user, logout } = useAuth();
if (!user) return <div>Loading...</div>;
return (
<div className="max-w-4xl mx-auto p-6">
<h1 className="text-3xl font-bold mb-6">Welcome, {user.name}!</h1>
<p className="text-lg mb-4">
Your role: <span className="font-semibold">{user.roles[0]}</span>
</p>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
<PermissionGate permissions={['view posts']}>
<div className="bg-white p-6 rounded-lg shadow-md">
<h2 className="text-xl font-semibold mb-4">Posts Management</h2>
<p className="mb-4">Manage all posts in the system</p>
<button className="bg-blue-500 text-white px-4 py-2 rounded">
View Posts
</button>
</div>
</PermissionGate>
<PermissionGate permissions={['create posts', 'edit posts']}>
<div className="bg-white p-6 rounded-lg shadow-md">
<h2 className="text-xl font-semibold mb-4">Content Editor</h2>
<p className="mb-4">Create and edit content</p>
<button className="bg-green-500 text-white px-4 py-2 rounded">
Create Post
</button>
</div>
</PermissionGate>
<PermissionGate permissions={['manage users']}>
<div className="bg-white p-6 rounded-lg shadow-md">
<h2 className="text-xl font-semibold mb-4">User Administration</h2>
<p className="mb-4">Manage all system users</p>
<button className="bg-purple-500 text-white px-4 py-2 rounded">
Manage Users
</button>
</div>
</PermissionGate>
</div>
<div className="mt-8">
<h2 className="text-2xl font-bold mb-4">Your Permissions</h2>
<div className="flex flex-wrap gap-2">
{user.permissions.map(permission => (
<span
key={permission}
className="bg-gray-200 px-3 py-1 rounded-full text-sm"
>
{permission}
</span>
))}
</div>
</div>
<button
onClick={logout}
className="mt-8 bg-red-500 text-white px-6 py-2 rounded-lg hover:bg-red-600"
>
Logout
</button>
</div>
);
};
export default Dashboard;
resources/js/components/Register.jsx
import { useState } from 'react';
import { useAuth } from '../context/AuthContext';
const Register = () => {
const { register } = useAuth();
const [formData, setFormData] = useState({
name: '',
email: '',
password: '',
password_confirmation: '',
role: 'viewer'
});
const [error, setError] = useState('');
const handleChange = (e) => {
setFormData({ ...formData, [e.target.name]: e.target.value });
};
const handleSubmit = async (e) => {
e.preventDefault();
try {
await register(formData);
} catch (err) {
setError(err.response?.data?.message || 'Registration failed');
}
};
return (
<div className="max-w-md mx-auto mt-16 p-6 bg-white rounded-lg shadow-md">
<h2 className="text-2xl font-bold mb-6 text-center">Create Account</h2>
{error && (
<div className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4">
{error}
</div>
)}
<form onSubmit={handleSubmit}>
<div className="mb-4">
<label className="block text-gray-700 mb-2" htmlFor="name">
Full Name
</label>
<input
type="text"
id="name"
name="name"
value={formData.name}
onChange={handleChange}
className="w-full px-3 py-2 border rounded-lg"
required
/>
</div>
<div className="mb-4">
<label className="block text-gray-700 mb-2" htmlFor="email">
Email Address
</label>
<input
type="email"
id="email"
name="email"
value={formData.email}
onChange={handleChange}
className="w-full px-3 py-2 border rounded-lg"
required
/>
</div>
<div className="mb-4">
<label className="block text-gray-700 mb-2" htmlFor="password">
Password
</label>
<input
type="password"
id="password"
name="password"
value={formData.password}
onChange={handleChange}
className="w-full px-3 py-2 border rounded-lg"
required
/>
</div>
<div className="mb-4">
<label className="block text-gray-700 mb-2" htmlFor="password_confirmation">
Confirm Password
</label>
<input
type="password"
id="password_confirmation"
name="password_confirmation"
value={formData.password_confirmation}
onChange={handleChange}
className="w-full px-3 py-2 border rounded-lg"
required
/>
</div>
<div className="mb-6">
<label className="block text-gray-700 mb-2">
Account Type
</label>
<div className="grid grid-cols-3 gap-4">
{['admin', 'editor', 'viewer'].map((role) => (
<div key={role} className="flex items-center">
<input
type="radio"
id={role}
name="role"
value={role}
checked={formData.role === role}
onChange={handleChange}
className="mr-2"
/>
<label htmlFor={role} className="capitalize">
{role}
</label>
</div>
))}
</div>
</div>
<button
type="submit"
className="w-full bg-blue-500 text-white py-2 rounded-lg hover:bg-blue-600"
>
Register
</button>
</form>
</div>
);
};
export default Register;
php artisan serve
npm run dev
http://localhost:8000
and test:You've now built a robust role-based access control system with:
✅ Laravel 12 API backend
✅ React.js frontend
✅ spatie/laravel-permission integration
✅ Permission-gated components
✅ Role-based registration
This architecture scales well for enterprise applications and provides a solid foundation for implementing complex permission systems.
Next Steps:
"Permissions are the gatekeepers of your application's functionality. Implement them thoughtfully and your application will be both secure and user-friendly."
Happy coding! 🚀
No comments yet. Be the first to comment!
Please log in to post a comment:
Sign in with Google