Laravel Ecommerce Tutorial: Part 3, Managing Roles And Permissions

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]