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! 🚀
Transform from beginner to Laravel expert with our personalized Coaching Class starting June 20, 2025. Limited enrollment ensures focused attention.
1-hour personalized coaching
Build portfolio applications
Industry-standard techniques
Interview prep & job guidance
Complete your application to secure your spot
Thank you for your interest in our Laravel mentorship program. We'll contact you within 24 hours with next steps.
No comments yet. Be the first to comment!
Please log in to post a comment:
Sign in with Google