Laravel 12 Multi-User-Rollenverwaltung – Implementierungsdokumentation

Diese Dokumentation beschreibt die Implementierung eines Multi-User-Rollenverwaltungssystems für eine Laravel 12-Anwendung mit Livewire. Alle Implementierungsschritte sind chronologisch dokumentiert.

1. Konzeption des Rollensystems

Für diese Anwendung wurden vier Rollen definiert:

  • Admin: Vollständiger Zugriff auf alle Funktionen, inkl. Rollenverwaltung
  • User: Standardbenutzer mit eingeschränkten Berechtigungen
  • Customer: Zugriff auf ein spezielles Kundendashboard
  • Company: Zugriff auf ein spezielles Unternehmens-Dashboard

2. Datenmodell erstellen

2.1 Role-Modell und Migration

php artisan make:model Role -m

Migration für Rollen anpassen:

// database/migrations/xxxx_xx_xx_create_roles_table.php
Schema::create('roles', function (Blueprint $table) {
    $table->id();
    $table->string('name');
    $table->string('slug')->unique();
    $table->string('description')->nullable();
    $table->timestamps();
});

2.2 Pivot-Tabelle für Benutzer-Rollen

php artisan make:migration create_role_user_table

Migration für Pivot-Tabelle anpassen:

// database/migrations/xxxx_xx_xx_create_role_user_table.php
Schema::create('role_user', function (Blueprint $table) {
    $table->id();
    $table->foreignId('user_id')->constrained()->onDelete('cascade');
    $table->foreignId('role_id')->constrained()->onDelete('cascade');
    $table->timestamps();

    $table->unique(['user_id', 'role_id']);
});

3. Modelle anpassen

3.1 Role-Modell erweitern:

// app/Models/Role.php
namespace AppModels;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;

class Role extends Model
{
    use HasFactory;

    protected $fillable = [
        'name',
        'slug',
        'description',
    ];

    public function users(): BelongsToMany
    {
        return $this->belongsToMany(User::class);
    }
}

3.2 User-Modell erweitern:

// app/Models/User.php (Ergänzungen)
// ...
use Illuminate\Database\Eloquent\Relations\BelongsToMany;

// ...

/**
 * Die Rollen, die dem Benutzer zugewiesen sind.
 */
public function roles(): BelongsToMany
{
    return $this->belongsToMany(Role::class);
}

/**
 * Prüfen, ob der Benutzer eine bestimmte Rolle hat.
 */
public function hasRole(string $role): bool
{
    return $this->roles()->where('slug', $role)->exists();
}

/**
 * Prüfen, ob der Benutzer eine der angegebenen Rollen hat.
 */
public function hasAnyRole(array $roles): bool
{
    return $this->roles()->whereIn('slug', $roles)->exists();
}

/**
 * Prüfen, ob der Benutzer alle angegebenen Rollen hat.
 */
public function hasAllRoles(array $roles): bool
{
    return $this->roles()->whereIn('slug', $roles)->count() === count($roles);
}

4. Seeder für Rollen und Benutzer erstellen

4.1 RoleSeeder

php artisan make:seeder RoleSeeder
// database/seeders/RoleSeeder.php
namespace DatabaseSeeders;

use AppModelsRole;
use Illuminate\Database\Seeder;

class RoleSeeder extends Seeder
{
    public function run(): void
    {
        $roles = [
            [
                'name' => 'Admin',
                'slug' => 'admin',
                'description' => 'Administrator mit vollen Berechtigungen',
            ],
            [
                'name' => 'User',
                'slug' => 'user',
                'description' => 'Standardbenutzer mit eingeschränkten Berechtigungen',
            ],
            [
                'name' => 'Customer',
                'slug' => 'customer',
                'description' => 'Kunde mit Zugang zum Kunden-Dashboard',
            ],
            [
                'name' => 'Company',
                'slug' => 'company',
                'description' => 'Unternehmen mit Zugang zum Unternehmens-Dashboard',
            ],
        ];

        foreach ($roles as $role) {
            Role::create($role);
        }
    }
}

4.2 UserSeeder

php artisan make:seeder UserSeeder
// database/seeders/UserSeeder.php
namespace Database\Seeders;

use App\Models\Role;
use App\Models\User;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\Hash;

class UserSeeder extends Seeder
{
    public function run(): void
    {
        // Testbenutzer mit bekanntem Passwort erstellen
        $password = Hash::make('password');

        // Admin-Benutzer erstellen
        $admin = User::create([
            'name' => 'Admin',
            'email' => 'admin@example.com',
            'password' => $password,
        ]);

        // Standard-Benutzer erstellen
        $user = User::create([
            'name' => 'User',
            'email' => 'user@example.com',
            'password' => $password,
        ]);

        // Customer-Benutzer erstellen
        $customer = User::create([
            'name' => 'Customer',
            'email' => 'customer@example.com',
            'password' => $password,
        ]);

        // Company-Benutzer erstellen
        $company = User::create([
            'name' => 'Company',
            'email' => 'company@example.com',
            'password' => $password,
        ]);

        // Rollen abrufen
        $adminRole = Role::where('slug', 'admin')->first();
        $userRole = Role::where('slug', 'user')->first();
        $customerRole = Role::where('slug', 'customer')->first();
        $companyRole = Role::where('slug', 'company')->first();

        // Rollen zuweisen
        if ($adminRole) $admin->roles()->attach($adminRole);
        if ($userRole) $user->roles()->attach($userRole);
        if ($customerRole) $customer->roles()->attach($customerRole);
        if ($companyRole) $company->roles()->attach($companyRole);
    }
}

5. Middleware für Rollenbeschränkungen

php artisan make:middleware CheckRole
// app/Http/Middleware/CheckRole.php
namespace AppHttpMiddleware;

use Closure;
use Illuminate\Http\Request;
use Symfony\Component\Http\Foundation\Response;

class CheckRole
{
    public function handle(Request $request, Closure $next, string ...$roles): Response
    {
        if (! $request->user()) {
            return redirect()->route('login');
        }

        if (empty($roles)) {
            return $next($request);
        }

        foreach ($roles as $role) {
            if ($request->user()->hasRole($role)) {
                return $next($request);
            }
        }

        abort(403, 'Unzureichende Berechtigungen.');
    }
}

5.1 Middleware registrieren

php artisan make:provider KernelServiceProvider
// app/Providers/KernelServiceProvider.php
namespace AppProviders;

use App\Http\Middleware\CheckRole;
use Illuminate\Contracts\Http\Kernel;
use Illuminate\Support\ServiceProvider;

class KernelServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        $this->app->bind('role.middleware', function () {
            return new CheckRole();
        });
    }

    public function boot(): void
    {
        $router = $this->app['router'];
        $router->aliasMiddleware('role', CheckRole::class);
    }
}

6. Livewire-Komponenten für die Dashboards

6.1 Admin-Dashboard

php artisan livewire:make AdminDashboard
// app/Livewire/AdminDashboard.php
namespace AppLivewire;

use App\Models\User;
use Illuminate\Support\Facades\Auth;
use LivewireComponent;

class AdminDashboard extends Component
{
    public function mount()
    {
        // Wir lassen die Middleware die Zugriffsrechte prüfen
    }

    public function render()
    {
        $users = User::with('roles')->get();
        $isAdmin = Auth::user()->hasRole('admin');

        return view('livewire.admin-dashboard', [
            'users' => $users,
            'isAdmin' => $isAdmin,
        ]);
    }
}

6.2 Customer-Dashboard

php artisan livewire:make CustomerDashboard
// app/Livewire/CustomerDashboard.php
namespace AppLivewire;

use Illuminate\Support\Facades\Auth;
use Livewire\Component;

class CustomerDashboard extends Component
{
    public function mount()
    {
        // Wir lassen die Middleware die Zugriffsrechte prüfen
    }

    public function render()
    {
        return view('livewire.customer-dashboard', [
            'user' => Auth::user(),
        ]);
    }
}

6.3 Company-Dashboard

php artisan livewire:make CompanyDashboard
// app/Livewire/CompanyDashboard.php
namespace AppLivewire;

use Illuminate\Support\Facades\Auth;
use Livewire\Component;

class CompanyDashboard extends Component
{
    public function mount()
    {
        // Wir lassen die Middleware die Zugriffsrechte prüfen
    }

    public function render()
    {
        return view('livewire.company-dashboard', [
            'user' => Auth::user(),
        ]);
    }
}

6.4 Rollenverwaltung

php artisan livewire:make RoleManagement
// app/Livewire/RoleManagement.php
namespace AppLivewire;

use App\Models\Role;
use App\Models\User;
use Illuminate\Support\Facade\Auth;
use Livewire\Component;
use Livewire\WithPagination;

class RoleManagement extends Component
{
    use WithPagination;

    public $selectedUser;
    public $roles = [];
    public $userRoles = [];
    public $search = '';

    protected $rules = [
        'userRoles' => 'required|array|min:1',
    ];

    public function mount()
    {
        // Wir lassen die Middleware die Zugriffsrechte prüfen
        $this->roles = Role::all();
    }

    public function selectUser($userId)
    {
        $this->selectedUser = User::with('roles')->find($userId);
        $this->userRoles = $this->selectedUser->roles->pluck('id')->toArray();
    }

    public function updateUserRoles()
    {
        $this->validate();

        if ($this->selectedUser) {
            $this->selectedUser->roles()->sync($this->userRoles);
            $this->selectedUser = User::with('roles')->find($this->selectedUser->id);

            session()->flash('message', 'Rollen erfolgreich aktualisiert.');
        }
    }

    public function render()
    {
        $users = User::with('roles')
            ->where('name', 'like', "%{$this->search}%")
            ->orWhere('email', 'like', "%{$this->search}%")
            ->paginate(10);

        return view('livewire.role-management', [
            'users' => $users,
        ]);
    }
}

7. Blade-Views für die Dashboards

7.1 Admin/User Dashboard

// resources/views/livewire/admin/admin-dashboard.blade.php
<div class="p-6 bg-white border-b border-gray-200 rounded-lg shadow-sm">
    <h1 class="text-2xl font-semibold mb-6">Admin/User Dashboard</h1>

    <div class="mb-8">
        <div class="grid grid-cols-1 md:grid-cols-3 gap-4">
            <div class="p-4 bg-blue-50 rounded-lg border border-blue-200">
                <h3 class="text-lg font-medium text-blue-800">Benutzer</h3>
                <p class="text-3xl font-bold">{{ $users->count() }}</p>
            </div>

            @if($isAdmin)
            <div class="p-4 bg-green-50 rounded-lg border border-green-200">
                <h3 class="text-lg font-medium text-green-800">Rollen</h3>
                <p class="text-3xl font-bold">{{ AppModelsRole::count() }}</p>
            </div>
            @endif

            <div class="p-4 bg-purple-50 rounded-lg border border-purple-200">
                <h3 class="text-lg font-medium text-purple-800">Meine Rolle</h3>
                <p class="text-xl font-semibold">
                    {{ auth()->user()->roles->pluck('name')->join(', ') }}
                </p>
            </div>
        </div>
    </div>

    <div class="mb-6">
        <h2 class="text-xl font-semibold mb-3">Benutzerübersicht</h2>
        <div class="overflow-x-auto">
            <table class="min-w-full bg-white border border-gray-200">
                <thead>
                    <tr>
                        <th class="px-4 py-2 text-left bg-gray-50 border-b">Name</th>
                        <th class="px-4 py-2 text-left bg-gray-50 border-b">Email</th>
                        <th class="px-4 py-2 text-left bg-gray-50 border-b">Rollen</th>
                    </tr>
                </thead>
                <tbody>
                    @foreach($users as $user)
                    <tr class="border-b hover:bg-gray-50">
                        <td class="px-4 py-2">{{ $user->name }}</td>
                        <td class="px-4 py-2">{{ $user->email }}</td>
                        <td class="px-4 py-2">
                            <div class="flex flex-wrap gap-1">
                                @foreach($user->roles as $role)
                                <span class="px-2 py-1 text-xs rounded-full 
                                    @if($role->slug === AppEnumsRoleType::ADMIN) bg-red-100 text-red-800 
                                    @elseif($role->slug === AppEnumsRoleType::USER) bg-blue-100 text-blue-800 
                                    @elseif($role->slug === AppEnumsRoleType::CUSTOMER) bg-green-100 text-green-800 
                                    @elseif($role->slug === AppEnumsRoleType::COMPANY) bg-purple-100 text-purple-800 
                                    @endif">
                                    {{ $role->name }}
                                </span>
                                @endforeach
                            </div>
                        </td>
                    </tr>
                    @endforeach
                </tbody>
            </table>
        </div>
    </div>

    @if($isAdmin)
    <div class="mt-8">
        <div class="flex items-center justify-between mb-4">
            <h2 class="text-xl font-semibold">Admin-Bereich</h2>
            <a href="{{ route('role.management') }}" class="px-4 py-2 bg-indigo-600 text-white rounded-md hover:bg-indigo-700">
                Rollen verwalten
            </a>
        </div>
        <div class="p-4 bg-yellow-50 border border-yellow-200 rounded-lg">
            <p class="text-yellow-800">
                Dieser Bereich ist nur für Administratoren sichtbar. Hier können Sie auf erweiterte Funktionen zugreifen.
            </p>
        </div>
    </div>
    @endif
</div>

7.2 Customer Dashboard

// resources/views/livewire/customer/customer-dashboard.blade.php
<div class="p-6 bg-white border-b border-gray-200 rounded-lg shadow-sm">
    <h1 class="text-2xl font-semibold mb-6">Kunden-Dashboard</h1>

    <div class="mb-8">
        <div class="p-6 bg-green-50 rounded-lg border border-green-200">
            <h2 class="text-xl font-medium text-green-800 mb-4">Willkommen, {{ $user->name }}!</h2>
            <p class="text-green-700 mb-3">
                Sie sind als <strong>Kunde</strong> angemeldet und haben Zugriff auf alle kundenbezogenen Funktionen und Informationen.
            </p>
        </div>
    </div>

    <div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8">
        <!-- Kundenspezifischer Inhalt -->
    </div>
</div>

7.3 Company Dashboard

// resources/views/livewire/company/company-dashboard.blade.php
<div class="p-6 bg-white border-b border-gray-200 rounded-lg shadow-sm">
    <h1 class="text-2xl font-semibold mb-6">Unternehmens-Dashboard</h1>

    <div class="mb-8">
        <div class="p-6 bg-purple-50 rounded-lg border border-purple-200">
            <h2 class="text-xl font-medium text-purple-800 mb-4">Willkommen, {{ $user->name }}!</h2>
            <p class="text-purple-700 mb-3">
                Sie sind als <strong>Unternehmen</strong> angemeldet und haben Zugriff auf alle unternehmensbezogenen Funktionen und Informationen.
            </p>
        </div>
    </div>

    <div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
        <!-- Unternehmensspezifischer Inhalt -->
    </div>
</div>

7.4 Rollenverwaltungsansicht

// resources/views/livewire/admin/role-management.blade.php
<div class="p-6 bg-white border-b border-gray-200 rounded-lg shadow-sm">
    <h1 class="text-2xl font-semibold mb-6">Rollenverwaltung</h1>

    @if(session()->has('message'))
    <div class="mb-4 p-4 bg-green-50 border border-green-200 text-green-800 rounded-lg">
        {{ session('message') }}
    </div>
    @endif

    <div class="mb-6">
        <div class="flex items-center justify-between mb-4">
            <h2 class="text-xl font-semibold">Benutzer</h2>
            <div class="flex">
                <input type="text" wire:model.debounce.300ms="search" class="px-4 py-2 border rounded-l-md focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500" 
                    placeholder="Suche nach Name oder E-Mail...">
                <button class="px-4 py-2 bg-indigo-600 text-white rounded-r-md hover:bg-indigo-700">
                    <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
                        <path fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z" clip-rule="evenodd" />
                    </svg>
                </button>
            </div>
        </div>

        <!-- Detaillierte Benutzer- und Rollentabelle mit RoleType-Konstanten -->
    </div>
</div>

8. RoleType-Enum für Rollenkonstanten

Um Hardcoding von Rollenbezeichnungen zu vermeiden, wurde ein Enum für die Rollenkonstanten erstellt:

// app/Enums/RoleType.php
namespace App\Enums;

class RoleType
{
    public const ADMIN = 'admin';
    public const USER = 'user';
    public const CUSTOMER = 'customer';
    public const COMPANY = 'company';
}

Diese Konstanten werden in den Views und im PHP-Code für konsistente Rollenreferenzen verwendet.

9. Routen definieren

// routes/web.php
<?php

use App\Enums\RoleType;
use App\Livewire\Admin\AdminDashboard;
use App\Livewire\Admin\RoleManagement;
use App\Livewire\Company\CompanyDashboard;
use App\Livewire\Customer\CustomerDashboard;
use Illuminate\Support\Facades\Route;
use App\Models\User;

// Öffentliche Dashboard-Route als Fallback
Route::view('dashboard', 'dashboard')
    ->middleware(['auth', 'verified'])
    ->name('dashboard');

// Rollenbasierte Dashboards mit RoleType-Konstanten
Route::middleware(['auth', 'role:'.RoleType::ADMIN.','.RoleType::USER])->group(function () {
    Route::get('/admin/dashboard', AdminDashboard::class)->name('admin.dashboard');
});

Route::middleware(['auth', 'role:'.RoleType::ADMIN])->group(function () {
    Route::get('/admin/roles', RoleManagement::class)->name('role.management');
});

Route::middleware(['auth', 'role:'.RoleType::CUSTOMER])->group(function () {
    Route::get('/customer/dashboard', CustomerDashboard::class)->name('customer.dashboard');
});

Route::middleware(['auth', 'role:'.RoleType::COMPANY])->group(function () {
    Route::get('/company/dashboard', CompanyDashboard::class)->name('company.dashboard');
});

// Nach dem Login zur richtigen Dashboard-Seite umleiten
Route::get('/dashboard-redirect', function() {
    $user = auth()->user();

    // Benutzer-Rollen-Cache aktualisieren, um sicherzustellen, dass wir aktuelle Daten haben
    $user = User::with('roles')->find($user->id);

    if (!$user || $user->roles->isEmpty()) {
        // Wenn Benutzer keine Rollen hat, zum Standard-Dashboard weiterleiten
        return redirect()->route('dashboard');
    }

    // Überprüfe, ob der Benutzer bestimmte Rollen hat
    $roles = $user->roles->pluck('slug')->toArray();

    if (in_array(RoleType::ADMIN, $roles) || in_array(RoleType::USER, $roles)) {
        return redirect()->route('admin.dashboard');
    } elseif (in_array(RoleType::CUSTOMER, $roles)) {
        return redirect()->route('customer.dashboard');
    } elseif (in_array(RoleType::COMPANY, $roles)) {
        return redirect()->route('company.dashboard');
    }

    // Fallback, wenn keine passende Rolle gefunden wurde
    return redirect()->route('dashboard');
})->middleware('auth')->name('dashboard.redirect');

10. Login-Weiterleitung anpassen

// app/Livewire/Auth/Login.php (Anpassung)
public function login(): void
{
    $this->validate();

    $this->ensureIsNotRateLimited();

    if (! Auth::attempt(['email' => $this->email, 'password' => $this->password], $this->remember)) {
        RateLimiter::hit($this->throttleKey());

        throw ValidationException::withMessages([
            'email' => __('auth.failed'),
        ]);
    }

    RateLimiter::clear($this->throttleKey());
    Session::regenerate();

    $this->redirectIntended(default: route('dashboard.redirect', absolute: false), navigate: true);
}

11. Datenbank-Migration und Seeding

# Migration durchführen
php artisan migrate:fresh

# Rollen erstellen
php artisan db:seed --class=RoleSeeder

# Benutzer erstellen und Rollen zuweisen
php artisan db:seed --class=UserSeeder

12. Fehlerbehebung bei Weiterleitungsschleifen

Bei der Implementierung traten Weiterleitungsschleifen auf, die durch folgende Maßnahmen behoben wurden:

  1. Entfernung redundanter Zugriffskontrollen:
    • Redundante Prüfungen in den mount()-Methoden der Livewire-Komponenten wurden entfernt.
    • Die Zugriffskontrollen wurden vollständig der Middleware überlassen.
  2. Optimierung der KernelServiceProvider:
    • Die automatische Anwendung der CheckRole-Middleware auf alle Routen wurde entfernt, um Weiterleitungsschleifen zu vermeiden.
  3. Verbesserung der Dashboard-Weiterleitung:
    • Die Rollenerkennung wurde verbessert, um sicherzustellen, dass die Benutzer zum richtigen Dashboard weitergeleitet werden.
    • Ein expliziter Fallback für Benutzer ohne Rollen wurde hinzugefügt.

13. Testbenutzerzugänge

Nach der Implementierung stehen die folgenden Benutzer zur Verfügung:

  1. Admin
    • E-Mail: admin@example.com
    • Passwort: password
    • Rolle: admin
  2. User
    • E-Mail: user@example.com
    • Passwort: password
    • Rolle: user
  3. Customer
    • E-Mail: customer@example.com
    • Passwort: password
    • Rolle: customer
  4. Company
    • E-Mail: company@example.com
    • Passwort: password
    • Rolle: company

Fazit

Das implementierte Multi-User-Rollenverwaltungssystem ermöglicht eine differenzierte Zugriffskontrolle für verschiedene Benutzertypen, jeweils mit eigenen Dashboards und angepassten Funktionen. Die Implementierung nutzt Laravel 12 mit Livewire für die Frontend-Komponenten und sorgt für eine klare Trennung der Benutzerrollen.