Laravel Ecommerce Tutorial: Part 3, Managing Roles And Permissions
Given Ncube
Welcome to part 3 of Laravel Ecommerce Tutorial. In the previous tutorial, we setup authorization with Spatie Laravel Permission with the help of our very own Laravel Authorizer and added the ability to manage users in our ecommerce website.
Aside from managing users, another important feature of any application is the ability to assign roles and permissions to users so that an SEO Specialist for example, won't have the ability to fulfill orders or delete products
In this tutorial we continue from where we left from the previous post and add
-
the ability to delete users
-
the create roles
-
the ability to assign roles to users
-
the ability to assign permissions to a role
-
the ability to assign users arbitrary permissions not provided by their role
Deleting users
First let's pickup where we left in the previous tutorial. Let's start by defining the destroy
action of the UserController
/**
* Remove the specified resource from storage.
*
* @param User $user
* @return RedirectResponse
*/
public function destroy(User $user): RedirectResponse
{
$user->delete();
return redirect()
->route('admin.home.index')
->with('success', 'User deleted successfully.');
}
This method simply deletes the user and redirects back the users index page with a flash message
Roles
Now that we are done with users let's move on to roles. We need the ability to create update and delete roles, assign permissions to roles. To start off let's create the roles controller
php artisan make:controller Admin\\RoleController -R --model=Role --test
This will prompt you to create the role model, accept and make sure the model will extend Spatie\Permission\Models\Role
instead of the default eloquent model. This will make it easier for us to generate permissions and policies for the models.
Let's register the route
Route::resource('roles', RoleController::class);
Of course we start by writing tests, I'm not going to put the tests in this post because it will make it extremely long, but since you saw how we wrote tests in the previous tuts write the tests for this controller and let's get started. Let's start by implementing the index
action
/**
* Display a listing of the resource.
*
* @return Renderable
*/
public function index(): Renderable
{
$roles = Role::paginate();
return view('admin.roles.index', [
'roles' => $roles,
]);
}
Next, create the admin.roles.index
view and populate it with
@extends('layouts.app')
@section('content')
<section class="section">
<div class="section-header">
<h1>Roles</h1>
<div class="section-header-button">
<a href="{{ route('admin.roles.create') }}"
class="btn btn-primary">Create New</a>
</div>
<div class="section-header-breadcrumb">
<div class="breadcrumb-item active"><a href="{{ route('admin.home.index') }}">Dashboard</a></div>
<div class="breadcrumb-item"><a href="{{ route('admin.roles.index') }}">Roles</a></div>
<div class="breadcrumb-item">Roles</div>
</div>
</div>
<div class="section-body">
<h2 class="section-title">Roles</h2>
<p class="section-lead">
You can manage all roles, such as editing, deleting and assigning permissions.
</p>
<div class="row mt-4">
<div class="col-12">
<div class="card">
<div class="card-header">
<h4>All Roles</h4>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-borderless rounded-md">
<tr class="">
<th>Name</th>
<th>Guard</th>
<th>Created</th>
</tr>
@foreach ($roles as $role)
<tr>
<td>
<a href="{{ route('admin.roles.edit', $role) }}"
class="btn btn-link text-decoration-none text-bg-dark">
{{ $role->name }}
</a>
</td>
<td>{{ $role->guard }}</td>
<td>{{ $role->created_at->diffForHumans() }}</td>
</tr>
@endforeach
</table>
</div>
<div class="float-right">
{{ $roles->links('vendor.pagination.bootstrap-5') }}
</div>
</div>
</div>
</div>
</div>
</div>
</section>
@endsection
Lastly add the roles link to the side bar. Just below the users link in the layouts.partials.sidebar
add
<li class="nav-item @if (Route::is('admin.roles.*')) active @endif">
<a href="{{ route('admin.roles.index') }}"
class="nav-link"
data-toggle="dropdown">
<i class="fas fa-lock"></i> <span>Roles</span>
</a>
</li>
At this point we should be able to click on the sidebar link and see all available roles in our application.
Now we need the ability to create new role. Let's create the view for the create
action.
/**
* Show the form for creating a new resource.
*
* @return Renderable
*/
public function create(): Renderable
{
return view('admin.roles.create');
}
And the create
view
@extends('layouts.app')
@section('title')
Create New User
@endsection
@section('content')
<section class="section">
<div class="section-header">
<div class="section-header-back">
<a href="{{ route('admin.users.index') }}"
class="btn btn-icon"><i class="fas fa-arrow-left"></i></a>
</div>
<h1>Create Role</h1>
<div class="section-header-breadcrumb">
<div class="breadcrumb-item active"><a href="{{ route('admin.home.index') }}">Dashboard</a></div>
<div class="breadcrumb-item"><a href="{{ route('admin.roles.index') }}">Roles</a></div>
<div class="breadcrumb-item">Create</div>
</div>
</div>
<div class="section-body">
<h2 class="section-title">Create Role</h2>
<p class="section-lead">
You can add new roles and assign permissions to them
</p>
<div class="container">
<div class="row">
<div class="col-12 col-md-7 ms-auto">
<div class="card">
<div class="card-header">
<h4>Role</h4>
</div>
<div class="card-body">
<form action="{{ route('admin.roles.store') }}"
method="POST">
@csrf
<div class="form-group">
<label for="name"
class="label form-control-label">Name</label>
<input type="text"
name="name"
id="name"
class="form-control @error('name') is-invalid @enderror"
value="{{ old('name') }}">
@error('name')
<div class="invalid-feedback">
{{ $message }}
</div>
@enderror
</div>
<div class="form-group">
<label for="guard"
class="label form-control-label">Guard</label>
<input type="text"
name="guard"
id="guard"
placeholder="web"
class="form-control @error('guard') is-invalid @enderror"
value="{{ old('guard') }}">
@error('email')
<div class="invalid-feedback">
{{ $message }}
</div>
@enderror
</div>
<div class="form-group">
<button type="submit"
class="btn btn-primary">Create Role</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
@endsection
The next thing is to actually store the role in the database, but first, let's create some validation rules. In the App\Http\Requests\StoreRoleRequest
modify the rules
and authorize
methods as follows
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class StoreRoleRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize(): bool
{
return $this->user()->can('create roles');
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, mixed>
*/
public function rules(): array
{
return [
'name' => 'required|string|unique:roles,name',
'guard' => 'sometimes|nullable|string',
];
}
}
This the request that will be used to create role, the authorized method ensures that only users with the ability to create roles
can create roles and the rules method is simply the validation.
Next let's type hint the request on our store method and implement the actual storage of the role
/**
* Store a newly created resource in storage.
*
* @param StoreRoleRequest $request
* @return RedirectResponse
*/
public function store(StoreRoleRequest $request): RedirectResponse
{
Role::create($request->validated());
return redirect()
->route('admin.roles.index')
->with('success', 'Role created successfully.');
}
With this in place we should be able to create the role from the UI. and since our logged user has a special role, super admin
they can do anything in the application, no need to assign the permission.
Now that we can create roles let's add the ability to edit the role and assign permissions. Let's implement the edit
action and view
/**
* Show the form for editing the specified resource.
*
* @param Role $role
* @return Renderable
*/
public function edit(Role $role): Renderable
{
$permissions = Permission::all();
return view('admin.roles.edit', [
'role' => $role,
'permissions' => $permissions,
]);
}
And now for the edit view, first let's make sure we can edit role itself, like it's name or the guard name
@extends('layouts.app')
@section('title')
Edit Role
@endsection
@section('content')
<section class="section">
<div class="section-header">
<div class="section-header-back">
<a href="{{ route('admin.roles.index') }}"
class="btn btn-icon"><i class="fas fa-arrow-left"></i></a>
</div>
<h1>Edit Role</h1>
<div class="section-header-breadcrumb">
<div class="breadcrumb-item active"><a href="{{ route('admin.home.index') }}">Dashboard</a></div>
<div class="breadcrumb-item"><a href="{{ route('admin.roles.index') }}">Roles</a></div>
<div class="breadcrumb-item">Edit</div>
</div>
</div>
<div class="section-body">
<h2 class="section-title">Edit Role</h2>
<p class="section-lead">
You can edit new roles and assign permissions to them
</p>
<div class="container">
<div class="row">
<div class="col-12 col-md-7 ms-auto">
<div class="card">
<div class="card-header">
<h4>Role</h4>
</div>
<div class="card-body">
<form action="{{ route('admin.roles.update', $role) }}"
method="POST">
@csrf
<div class="form-group">
<label for="name"
class="label form-control-label">Name</label>
<input type="text"
name="name"
id="name"
class="form-control @error('name') is-invalid @enderror"
value="{{ old('name', $role->name) }}">
@error('name')
<div class="invalid-feedback">
{{ $message }}
</div>
@enderror
</div>
<div class="form-group">
<label for="guard"
class="label form-control-label">Guard</label>
<input type="text"
name="guard"
id="guard"
placeholder="web"
class="form-control @error('guard') is-invalid @enderror"
value="{{ old('guard', $role->guard) }}">
@error('email')
<div class="invalid-feedback">
{{ $message }}
</div>
@enderror
</div>
<div class="form-group">
<button type="submit"
class="btn btn-primary">Update Role</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
@endsection
At this point we should be able to see the edit role page. Let's implement the update model. Let's start by implementing the UpdateRoleRequest
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class UpdateRoleRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize(): bool
{
return $this->user()->can('update role');
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, mixed>
*/
public function rules(): array
{
return [
'name' =>
'required|string|unique:roles,name,' . $this->route('role')->id,
'guard' => 'sometimes|nullable|string',
];
}
}
And next, let's implement the update action
/**
* Update the specified resource in storage.
*
* @param UpdateRoleRequest $request
* @param Role $role
* @return RedirectResponse
*/
public function update(UpdateRoleRequest $request, Role $role): RedirectResponse
{
$role->update($request->validated());
return redirect()
->route('admin.roles.index')
->with('success', 'Role updated successfully.');
}
Now the next thing is the ability to delete this role.
Open the admin.roles.index
view and add this snippet inside the table tr
tag
<td>
<button data-bs-target="#deleteRoleModal{{ $role->id }}"
data-bs-toggle="modal"
type="submit"
class="btn btn-danger btn-sm">
<i class="fas fa-trash"></i>
Delete
</button>
</td>
and just outside the tr
inside the for
loop add this modal
@push('modals')
<div class="modal fade"
tabindex="-1"
id="deleteRoleModal{{ $role->id }}">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Confirm</h5>
<button type="button"
class="btn-close"
data-bs-dismiss="modal"
aria-label="Close"></button>
</div>
<div class="modal-body">
<p>
Once this action is done some users may lose access to some
parts of the application. Are you sure you want to continue?
</p>
</div>
<div class="modal-footer">
<button type="button"
class="btn btn-secondary"
data-bs-dismiss="modal">Cancel</button>
<a href="{{ route('admin.roles.destroy', $role) }}"
data-turbo-method="delete"
class="btn btn-danger">Delete</a>
</div>
</div>
</div>
</div>
@endpush
And lastly let's implement the delete method in our controller
/**
* Remove the specified resource from storage.
*
* @param Role $role
* @return RedirectResponse
*/
public function destroy(Role $role): RedirectResponse
{
$role->delete();
return redirect()
->route('admin.roles.index')
->with('success', 'Role deleted successfully.');
}
At this point we can manage the roles, create, update, delete. Now let's the ability to assign permissions to a role or the user.
Let's start with the user, We need the ability to assign roles to a user, that way the user would inherit the permission the role has. To start let's create the controller
php artisan make:controller Admin\\UserRoleController -i
and let's define it as follows
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
class UserRoleController extends Controller
{
/**
* Handle the incoming request.
*
* @param Request $request
* @param User $user
* @return RedirectResponse
*/
public function __invoke(Request $request, User $user): RedirectResponse
{
$data = $request->validate([
'roles' => 'required|array',
'roles.*' => 'sometimes|string|exists:\App\Models\Role,name',
]);
$user->syncRoles($data['roles']);
return redirect()
->back(fallback: route('admin.users.edit', $user))
->with('success', 'User roles updated successfully.');
}
}
For this to work, let's register the route
Route::post('/user/{user}/roles', UserRoleController::class)->name(
'users.roles.assign',
);
Finally let's add this snippet in the users edit view at the end of the view inside the section
tag
<div class="border"></div>
<div class="section-body">
<h2 class="section-title">Roles</h2>
<p class="section-lead">
Assign roles to this user
</p>
<div class="container">
<div class="row">
<div class="col-12 col-md-7 ms-auto">
<div class="card">
<div class="card-header">
<h4>Roles</h4>
</div>
<div class="card-body">
<form action="{{ route('admin.users.roles.assign', $user) }}"
method="POST">
@csrf
<div class="form-group">
<label class="form-label">
Roles
</label>
<div class="selectgroup selectgroup-pills">
@foreach ($roles as $role)
<label class="selectgroup-item mb-3">
<input type="checkbox"
name="roles[{{ $role->name }}]"
value="{{ $role->name }}"
class="selectgroup-input"
{{ $user->hasRole($role) || collect(old('roles',[]))->has($role->name) ? 'checked' : '' }}>
<span class="selectgroup-button">{{ $role->name }}</span>
</label>
@endforeach
</div>
</div>
<div class="form-group">
<button type="submit"
class="btn btn-primary">Update Roles</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
As it is, blade would throw errors, let's modify the user edit action in the users controller to add permissions and roles to the view.
/**
* Show the form for editing the specified resource.
*
* @param User $user
* @return Renderable
*/
public function edit(User $user): Renderable
{
$permissions = Permission::all();
$roles = Role::all();
return view('admin.users.edit', [
'user' => $user,
'permissions' => $permissions,
'roles' => $roles,
]);
}
At this point we can assign roles to the user, however, we may want to assign arbitrary permissions that are not inherited from the role.
Let's start again with the controller
php artisan make:controller Admin\\UserPermissionController -i
and then like the user roles controller, let's define it as follows
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
class UserPermissionController extends Controller
{
/**
* Handle the incoming request.
*
* @param Request $request
* @param User $user
* @return RedirectResponse
*/
public function __invoke(Request $request, User $user): RedirectResponse
{
$data = $request->validate([
'permissions' => 'required|array',
'permissions.*' =>
'sometimes|string|exists:\Spatie\Permission\Models\Permission,name',
]);
$user->syncPermissions($data['permissions']);
return redirect()
->back(fallback: route('admin.users.edit', $user))
->with('success', 'User permissions updated successfully.');
}
}
This piece of code syncs the provided permissions to the user. Let's register the route for this to work
Route::post(
'/users/{user}/permissions',
UserPermissionController::class,
)->name('users.permissions.assign');
Finally append the following snippet like we did above,
<div class="border"></div>
<div class="section-body">
<h2 class="section-title">Permissions</h2>
<p class="section-lead">
Assign direct permissions to the user not inherited from roles
</p>
<div class="container">
<div class="row">
<div class="col-12 col-md-7 ms-auto">
<div class="card">
<div class="card-header">
<h4>Permissions</h4>
</div>
<div class="card-body">
<form action="{{ route('admin.users.permissions.assign', $user) }}"
method="POST">
@csrf
@foreach ($permissions->groupBy(fn($permission) => Str::plural(Str::afterLast($permission->name, ' '))) as $model => $modelPermissions)
<div class="form-group">
<label class="form-label">
{{ Str::title($model) }}
</label>
<div class="selectgroup selectgroup-pills">
@foreach ($modelPermissions as $permission)
<label class="selectgroup-item mb-3">
<input type="checkbox"
name="permissions[{{ $permission->name }}]"
value="{{ $permission->name }}"
class="selectgroup-input"
{{ $user->hasDirectPermission($permission) || collect(old('permissions', []))->has($permission->name) ? 'checked' : '' }}>
<span class="selectgroup-button">{{ $permission->name }}</span>
</label>
@endforeach
</div>
</div>
@endforeach
<div class="form-group">
<button type="submit"
class="btn btn-primary">Update Permissions</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
At this point we can assign roles and permission to the user. Next let's add the ability to assign permissions to a role.
Let's start by creating a controller
php artisan make:controller Admin\\RolePermissionController -i
And let's define it as follows
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\Role;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
class RolePermissionController extends Controller
{
/**
* Handle the incoming request.
*
* @param Request $request
* @param Role $role
* @return RedirectResponse
*/
public function __invoke(Request $request, Role $role): RedirectResponse
{
$data = $request->validate([
'permissions' => 'required|array',
'permissions.*' =>
'sometimes|string|exists:\Spatie\Permission\Models\Permission,name',
]);
$role->syncPermissions($data['permissions']);
return redirect()
->back(fallback: route('admin.roles.edit', $role))
->with('success', 'Role permissions updated successfully.');
}
}
Next let's register the route
Route::post(
'/roles/{role}/permissions',
RolePermissionController::class,
)->name('roles.permissions.assign');
And again let's append this snippet in the roles edit view
<div class="border"></div>
<div class="section-body">
<h2 class="section-title">Permissions</h2>
<p class="section-lead">
Assign permissions to this role
</p>
<div class="container">
<div class="row">
<div class="col-12 col-md-7 ms-auto">
<div class="card">
<div class="card-header">
<h4>Permissions</h4>
</div>
<div class="card-body">
<form action="{{ route('admin.roles.permissions.assign', $role) }}"
method="POST">
@csrf
@foreach ($permissions->groupBy(fn($permission) => Str::plural(Str::afterLast($permission->name, ' '))) as $model => $modelPermissions)
<div class="form-group">
<label class="form-label">
{{ Str::title($model) }}
</label>
<div class="selectgroup selectgroup-pills">
@foreach ($modelPermissions as $permission)
<label class="selectgroup-item mb-3">
<input type="checkbox"
name="permissions[{{ $permission->name }}]"
value="{{ $permission->name }}"
class="selectgroup-input"
{{ $role->hasPermissionTo($permission) || collect(old('permissions', []))->has($permission->name) ? 'checked' : '' }}>
<span class="selectgroup-button">{{ $permission->name }}</span>
</label>
@endforeach
</div>
</div>
@endforeach
<div class="form-group">
<button type="submit"
class="btn btn-primary">Update Permissions</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
Let's modify the roles controller edit action to add permissions to the view
/**
* Show the form for editing the specified resource.
*
* @param Role $role
* @return Renderable
*/
public function edit(Role $role): Renderable
{
$permissions = Permission::all();
return view('admin.roles.edit', [
'role' => $role,
'permissions' => $permissions,
]);
}
Right now we can fully manage roles, assign permissions, assign roles to users. Basically all the foundations of our application is now ready.
In the next tutorial we will add the ability to manage product categories in the store. In the mean time, subscribe to the newsletter and get notified when I post the next tutorial.
[convertkit=2542481]