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! 🚀

LIVE MENTORSHIP ONLY 5 SPOTS

Laravel Mastery
Coaching Class Program

KritiMyantra

Transform from beginner to Laravel expert with our personalized Coaching Class starting June 20, 2025. Limited enrollment ensures focused attention.

Daily Sessions

1-hour personalized coaching

Real Projects

Build portfolio applications

Best Practices

Industry-standard techniques

Career Support

Interview prep & job guidance

Total Investment
$200
Duration
30 hours
1h/day

Enrollment Closes In

Days
Hours
Minutes
Seconds
Spots Available 5 of 10 remaining
Next cohort starts:
June 20, 2025

Join the Program

Complete your application to secure your spot

Application Submitted!

Thank you for your interest in our Laravel mentorship program. We'll contact you within 24 hours with next steps.

What happens next?

  • Confirmation email with program details
  • WhatsApp message from our team
  • Onboarding call to discuss your goals

Tags

Comments

No comments yet. Be the first to comment!

Please log in to post a comment:

Sign in with Google

Related Posts