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
UserResourcez 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
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
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:
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
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:
php artisan make:filament-resource User --generate
W formularzu dodaj pole do ról i hasło:
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
// 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():
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:
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ą:
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
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:
$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 --all → optimize: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.