16 posts 65 tags 7 domains

Filament 5 + Laravel 12: userzy, role i uprawnienia z Filament Shield

Kompletny setup zarządzania użytkownikami i uprawnieniami w panelu Filament — instalacja, generowanie permissionów, typowe pułapki.

Problem

Świeżo postawiony panel Filament 5 na Laravel 12 nie ma żadnego sensownego mechanizmu zarządzania użytkownikami ani uprawnieniami. Trzeba ogarnąć:

  • model ról i permissionów
  • generowanie permissionów per Resource (żeby nie pisać tego ręcznie)
  • własny UserResource z poprawnym hashowaniem hasła
  • typowe problemy: znikająca zakładka w sidebarze, niemożność zalogowania nowego usera

Stack: Laravel 12 + PHP 8 + Filament 5 + Spatie Permission + Filament Shield.

Instalacja

bash
composer require spatie/laravel-permission
composer require bezhansalleh/filament-shield

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

php artisan vendor:publish --tag="filament-shield-config"
php artisan shield:setup --fresh
php artisan shield:install admin

Argument admin w ostatniej komendzie to ID panelu (nie ścieżka URL). Shield doda plugin do AdminPanelProvider, utworzy RoleResource i przygotuje rolę super_admin.

Model User

php
use Spatie\Permission\Traits\HasRoles;
use Filament\Models\Contracts\FilamentUser;
use Filament\Panel;

class User extends Authenticatable implements FilamentUser
{
    use HasRoles;

    public function canAccessPanel(Panel $panel): bool
    {
        return $this->hasAnyRole(['super_admin', 'admin', 'editor']);
    }
}

canAccessPanel() to pierwsza linia obrony — bez odpowiedniej roli user nie wejdzie do panelu w ogóle.

Generowanie permissionów — najważniejsza komenda

Po stworzeniu każdego nowego Resource'a uruchom:

bash
php artisan shield:generate --all

Komenda wygeneruje permissiony typu view_product, view_any_product, create_product, update_product, delete_product, restore_product, force_delete_product + odpowiednie Policy. Potem w panelu Shield przypisujesz je do ról przez UI.

Super admin

bash
php artisan shield:super-admin

Komenda zapyta o usera i nada mu rolę super_admin, która przez Gate::before ma dostęp do wszystkiego — pomija policy w ogóle.

UserResource

Filament nie generuje go automatycznie:

bash
php artisan make:filament-resource User --generate

W formularzu dodaj pole do ról i hasło:

php
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;

Select::make('roles')
    ->multiple()
    ->relationship('roles', 'name')
    ->preload()
    ->searchable(),

TextInput::make('password')
    ->password()
    ->dehydrated(fn ($state) => filled($state))
    ->required(fn (string $context) => $context === 'create'),

Laravel 12 ma w app/Models/User.php cast 'password' => 'hashed' od razu po laravel new — hasło hashuje się automatycznie przy zapisie. Nie dodawaj dehydrateStateUsing(Hash::make(...)), bo zhashujesz hash i logowanie przestanie działać.

dehydrated(fn ($state) => filled($state)) mówi Filamentowi: "jeśli pole jest puste przy edycji, nie wysyłaj go do bazy". Bez tego edycja usera bez wpisywania nowego hasła nadpisze hasło pustym stringiem.

Sprawdzanie uprawnień w kodzie

php
// Resource / Action / kontroler
if ($user->can('update_product')) { ... }
if ($user->hasRole('editor')) { ... }

// Blade
@can('delete_product')
    <button>Usuń</button>
@endcan

Resource'y Filamenta automatycznie respektują policy wygenerowane przez Shielda — jeśli user nie ma delete_product, przycisk Delete sam znika z tabeli.

Ograniczanie Actions

Każda Action ma ->visible() i ->authorize():

php
use Filament\Actions\Action;

protected function getHeaderActions(): array
{
    return [
        // prosta widoczność
        Action::make('export')
            ->visible(fn () => auth()->user()->hasRole('admin')),

        // przez permission
        Action::make('importCsv')
            ->visible(fn () => auth()->user()->can('import_products')),

        // przez Policy (rzuca 403 przy obejściu)
        Action::make('delete')
            ->authorize('delete_product'),
    ];
}

visible() — prosty boolean, dobry do UI. authorize() — przechodzi przez Gate, blokuje nawet bezpośrednie wywołania Livewire. Używaj dla destrukcyjnych operacji.

Panel admina pod /

Domyślnie panel siedzi pod /admin. Żeby przenieść na root, w AdminPanelProvider:

php
public function panel(Panel $panel): Panel
{
    return $panel
        ->default()
        ->id('admin')
        ->path('')              // puste = panel na /
        ->login()
        ->plugin(FilamentShieldPlugin::make());
}

I posprzątaj routes/web.php — usuń domyślny Route::get('/', fn () => view('welcome')), bo będzie kolidował.

Cache po zmianach — druga ważna komenda

Jeśli po shield:generate --all zakładka dalej się nie pojawia, sidebar nie odświeża się, role nie działają:

bash
php artisan optimize:clear
php artisan filament:clear-cached-components
php artisan permission:cache-reset

Plus twardy refresh przeglądarki (Ctrl+Shift+R), bo Filament cache'uje też assety po stronie klienta.

Diagnostyka — gdy coś nie działa

Nie widzę zakładki w sidebarze

bash
ls app/Filament/Resources/        # czy plik istnieje?
php artisan shield:generate --all  # wygeneruj permissiony
php artisan shield:super-admin     # zrób z siebie super admina
php artisan optimize:clear

Nowy user nie może się zalogować

W tinkerze:

php
$user = \App\Models\User::where('email', 'nowy@example.com')->first();

// 1. Czy hasło jest hashem? (powinno zaczynać się od $2y$)
dump($user->password);

// 2. Czy hasło przechodzi?
dump(\Hash::check('twoje_haslo', $user->password));

// 3. Czy ma rolę?
dump($user->getRoleNames()->toArray());

// 4. Czy może wejść do panelu?
$panel = \Filament\Facades\Filament::getPanel('admin');
dump($user->canAccessPanel($panel));

Workflow w praktyce

Tworzysz Resource → shield:generate --alloptimize:clear → w panelu wchodzisz w Roles → tworzysz rolę → zaznaczasz checkboxy permissionów → przypisujesz rolę userowi. Koniec — bez pisania kodu uprawnień ręcznie.

Co się dzieje pod spodem

Spatie Permission trzyma role i permissiony w tabelach roles, permissions, model_has_roles, model_has_permissions. Trait HasRoles na modelu User dodaje relacje many-to-many.

Filament Shield generuje per Resource zestaw siedmiu permissionów (view, view_any, create, update, delete, restore, force_delete) i odpowiadającą Policy w app/Policies. Resource Filamenta automatycznie wywołuje metody policy (viewAny(), create(), update() itd.) w odpowiednich momentach lifecycle'u panelu — dlatego brak permissiona = niewidoczny przycisk lub całe ukrycie Resource'a.

Super admin działa przez Gate::before w AuthServiceProvider, który Shield konfiguruje automatycznie — ten callback zwraca true dla każdego ability, jeśli user ma rolę super_admin, omijając policy w całości.

Permissiony są cache'owane przez Spatie w celu wydajności — stąd potrzeba permission:cache-reset po zmianach na produkcji.