Блог на Laravel 7, часть 11. Панель управления — назначение ролей и прав для пользователей

18.01.2021

Теги: LaravelMySQLPHPWeb-разработкаБазаДанныхБлогПользовательПраваДоступаПрактикаФреймворкШаблонСайта

Теперь о правах и ролях пользователей. У нас уже есть форма редактирования пользователя — можем в нее добавить checkbox-ы, позволяющие выбрать права и роли. Еще один способ — создать отдельную страницу в панели управления, где можно будет назначать для пользователя права и роли. Не решил, какой способ лучше, так что реализуем оба — у нас же учебный проект.

Роли и права, первый вариант

Чтобы создать на форме checkbox-ы для назначения прав и ролей — в контроллере надо получить все права и роли и передать их в шаблон. Чтобы форма редактирования пользователя не получилось слишком большой — создадим два шаблона и подключим их в форме.

class UserController extends Controller {
    /**
     * Показывает форму для редактирования пользователя
     */
    public function edit(User $user) {
        $allroles = Role::all();
        $allperms = Permission::all();
        return view(
            'admin.user.edit',
            compact('user', 'allperms', 'allroles')
        );
    }
    /**
     * Обновляет данные пользователя в базе данных
     */
    public function update(Request $request, User $user) {
        /*
         * Проверяем данные формы
         */
        $this->validator($request->all(), $user->id)->validate();
        /*
         * Обновляем пользователя
         */
        if ($request->change_password) { // если надо изменить пароль
            $request->merge(['password' => Hash::make($request->password)]);
            $user->update($request->all());
        } else {
            $user->update($request->except('password'));
        }
        /*
         * Назначаем роли и права
         */
        if (auth()->user()->hasPermAnyWay('assign-role')) {
            $user->roles()->sync($request->roles);
        }
        if (auth()->user()->hasPermAnyWay('assign-permission')) {
            $user->permissions()->sync($request->perms);
        }
        /*
         * Возвращаемся к списку
         */
        return redirect()
            ->route('admin.user.index')
            ->with('success', 'Данные пользователя успешно обновлены');
    }
    /* ... */
}
@extends('layout.admin', ['title' => 'Редактирование пользователя'])

@section('content')
    <h1 class="mb-4">Редактирование пользователя</h1>
    <form method="post" action="{{ route('admin.user.update', ['user' => $user->id]) }}">
        @csrf
        @method('PUT')
        <div class="form-group">
            <input type="text" class="form-control" name="name" placeholder="Имя, Фамилия"
                   required maxlength="255" value="{{ old('name') ?? $user->name }}">
        </div>
        <div class="form-group">
            <input type="email" class="form-control" name="email" placeholder="Адрес почты"
                   required maxlength="255" value="{{ old('email') ?? $user->email }}">
        </div>
        <div class="form-group form-check">
            <input type="checkbox" class="form-check-input" name="change_password"
                   id="change_password">
            <label class="form-check-label" for="change_password">
                Изменить пароль пользователя
            </label>
        </div>
        <div class="form-group">
            <input type="text" class="form-control" name="password" maxlength="255"
                   placeholder="Новый пароль" value="">
        </div>
        <div class="form-group">
            <input type="text" class="form-control" name="password_confirmation" maxlength="255"
                   placeholder="Пароль еще раз" value="">
        </div>
        @perm('assign-role')
            @include('admin.user.all-roles')
        @endperm
        @perm('assign-permission')
            @include('admin.user.all-perms')
        @endperm
        <div class="form-group">
            <button type="submit" class="btn btn-success">Сохранить</button>
        </div>
    </form>
@endsection

Шаблон resources/views/admin/user/all-roles.blade.php для выбора ролей пользователя:

<h5>Роли</h5>
<div class="form-group d-flex flex-wrap">
    @php
        /*
         * Тут возможны такие варианты:
         * 1. Форма редактирования еще не отправлена, привязанные роли берем
         *    из связи модели User с моделью Role через сводную таблицу
         * 2. Форма редактирования была отправлена, но были ошибки формы,
         *    поэтому отмеченные админом checkbox-ы берем из old()
         */
        $roles = $user->roles->keyBy('id')->keys()->toArray();
        if (old('roles')) $roles = old('roles');
    @endphp
    @foreach ($allroles as $item)
        @php $checked = in_array($item->id, $roles) @endphp
        <div class="form-check-inline w-25 mr-0">
            <input class="form-check-input" type="checkbox"
                   name="roles[]" id="role-id-{{ $item->id }}"
                   value="{{ $item->id }}" @if($checked) checked @endif>
            <label class="form-check-label" for="role-id-{{ $item->id }}">
                {{ $item->name }}
            </label>
        </div>
    @endforeach
</div>

Шаблон resources/views/admin/user/all-perms.blade.php для выбора прав пользователя:

<h5>Права</h5>
<div class="form-group d-flex flex-wrap">
    @php
        /*
         * Тут возможны такие варианты:
         * 1. Форма редактирования еще не отправлена, привязанные права берем
         *    из связи модели User с моделью Permission через сводную таблицу
         * 2. Форма редактирования была отправлена, но были ошибки формы,
         *    поэтому отмеченные админом checkbox-ы берем из old()
         */
        $perms = $user->permissions->keyBy('id')->keys()->toArray();
        if (old('perms')) $perms = old('perms');
    @endphp
    @foreach ($allperms as $item)
        @php $checked = in_array($item->id, $perms) @endphp
        <div class="form-check-inline w-25 mr-0">
            <input class="form-check-input" type="checkbox"
                   name="perms[]" id="perm-id-{{ $item->id }}"
                   value="{{ $item->id }}" @if($checked) checked @endif>
            <label class="form-check-label" for="perm-id-{{ $item->id }}">
                {{ $item->name }}
            </label>
        </div>
    @endforeach
</div>

Роли и права, второй вариант

Нам потребуется разрешить еще один маршрут для ресурсного контроллера — admin.user.show, отвечать за который будет метод show().

/*
 * Панель управления: CRUD-операции над постами, категориями, тегами
 */
Route::group([
    'as' => 'admin.', // имя маршрута, например admin.index
    'prefix' => 'admin', // префикс маршрута, например admin/index
    'namespace' => 'Admin', // пространство имен контроллеров
    'middleware' => ['auth'] // один или несколько посредников
], function () {
    /*
     * Просмотр и редактирование пользователей
     */
    Route::resource('user', 'UserController', ['except' => [
        'create', 'store', 'destroy' /* раньше метод show() был запрещен */
    ]]);
});
class UserController extends Controller {
    /**
     * Показывает права и роли пользователя
     */
    public function show(User $user) {
        $roles = Role::all();
        $perms = Permission::all();
        return view(
            'admin.user.show',
            compact('user', 'roles', 'perms')
        );
    }
    /* ... */
}

Шаблон для показа списка всех пользователей resources/views/admin/user/index.blade.php будет содержать ссылку для перехода к управлению правами и ролями.

@extends('layout.admin', ['title' => 'Все пользователи'])

@section('content')
    <h1 class="mb-4">Все пользователи</h1>

    <table class="table table-bordered">
        <tr>
            <th>#</th>
            <th width="20%">Дата регистрации</th>
            <th width="25%">Имя, фамилия</th>
            <th width="20%">Адрес почты</th>
            <th width="15%">Публикаций</th>
            <th width="15%">Комментариев</th>
            <th><i class="fas fa-edit"></i></th>
            <th><i class="fas fa-user-cog"></i></th>
        </tr>
        @foreach($users as $user)
            <tr>
                <td>{{ $user->id }}</td>
                <td>{{ $user->created_at }}</td>
                <td>{{ $user->name }}</td>
                <td><a href="mailto:{{ $user->email }}">{{ $user->email }}</a></td>
                <td>{{ $user->posts->count() }}</td>
                <td>{{ $user->comments->count() }}</td>
                <td>
                    @perm('edit-user')
                        <a href="{{ route('admin.user.edit', ['user' => $user->id]) }}">
                            <i class="far fa-edit"></i>
                        </a>
                    @endperm
                </td>
                <td>
                    <a href="{{ route('admin.user.show', ['user' => $user->id]) }}">
                        <i class="far fa-user-cog"></i>
                    </a>
                </td>
            </tr>
        @endforeach
    </table>
    {{ $users->links() }}
@endsection

Шаблон страницы resources/views/admin/user/show.blade.php для просмотра и управления правами и ролями пользователя.

@extends('layout.admin', ['title' => 'Роли и права'])

@section('content')
    <h1 class="mb-4">Роли и права</h1>
    <p>Пользователь: {{ $user->name }}</p>

    <h2>Роли</h2>
    <table class="table table-bordered">
        <tr>
            <th width="10%">id</th>
            <th width="30%">Slug</th>
            <th width="30%">Name</th>
            <th width="30%">Assign/Unassign</th>
        </tr>
        @foreach($roles as $role)
            <tr>
                <td>{{ $role->id }}</td>
                <td>{{ $role->slug }}</td>
                <td>{{ $role->name }}</td>
                <td>
                    @php $params = ['user' => $user->id, 'role' => $role->id]; @endphp
                    @if ($user->hasRole($role->slug))
                        <a href="{{ route('admin.user.unassign.role', $params) }}"
                           class="text-danger">Отнять эту роль</a>
                    @else
                        <a href="{{ route('admin.user.assign.role', $params) }}"
                           class="text-success">Назначить эту роль</a>
                    @endif
                </td>
            </tr>
        @endforeach
    </table>

    <h2>Права</h2>
    <table class="table table-bordered">
        <tr>
            <th width="10%">id</th>
            <th width="30%">Slug</th>
            <th width="30%">Name</th>
            <th width="30%">Assign/Unassign</th>
        </tr>
        @foreach($perms as $perm)
            <tr>
                <td>{{ $perm->id }}</td>
                <td>{{ $perm->slug }}</td>
                <td>{{ $perm->name }}</td>
                <td>
                    @php $params = ['user' => $user->id, 'perm' => $perm->id]; @endphp
                    @if ($user->hasPerm($perm->slug))
                        <a href="{{ route('admin.user.unassign.perm', $params) }}"
                           class="text-danger">Отнять это право</a>
                    @else
                        <a href="{{ route('admin.user.assign.perm', $params) }}"
                           class="text-success">Назначить это право</a>
                    @endif
                </td>
            </tr>
        @endforeach
    </table>
@endsection

Для назначения и удаления права или роли нам потребуются еще четыре маршрута и четыре метода контроллера:

/*
 * Панель управления: CRUD-операции над постами, категориями, тегами
 */
Route::group([
    'as' => 'admin.', // имя маршрута, например admin.index
    'prefix' => 'admin', // префикс маршрута, например admin/index
    'namespace' => 'Admin', // пространство имен контроллеров
    'middleware' => ['auth'] // один или несколько посредников
], function () {
    /*
     * Просмотр и редактирование пользователей
     */
    Route::resource('user', 'UserController', ['except' => [
        'create', 'store', 'destroy'
    ]]);
    // доп.маршрут, чтобы назначить роль
    Route::get('user/{user}/role/{role}/assign', 'UserController@assignRole')
        ->name('user.assign.role');
    // доп.маршрут, чтобы назначить право
    Route::get('user/{user}/perm/{perm}/assign', 'UserController@assignPerm')
        ->name('user.assign.perm');
    // доп.маршрут, чтобы отнять роль
    Route::get('user/{user}/role/{role}/unassign', 'UserController@unassignRole')
        ->name('user.unassign.role');
    // доп.маршрут, чтобы отнять право
    Route::get('user/{user}/perm/{perm}/unassign', 'UserController@unassignPerm')
        ->name('user.unassign.perm');
});
namespace App\Http\Controllers\Admin;

use App\Http\Controllers\Controller;
use App\Permission;
use App\Role;
use App\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Validator;

class UserController extends Controller {
    /**
     * Показывает права и роли пользователя
     */
    public function show(User $user) {
        $roles = Role::all();
        $perms = Permission::all();
        return view(
            'admin.user.show',
            compact('user', 'roles', 'perms')
        );
    }

    public function assignRole(User $user, Role $role) {
        if ( ! auth()->user()->hasPermAnyWay('assign-role')) {
            return redirect()
                ->route('admin.user.show', ['user' => $user->id])
                ->withErrors('Нет прав на выполнение этого действия');
        }
        $user->assignRoles($role->slug);
        return redirect()
            ->route('admin.user.show', ['user' => $user->id])
            ->with('success', 'Данные пользователя успешно обновлены');
    }

    public function assignPerm(User $user, Permission $perm) {
        if ( ! auth()->user()->hasPermAnyWay('assign-permission')) {
            return redirect()
                ->route('admin.user.show', ['user' => $user->id])
                ->withErrors('Нет прав на выполнение этого действия');
        }
        $user->assignPermissions($perm->slug);
        return redirect()
            ->route('admin.user.show', ['user' => $user->id])
            ->with('success', 'Данные пользователя успешно обновлены');
    }

    public function unassignRole(User $user, Role $role) {
        if ( ! auth()->user()->hasPermAnyWay('assign-role')) {
            return redirect()
                ->route('admin.user.show', ['user' => $user->id])
                ->withErrors('Нет прав на выполнение этого действия');
        }
        $user->unassignRoles($role->slug);
        return redirect()
            ->route('admin.user.show', ['user' => $user->id])
            ->with('success', 'Данные пользователя успешно обновлены');
    }

    public function unassignPerm(User $user, Permission $perm) {
        if ( ! auth()->user()->hasPermAnyWay('assign-permission')) {
            return redirect()
                ->route('admin.user.show', ['user' => $user->id])
                ->withErrors('Нет прав на выполнение этого действия');
        }
        $user->unassignPermissions($perm->slug);
        return redirect()
            ->route('admin.user.show', ['user' => $user->id])
            ->with('success', 'Данные пользователя успешно обновлены');
    }
    /* ... */
}

Создание, редактирование и удаление роли

Нам потребуется ресурсный контроллер для CRUD-операций над ролями, новые маршруты и несколько шаблонов для просмотра списка ролей, создания и редактирования роли.

Новые маршруты:

/*
 * Панель управления: CRUD-операции над постами, категориями, тегами
 */
Route::group([
    'as' => 'admin.', // имя маршрута, например admin.index
    'prefix' => 'admin', // префикс маршрута, например admin/index
    'namespace' => 'Admin', // пространство имен контроллеров
    'middleware' => ['auth'] // один или несколько посредников
], function () {
    /*
     * CRUD-операции над ролями
     */
    Route::resource('role', 'RoleController', ['except' => 'show']);
});

Ресурсный контроллер:

> php artisan make:controller Admin/RoleController --resource --model=Role
namespace App\Http\Controllers\Admin;

use App\Http\Controllers\Controller;
use App\Permission;
use App\Role;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator;

class RoleController extends Controller {

    public function __construct() {
        $this->middleware('perm:manage-roles')->only('index');
        $this->middleware('perm:create-role')->only(['create', 'store']);
        $this->middleware('perm:edit-role')->only(['edit', 'update']);
        $this->middleware('perm:delete-role')->only('destroy');
    }

    /**
     * Показывает список всех ролей пользователя
     */
    public function index() {
        $roles = Role::paginate(8);
        return view('admin.role.index', compact('roles'));
    }

    /**
     * Показывает форму для создания роли
     */
    public function create() {
        $allperms = Permission::all();
        return view('admin.role.create', compact('allperms'));
    }

    /**
     * Сохраняет новую роль в базу данных
     */
    public function store(Request $request) {
        $this->validator($request->all(), null)->validate();
        $role = Role::create($request->all());
        $role->permissions()->attach($request->perms ?? []);
        return redirect()
            ->route('admin.role.index')
            ->with('success', 'Новая роль успешно создана');
    }

    /**
     * Показывает форму для редактирования роли
     */
    public function edit(Role $role) {
        $allperms = Permission::all();
        return view('admin.role.edit', compact('role', 'allperms'));
    }

    /**
     * Обновляет роль в базе данных
     */
    public function update(Request $request, Role $role) {
        if ($role->id === 1) {
            return redirect()
                ->route('admin.role.index')
                ->withErrors('Эту роль нельзя редактировать');
        }
        $this->validator($request->all(), $role->id)->validate();
        $role->update($request->all());
        $role->permissions()->sync($request->perms ?? []);
        return redirect()
            ->route('admin.role.index')
            ->with('success', 'Роль была успешно отредактирована');
    }

    /**
     * Удаляет роль из базы данных
     */
    public function destroy(Role $role) {
        if ($role->id === 1) {
            return redirect()
                ->route('admin.role.index')
                ->withErrors('Эту роль нельзя удалить');
        }
        $role->delete();
        return redirect()
            ->route('admin.role.index')
            ->with('success', 'Роль была успешно удалена');
    }

    /**
     * Возвращает объект валидатора с нужными правилами
     */
    private function validator($data, $id) {
        $unique = 'unique:roles,slug';
        if ($id) {
            // проверка на уникальность slug роли при редактировании,
            // исключая эту роль по идентифкатору в таблице БД roles
            $unique = 'unique:roles,slug,'.$id.',id';
        }
        $rules = [
            'name' => [
                'required',
                'string',
                'max:50',
            ],
            'slug' => [
                'required',
                'max:50',
                $unique,
                'regex:~^[-_a-z0-9]+$~i',
            ]
        ];
        $messages = [
            'required' => 'Поле «:attribute» обязательно для заполнения',
            'max' => 'Поле «:attribute» должно быть не больше :max символов',
        ];
        $attributes = [
            'name' => 'Наименование',
            'slug' => 'Идентификатор'
        ];
        return Validator::make($data, $rules, $messages, $attributes);
    }
}

Поскольку в контроллере мы используем «mass assigment» — добавим в модель Role свойство $fillable:

class Role extends Model {
    protected $fillable = [
        'name',
        'slug',
    ];
    /* ... */
}

Шаблон resources/views/admin/role/index.blade.php для показа списка всех ролей пользователя:

@extends('layout.admin', ['title' => 'Все роли'])

@section('content')
    <h1>Все роли</h1>
    @perm('create-role')
    <a href="{{ route('admin.role.create') }}" class="btn btn-success mb-4">
        Создать роль
    </a>
    @endperm
    @if ($roles->count())
        <table class="table table-bordered">
            <tr>
                <th>#</th>
                <th width="45%">Идентификатор</th>
                <th width="45%">Наименование</th>
                <th><i class="fas fa-edit"></i></th>
                <th><i class="fas fa-trash-alt"></i></th>
            </tr>
            @foreach ($roles as $role)
                <tr>
                    <td>{{ $role->id }}</td>
                    <td>{{ $role->slug }}</td>
                    <td>{{ $role->name }}</td>
                    <td>
                        @perm('edit-role')
                            <a href="{{ route('admin.role.edit', ['role' => $role->id]) }}">
                                <i class="far fa-edit"></i>
                            </a>
                        @endperm
                    </td>
                    <td>
                        @perm('delete-role')
                        <form action="{{ route('admin.role.destroy', ['role' => $role->id]) }}"
                              method="post" onsubmit="return confirm('Удалить эту роль?')">
                            @csrf
                            @method('DELETE')
                            <button type="submit" class="m-0 p-0 border-0 bg-transparent">
                                <i class="far fa-trash-alt text-danger"></i>
                            </button>
                        </form>
                        @endperm
                    </td>
                </tr>
            @endforeach
        </table>
        {{ $roles->links() }}
    @endif
@endsection

Шаблон resources/views/admin/role/create.blade.php для создания новой роли:

@extends('layout.admin', ['title' => 'Создание роли'])

@section('content')
    <h1>Создание роли</h1>
    <form method="post" action="{{ route('admin.role.store') }}">
        @csrf
        @include('admin.role.form')
    </form>
@endsection

Шаблон resources/views/admin/role/edit.blade.php для редактирования роли:

@extends('layout.admin', ['title' => 'Редактирование роли'])

@section('content')
    <h1>Редактирование роли</h1>
    <form method="post" action="{{ route('admin.role.update', ['role' => $role->id]) }}">
        @csrf
        @method('PUT')
        @include('admin.role.form')
    </form>
@endsection

Шаблон resources/views/admin/role/form.blade.php формы создания-редактирования:

<div class="form-group">
    <input type="text" class="form-control" name="name" placeholder="Наименование"
           required maxlength="50" value="{{ old('name') ?? $role->name ?? '' }}">
</div>
<div class="form-group">
    <input type="text" class="form-control" name="slug" placeholder="Идентификатор"
           required maxlength="50" value="{{ old('slug') ?? $role->slug ?? '' }}">
</div>
<h5>Права</h5>
<div class="form-group d-flex flex-wrap">
@php
    // если это форма создания новой роли
    $perms = [];
    // если это форма редактирования роли
    if (isset($role))  $perms = $role->permissions->keyBy('id')->keys()->toArray();
    // форма была отправлена, но с ошибками
    if (old('perms')) $perms = old('perms');
@endphp
@foreach ($allperms as $item)
    @php $checked = in_array($item->id, $perms) @endphp
    <div class="form-check-inline w-25 mr-0">
        <input class="form-check-input" type="checkbox"
               name="perms[]" id="perm-id-{{ $item->id }}"
               value="{{ $item->id }}" @if($checked) checked @endif>
        <label class="form-check-label" for="perm-id-{{ $item->id }}">
            {{ $item->name }}
        </label>
    </div>
@endforeach
</div>
<div class="form-group">
    <button type="submit" class="btn btn-success">Сохранить</button>
</div>

Поиск: Laravel • MySQL • PHP • Web-разработка • База данных • Блог • Пользователь • Права доступа • Практика • Фреймворк • Шаблон сайта

Каталог оборудования
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
Производители
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
Функциональные группы
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.