Blog MoonShine · Laravel

Roles y permisos en Laravel 12 con Spatie y MoonShine 4

La mayoría de tutoriales de Spatie se quedan en el modelo. Este va más allá: integramos Spatie Laravel Permission con MoonShine 4 para tener un panel administrativo completo donde gestionar usuarios, roles y permisos sin escribir vistas.

01

¿Qué vamos a construir?

Un sistema de roles y permisos completo con panel de administración. Al terminar tendrás:

  1. Roles dinámicos gestionados desde el panel (Super Admin, Editor, etc.)
  2. Permisos granulares por recurso: viewAny, create, update, delete y más
  3. Usuarios con rol asignable desde el formulario de edición en MoonShine
  4. El rol Super Admin con acceso total sin restricciones

Tres paquetes trabajando juntos:

spatie/laravel-permission          → la lógica de roles y permisos
moonshine/moonshine                → el panel de administración
sweet1s/moonshine-roles-permissions → el puente entre los dos
02

Instalar los paquetes

Partimos de un proyecto Laravel 12 limpio. Instalamos los tres paquetes en orden:

# 1. MoonShine 4
composer require moonshine/moonshine
php artisan moonshine:install

# 2. Spatie Laravel Permission
composer require spatie/laravel-permission

# 3. Puente MoonShine ↔ Spatie
composer require sweet1s/moonshine-roles-permissions

El comando moonshine:install publica la configuración, crea el MoonShineServiceProvider y registra las rutas del panel en /moonshine.

Configura tu base de datos en .env antes de continuar. El siguiente paso ejecuta migraciones.
03

Publicar migraciones y migrar

Spatie necesita publicar sus migraciones antes de ejecutarlas:

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

Esto crea cinco tablas nuevas: roles, permissions, model_has_roles, model_has_permissions y role_has_permissions.

04

Configurar el modelo User

El modelo User necesita el trait del paquete sweet1s en lugar del trait estándar de Spatie. Abre app/Models/User.php:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Sweet1s\MoonshineRBAC\Traits\MoonshineRBACHasRoles;

class User extends Authenticatable
{
    use HasFactory, Notifiable, MoonshineRBACHasRoles;

    const SUPER_ADMIN_ROLE_ID = 1;

    protected $fillable = [
        'name',
        'email',
        'password',
    ];

    protected $hidden = [
        'password',
        'remember_token',
    ];

    protected function casts(): array
    {
        return [
            'email_verified_at' => 'datetime',
            'password'          => 'hashed',
        ];
    }
}
MoonshineRBACHasRoles extiende internamente el trait HasRoles de Spatie, así que no necesitás agregar los dos por separado. La constante SUPER_ADMIN_ROLE_ID = 1 le indica al sistema cuál rol tiene acceso total.
05

Crear el modelo Role

Necesitamos un modelo Role propio que extienda el de Spatie y agregue el comportamiento de MoonShine:

php artisan make:model Role

Reemplazá el contenido generado por este:

<?php

namespace App\Models;

use Spatie\Permission\Models\Role as SpatieRole;
use Sweet1s\MoonshineRBAC\Traits\HasMoonShineRolePermissions;

class Role extends SpatieRole
{
    use HasMoonShineRolePermissions;

    protected $with = ['permissions'];
}

El $with = ['permissions'] carga eager las permissions del rol en cada consulta, evitando el problema N+1 cuando MoonShine muestra la lista de roles.

06

Instalar el sistema RBAC

Este comando del paquete sweet1s hace tres cosas: genera los permisos automáticos para cada recurso registrado, crea el rol Super Admin (ID 1) y le asigna todos los permisos:

php artisan moonshine-rbac:install

Los permisos que genera para cada recurso son:

viewAny   → ver el listado
view      → ver detalle
create    → crear registros
update    → editar registros
delete    → eliminar
massDelete → eliminación masiva
restore   → restaurar eliminados
forceDelete → eliminación permanente
Ejecutá este comando cada vez que registres un nuevo Resource en MoonShine para que se generen sus permisos automáticamente.
07

Crear los Resources en MoonShine

Necesitamos tres resources: uno para roles, uno para permisos y uno para usuarios.

RoleResource — gestión de roles con asignación de permisos:

<?php

declare(strict_types=1);

namespace App\MoonShine\Resources;

use App\Models\Role;
use MoonShine\Laravel\Resources\ModelResource;
use MoonShine\UI\Fields\ID;
use MoonShine\UI\Fields\Number;
use MoonShine\UI\Fields\Text;
use Sweet1s\MoonshineRBAC\Traits\WithPermissionsFormComponent;
use Sweet1s\MoonshineRBAC\Traits\WithRolePermissions;

class RoleResource extends ModelResource
{
    use WithRolePermissions;
    use WithPermissionsFormComponent;

    protected string $model = Role::class;
    protected string $column = 'name';

    public function getTitle(): string
    {
        return 'Roles';
    }

    protected function indexFields(): iterable
    {
        return [
            ID::make(),
            Text::make('Name', 'name'),
            Number::make('Priority', 'priority'),
        ];
    }

    protected function formFields(): iterable
    {
        return [
            ID::make(),
            Text::make('Name', 'name')->required(),
            Number::make('Priority', 'priority')->default(0),
        ];
    }

    public function rules(mixed $item): array
    {
        return [
            'name'     => 'required|string|max:255',
            'priority' => 'nullable|integer',
        ];
    }

    public function search(): array
    {
        return ['id', 'name'];
    }
}

El trait WithPermissionsFormComponent inyecta automáticamente un componente de checkboxes en el formulario para asignar permisos al rol.

PermissionResource — CRUD de permisos individuales:

<?php

declare(strict_types=1);

namespace App\MoonShine\Resources;

use MoonShine\Laravel\Resources\ModelResource;
use MoonShine\UI\Fields\ID;
use MoonShine\UI\Fields\Text;
use Spatie\Permission\Models\Permission;
use Sweet1s\MoonshineRBAC\Traits\WithRolePermissions;

class PermissionResource extends ModelResource
{
    use WithRolePermissions;

    protected string $model = Permission::class;
    protected string $column = 'name';

    public function getTitle(): string
    {
        return 'Permissions';
    }

    protected function indexFields(): iterable
    {
        return [
            ID::make(),
            Text::make('Name', 'name'),
            Text::make('Guard', 'guard_name'),
        ];
    }

    protected function formFields(): iterable
    {
        return [
            ID::make(),
            Text::make('Name', 'name')->required(),
            Text::make('Guard', 'guard_name')->default('web'),
        ];
    }

    public function rules(mixed $item): array
    {
        return [
            'name'       => 'required|string|max:255',
            'guard_name' => 'required|string|max:255',
        ];
    }
}

UserResource — usuarios con selector de rol en el formulario:

<?php

declare(strict_types=1);

namespace App\MoonShine\Resources;

use App\Models\User;
use Illuminate\Validation\Rule;
use MoonShine\Laravel\Resources\ModelResource;
use MoonShine\UI\Components\Layout\Box;
use MoonShine\UI\Components\Layout\Column;
use MoonShine\UI\Components\Layout\Grid;
use MoonShine\UI\Fields\Date;
use MoonShine\UI\Fields\Email;
use MoonShine\UI\Fields\ID;
use MoonShine\UI\Fields\Password;
use MoonShine\UI\Fields\PasswordRepeat;
use MoonShine\UI\Fields\Text;
use Sweet1s\MoonshineRBAC\Traits\WithRoleFormComponent;
use Sweet1s\MoonshineRBAC\Traits\WithRolePermissions;

class UserResource extends ModelResource
{
    use WithRolePermissions;
    use WithRoleFormComponent;

    protected string $model = User::class;
    protected string $column = 'name';

    public function getTitle(): string
    {
        return 'Users';
    }

    protected function indexFields(): iterable
    {
        return [
            ID::make(),
            Text::make('Name', 'name'),
            Email::make('Email', 'email'),
            Date::make('Created at', 'created_at')->format('d.m.Y'),
        ];
    }

    protected function formFields(): iterable
    {
        return [
            Grid::make([
                Column::make([
                    Box::make('Información principal', [
                        ID::make(),
                        Text::make('Name', 'name')->required(),
                        Email::make('Email', 'email')->required(),
                    ]),
                    Box::make('Cambiar contraseña', [
                        Password::make('Password', 'password')
                            ->customAttributes(['autocomplete' => 'new-password'])
                            ->eye(),
                        PasswordRepeat::make('Repeat Password', 'password_repeat')
                            ->customAttributes(['autocomplete' => 'confirm-password'])
                            ->eye(),
                    ]),
                ]),
            ]),
        ];
    }

    public function rules(mixed $item): array
    {
        return [
            'name'  => 'required',
            'email' => [
                'sometimes', 'bail', 'required', 'email',
                Rule::unique('users')->ignoreModel($item),
            ],
            'password' => $item->exists
                ? 'sometimes|nullable|min:6|same:password_repeat'
                : 'required|min:6|same:password_repeat',
        ];
    }

    public function search(): array
    {
        return ['id', 'name', 'email'];
    }
}

El trait WithRoleFormComponent agrega el selector de rol al formulario de edición de usuario automáticamente.

08

Registrar los Resources

En app/Providers/MoonShineServiceProvider.php registramos los tres resources:

<?php

declare(strict_types=1);

namespace App\Providers;

use App\MoonShine\Resources\PermissionResource;
use App\MoonShine\Resources\RoleResource;
use App\MoonShine\Resources\UserResource;
use Illuminate\Support\ServiceProvider;
use MoonShine\Contracts\Core\DependencyInjection\CoreContract;
use MoonShine\Laravel\DependencyInjection\MoonShine;
use MoonShine\Laravel\DependencyInjection\MoonShineConfigurator;

class MoonShineServiceProvider extends ServiceProvider
{
    public function boot(CoreContract $core): void
    {
        $core
            ->resources([
                UserResource::class,
                RoleResource::class,
                PermissionResource::class,
            ])
            ->pages([
                ...$core->getConfig()->getPages(),
            ]);
    }
}
09

Crear el usuario administrador

Podés crear el admin con un seeder o directamente desde Tinker.

Opción A — Seeder (recomendado para proyectos en equipo):

<?php

namespace Database\Seeders;

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

class AdminUserSeeder extends Seeder
{
    public function run(): void
    {
        $admin = User::create([
            'name'               => 'Administrador',
            'email'              => 'admin@admin.com',
            'password'           => Hash::make('password'),
            'email_verified_at'  => now(),
        ]);

        $superAdminRole = Role::find(1);

        if ($superAdminRole) {
            $admin->assignRole($superAdminRole);
            $this->command->info('Admin creado: admin@admin.com / password');
        } else {
            $this->command->error('Primero ejecutá: php artisan moonshine-rbac:install');
        }
    }
}
php artisan db:seed --class=AdminUserSeeder

Opción B — Tinker (para pruebas rápidas):

php artisan tinker

$admin = App\Models\User::create([
    'name'              => 'Administrador',
    'email'             => 'admin@admin.com',
    'password'          => Hash::make('password'),
    'email_verified_at' => now(),
]);

$admin->assignRole(App\Models\Role::find(1));
10

Ingresar al panel

Abrí tu navegador en:

http://tu-proyecto.test/moonshine

Ingresá con admin@admin.com / password. El panel mostrará tres secciones en el menú:

  1. Users — listado de usuarios con columnas name, email, fecha. El formulario de edición incluye el selector de rol.
  2. Roles — listado de roles. Al crear o editar un rol aparecen checkboxes con todos los permisos disponibles agrupados por resource.
  3. Permissions — listado de permisos individuales con su guard.
El Super Admin (ID 1) tiene acceso total independientemente de los permisos asignados. Para probar la restricción de permisos creá un segundo rol con permisos limitados y asignáselo a un usuario diferente.
11

Problemas comunes

El menú de roles no aparece en el panel:

php artisan cache:clear
php artisan config:clear
php artisan moonshine-rbac:install

Error "Class Role not found" al asignar rol:
Verificá que en config/permission.php el modelo de roles apunta al tuyo:

'models' => [
    'role'       => App\Models\Role::class,
    'permission' => Spatie\Permission\Models\Permission::class,
],

Los permisos de un Resource nuevo no aparecen:
Registrá el Resource en MoonShineServiceProvider y volvé a ejecutar:

php artisan moonshine-rbac:install
jamesmosq / roles-y-permisos
Código completo de este tutorial — Laravel 12 con Spatie Permission, MoonShine 4, sweet1s RBAC, tres Resources y seeder de administrador.
Ver en GitHub
Volver al blog