Implementing Roles & Permissions in Laravel 12 with React.js: A Comprehensive Guide

Author

Kritim Yantra

Jun 08, 2025

Implementing Roles & Permissions in Laravel 12 with React.js: A Comprehensive Guide

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.

Why Roles and Permissions Matter

In modern applications, different users need different levels of access:

  • Admins manage everything
  • Editors create and modify content
  • Viewers only see information
  • Custom roles for specialized tasks

Implementing this properly ensures security and provides a better user experience.


Prerequisites

Before we begin, ensure you have:

  • PHP (>= 8.1) & Composer installed
  • Node.js & npm
  • Laravel 12 installed (composer create-project laravel/laravel laravel-roles-react)
  • Basic knowledge of Laravel and React.js

Step 1: Setup Laravel 12 with API Authentication

1.1 Install Laravel and Configure API

composer create-project laravel/laravel laravel-roles-react
cd laravel-roles-react
php artisan api:install

This creates:

  • routes/api.php for API routes
  • Installs Laravel Sanctum (simpler than Passport for basic token auth)

1.2 Setup Database

Configure your .env file:

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=laravel_roles
DB_USERNAME=root
DB_PASSWORD=

1.3 Migrate Database

php artisan migrate

Step 2: Implement Roles and Permissions

We'll use the popular spatie/laravel-permission package.

2.1 Install Package

composer require spatie/laravel-permission

2.2 Publish Configuration

php artisan vendor:publish --provider="Spatie\Permission\PermissionServiceProvider"

2.3 Add Trait to User Model

app/Models/User.php

use Spatie\Permission\Traits\HasRoles;

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

2.4 Create Role and Permission Seeders

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']);
}

2.5 Run Migrations with Seeders

php artisan migrate:fresh --seed

Step 3: Create API Endpoints

3.1 Update AuthController

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()
    ]);
}

3.2 Create Middleware for Permissions

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);
}

3.3 Register Middleware in Kernel

bootstrap/app.php

->withMiddleware(function ($middleware) {
    $middleware->alias([
        'permission' => \App\Http\Middleware\CheckPermission::class,
    ]);
})

3.4 Create Protected Routes

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');
});

Step 4: Set Up React.js Frontend

4.1 Install React and Dependencies

npm install react react-dom @vitejs/plugin-react axios react-router-dom
npm install @heroicons/react

4.2 Configure Vite

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(),
    ],
});

4.3 Create React Components Structure

resources/js/
├── components/
│   ├── Dashboard.jsx
│   ├── Login.jsx
│   ├── Register.jsx
│   ├── Navbar.jsx
│   ├── PostList.jsx
│   └── PermissionGate.jsx
├── context/
│   └── AuthContext.jsx
├── App.jsx
└── main.jsx

4.4 Create Auth Context

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);

4.5 Create PermissionGate Component

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;

4.6 Create Dashboard with Role-Based UI

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;

4.7 Create Registration Form with Role Selection

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;

Step 5: Testing the Application

  1. Start Laravel backend:
php artisan serve
  1. Start React frontend:
npm run dev
  1. Visit http://localhost:8000 and test:
  • Register with different roles
  • Login and see role-specific UI
  • Test permission-based access
  • Verify admin-only features

Best Practices for Production

  1. Rate Limiting: Protect your API endpoints
  2. Validation: Validate all incoming requests
  3. Caching: Cache roles and permissions
  4. Audit Logs: Track permission changes
  5. UI Feedback: Show permission errors gracefully
  6. Refresh Tokens: Implement token refresh mechanism

Conclusion

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:

  • Add role management UI
  • Implement permission groups
  • Add multi-tenancy support
  • Create a permission assignment interface

"Permissions are the gatekeepers of your application's functionality. Implement them thoughtfully and your application will be both secure and user-friendly."

Happy coding! 🚀

Tags

Comments

No comments yet. Be the first to comment!

Please log in to post a comment:

Sign in with Google

Related Posts