DEV Community

Cover image for Laravel Ecommerce Tutorial: Part 2, Users And Authorization
Given Ncube
Given Ncube

Posted on • Originally published at flixtechs.co.zw

Laravel Ecommerce Tutorial: Part 2, Users And Authorization

In the previous post, we set up our project and installed basic tooling to help with our development. This is the second post on our ongoing series of developing an ecommerce website with Laravel.

In this post we are going to setup user authentication with Laravel Fortify, setup authorization with Spatie Laravel Permission with the help of our very own Laravel Authorizer. Let's get started!

Authentication

We are not going to use a starter kit for our user authentication, instead we are going to setup Laravel Fortify from scratch. To get started, let's install Laravel Fortify.

composer require laravel/fortify
Enter fullscreen mode Exit fullscreen mode

Next, let's publish Fortify's resources using the vendor:publish command:

php artisan vendor:publish --provider="Laravel\Fortify\FortifyServiceProvider"
Enter fullscreen mode Exit fullscreen mode

This command will publish Fortify's actions to your app/Actions directory, which will be created if it does not exist. In addition, the FortifyServiceProvider, configuration file, and all necessary database migrations will be published.

Next, you should migrate your database:

php artisan migrate
Enter fullscreen mode Exit fullscreen mode

Next, add the FortifyServiceProvider to your providers array in your config/app.php file.

Next, we need to create our auth views. Now, we will be using Stisla admin template. Let's start by installing bootstrap css

yarn add --dev @popperjs/core bootstrap
Enter fullscreen mode Exit fullscreen mode

In order to compile our bootstrap css let's add sass

yarn add --dev sass
Enter fullscreen mode Exit fullscreen mode

Next, let's create a resources/sass/app.scss file and add the following

@import "variables";
@import "bootstrap/scss/bootstrap";
Enter fullscreen mode Exit fullscreen mode

Next, create an empty resources/sass/_variables.scss

Now let's tell vite how to handle our sass, define your vite.config.js as follows

import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin';

export default defineConfig({
    plugins: [
        laravel({
            input: ['resources/sass/app.scss', 'resources/js/app.js'],
            refresh: true,
        }),
    ],
});
Enter fullscreen mode Exit fullscreen mode

Next, let's import the Stisla admin template. Go ahead and clone Stisla from GitHub.

git clone https://github.com/stisla/stisla.git
Enter fullscreen mode Exit fullscreen mode

After cloning the repo, copy everything in src/scss/* to your resources/sass directory

After that in your resources/sass/app.scss add the following

@import "style";
Enter fullscreen mode Exit fullscreen mode

Next we need to create our views. But first we need a way to generate views with just one command. Let's install this package to help with that:

composer require 'maddhatter/laravel-view-generator:dev-master' --dev
Enter fullscreen mode Exit fullscreen mode

After installing, let's create our auth layout with

php artisan make:view layouts.auth
Enter fullscreen mode Exit fullscreen mode

In the layout file add the following

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta content="width=device-width, initial-scale=1, maximum-scale=1, shrink-to-fit=no"
          name="viewport">
    <title>Login - Ecommerce</title>

    <link rel="stylesheet"
          href="https://use.fontawesome.com/releases/v5.7.2/css/all.css"
          integrity="sha384-fnmOCqbTlWIlj8LyTjo7mOUStjsKC4pOpQbqyi7RrhN7udi9RwhKkMHpvLbHG9Sr"
          crossorigin="anonymous">

    @vite(['resources/sass/app.scss', 'resources/js/app.js'])

</head>

<body>
    <div id="app">
        @yield('content')
    </div>
</body>

</html>
Enter fullscreen mode Exit fullscreen mode

Next, create the resources/auth/login.blade.php and add the following

@extends('layouts.auth')

@section('content')
   <section class="">
        <div class="d-flex flex-wrap align-items-center">
            <div class="col-lg-4 col-md-6 col-12 order-lg-1 min-vh-100 order-2 bg-white">
                <div class="p-4 m-3 mt-md-5 py-md-5">
                    <h4 class="text-dark font-weight-normal">Welcome to <span class="fw-bold">Ecommerce</span></h4>
                    <p class="text-muted">
                        Before you get started, you must login or register if you don't already have an
                        account.
                    </p>

                    <form method="POST"
                          action="{{ route('login') }}"
                          class="needs-validation"
                          novalidate="">
                        @csrf
                        <div class="form-group">
                            <label for="email">Email</label>
                            <input id="email"
                                   type="email"
                                   class="form-control @error('email') is-invalid @enderror"
                                   name="email"
                                   tabindex="1"
                                   required
                                   autofocus>
                            @error('email')
                                <div class="invalid-feedback">
                                    Please fill in your email
                                </div>
                            @enderror
                        </div>

                        <div class="form-group">
                            <div class="d-block">
                                <label for="password"
                                       class="control-label">Password</label>
                            </div>
                            <input id="password"
                                   type="password"
                                    class="form-control @error('password') is-invalid @enderror"
                                   name="password"
                                   tabindex="2"
                                   required>
                            @error('password')
                                <div class="invalid-feedback">
                                    please fill in your password
                                </div>
                            @enderror
                        </div>

                        <div class="form-group">
                            <div class="custom-control custom-checkbox">
                                <input type="checkbox"
                                       name="remember"
                                       class="custom-control-input @error('remember') is-invalid @enderror"
                                       tabindex="3"
                                       id="remember-me">
                                <label class="custom-control-label"
                                       for="remember-me">Remember Me</label>

                            </div>
                        </div>

                        <div class="form-group text-end">
                            <a href="{{ route('password.request') }}"
                               class="float-start mt-3">
                                Forgot Password?
                            </a>
                            <button type="submit"
                                    class="btn btn-primary btn-lg btn-icon icon-right"
                                    tabindex="4">
                                Login
                            </button>
                        </div>

                        <div class="mt-5 text-center">
                            Don't have an account? <a href="{{ route('register') }}">Create new one</a>
                        </div>
                    </form>


                    <div class="text-center mt-5 text-small">
                        Copyright {{ date('Y') }} Ecommerce. All rights reserved.
                    </div>
                </div>
            </div>
            <div class="col-lg-8 col-12 order-lg-2 order-1 min-vh-100 background-walk-y position-relative overlay-gradient-bottom"
                 style="background-image: url('https://images.unsplash.com/photo-1672862817339-51ef2610a5d0?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=2109&q=80'); background-size: cover">
                <div class="absolute-bottom-left index-2">
                    <div class="text-light p-5 pb-2">
                        <div class="mb-5 pb-3">
                            <h1 class="mb-2 display-4 font-weight-bold">Good Morning</h1>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </section>
@endsection
Enter fullscreen mode Exit fullscreen mode

Let's tell Fortify how to return the login response. In the FortifyServiceProvider boot method

Fortify::loginView(static function () {
    return view('auth.login');
});
Enter fullscreen mode Exit fullscreen mode

Use the Stisla templates to create other auth views, forgot-password, password reset, etc

Authorization

Now, after creating our auth views it's time to setup authorization.

To make authorization easier let's add our very own laravel-authorizer

composer require flixtechs-labs/laravel-authorizer
Enter fullscreen mode Exit fullscreen mode

And set it up by running

php artisan authorizer:setup
Enter fullscreen mode Exit fullscreen mode

In the User model add the following trait

use Spatie\Permission\Traits\HasRoles;

class User extends Authenticatable
{
    use HasRoles;
...
Enter fullscreen mode Exit fullscreen mode

We need a way to create the super admin user. We can do that by using an artisan command. Let's create the command.

php artisan make:command MakeAdmin --test --command="make:admin"
Enter fullscreen mode Exit fullscreen mode

Since we are responsible developers, let's start by writing a test for our command. In the tests/Feature/Console/Commands/MakeAdminTest.php add the following

<?php

namespace Tests\Feature\Console\Commands;

use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

class MakeAdminTest extends TestCase
{
    use RefreshDatabase;

    /**
     * A basic feature test example.
     *
     * @return void
     */
    public function test_can_make_admin_user_even_if_user_does_not_exist(): void
    {
        $this->artisan('make:admin')
            ->expectsQuestion(
                'What is the email of the user you want to make admin?',
                'admin@example.com',
            )
            ->expectsQuestion('What is their name?', 'Admin')
            ->expectsQuestion(
                'What is the password of the user you want to make admin?',
                'password',
            )
            ->expectsQuestion(
                'Please confirm the password of the user you want to make admin?',
                'password',
            )
            ->expectsOutput(
                'User admin@example.com now has full access to your site.',
            );

        $this->assertDatabaseHas('users', [
            'email' => 'admin@example.com',
        ]);

        $this->assertContains(
            'super admin',
            User::where('email', 'admin@example.com')
                ->first()
                ->roles->pluck('name'),
        );
    }

    public function test_can_make_admin_if_user_exists(): void
    {
        $user = User::factory()->create();

        $this->artisan('make:admin')
            ->expectsQuestion(
                'What is the email of the user you want to make admin?',
                $user->email,
            )
            ->expectsOutput(
                'User ' . $user->email . ' now has full access to your site.',
            );
    }
}
Enter fullscreen mode Exit fullscreen mode

Let's proceed to write our command as follows

<?php

namespace App\Console\Commands;

use App\Actions\Fortify\PasswordValidationRules;
use App\Models\User;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Validator;
use Spatie\Permission\Models\Role;

class MakeAdmin extends Command
{
    use PasswordValidationRules;

    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'make:admin';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'Create a user with super admin access';

    /**
     * Execute the console command.
     *
     * @return int
     */
    public function handle(): int
    {
        $user = $this->getUserInfo();

        if (!$user) {
            return 0;
        }

        $this->assignRole($user);

        $this->info(
            'User ' . $user->email . ' now has full access to your site.',
        );

        return 1;
    }

    /**
     * Get the user information from the user.
     *
     * @return bool|User
     */
    public function getUserInfo(): bool|User
    {
        $email = $this->ask(
            'What is the email of the user you want to make admin?',
        );

        $user = User::where('email', $email)->first();

        if (!is_null($user)) {
            return $user;
        }

        $name = $this->ask('What is their name?');
        $password = $this->secret(
            'What is the password of the user you want to make admin?',
        );

        $passwordConfirmation = $this->secret(
            'Please confirm the password of the user you want to make admin?',
        );

        $validator = Validator::make(
            [
                'name' => $name,
                'email' => $email,
                'password' => $password,
                'password_confirmation' => $passwordConfirmation,
            ],
            [
                'name' => ['required', 'string', 'max:255'],
                'email' => [
                    'required',
                    'string',
                    'email',
                    'max:255',
                    'unique:users',
                ],
                'password' => $this->passwordRules(),
            ],
        );

        if ($validator->fails()) {
            $this->error('Operation failed. Please check errors below:');

            foreach ($validator->errors()->all() as $error) {
                $this->error($error);
            }

            return false;
        }

        return User::create([
            ...$validator->validated(),
            'password' => Hash::make($password),
        ]);
    }

    /**
     * Assign the super admin role to the user
     *
     * @param User $user
     * @return void
     */
    public function assignRole(User $user): void
    {
        $role = Role::findOrCreate('super admin');
        $user->assignRole($role);
    }
}
Enter fullscreen mode Exit fullscreen mode

The above code first prompts for an email address. If the user with that email address exists, it then assigns the super admin role to that user, else it asks for the name and password and creates a new user who is then assigned the super admin role.

Now go ahead and create your first super admin user

php artisan make:admin
Enter fullscreen mode Exit fullscreen mode

Next let's configure our gates to allow anywhere anyone with a super admin role.

In your AuthServiceProvider

/**
 * Register any authentication / authorization services.
 *
 * @return void
 */
 public function boot()
 {
    $this->registerPolicies();

    Gate::before(static function ($user, $ability) {
        return $user->hasRole('super admin') ? true : null;
    });
 }
Enter fullscreen mode Exit fullscreen mode

At this point, we have set up our authentication with Laravel Forty, our authorization is ready and at this point allows anyone with a super admin role in. Now let's create a basic dashboard home page.

Let's start by creating our controller

php artisan make:controller Admin\\HomeController --test
Enter fullscreen mode Exit fullscreen mode

Like the good developers we are, let's write some tests for the HomeController. In the tests/Feature/Http/Controllers/Admin/HomeControllerTest.php add the following

<?php

namespace Tests\Feature\Http\Controllers\Admin;

use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithFaker;
use Spatie\Permission\Models\Permission;
use Tests\TestCase;

class HomeControllerTest extends TestCase
{
    use RefreshDatabase;

    /**
     * Test that the admin home page is accessible to super admins.
     *
     * @return void
     */
    public function test_can_see_admin_home_page_if_user_is_super_admin(): void
    {
        $this->actingAs($this->superAdminUser())
            ->get(route('admin.home.index'))
            ->assertStatus(200)
            ->assertViewIs('admin.home.index')
            ->assertSee('Dashboard');
    }

    /**
     * Test that a user cannot see the admin home page if they are not a super admin.
     *
     * @return void
     */
    public function test_cannot_see_admin_home_page_if_user_is_not_super_admin(): void
    {
        $this->actingAs($this->user())
            ->get(route('admin.home.index'))
            ->assertForbidden();
    }

    /**
     * Test that the user is redirected to the login page if they are not logged in.
     *
     * @return void
     */
    public function test_cannot_see_admin_home_page_if_user_is_not_logged_in(): void
    {
        $this->get(route('admin.home.index'))->assertRedirect(route('login'));
    }

    /**
     * Test that the admin home page is accessible if user has the permission
     *
     * @return void
     */
    public function test_can_see_admin_home_page_if_user_has_permission(): void
    {
        $user = $this->user();
        $user->givePermissionTo(
            Permission::findOrCreate('view admin dashboard'),
        );

        $this->actingAs($user)
            ->get(route('admin.home.index'))
            ->assertStatus(200)
            ->assertViewIs('admin.home.index')
            ->assertSee('Dashboard');
    }
}
Enter fullscreen mode Exit fullscreen mode

And in your tests/TestCase.php

<?php

namespace Tests;

use App\Models\User;
use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
use Spatie\Permission\Models\Role;

abstract class TestCase extends BaseTestCase
{
    use CreatesApplication;

    /**
     * Create a super admin user.
     *
     * @return User
     */
    protected function superAdminUser(): User
    {
        $user = User::factory()->create();
        $user->assignRole(Role::findOrCreate('super admin'));

        return $user;
    }

    /**
     * Create a user.
     *
     * @return User
     */
    protected function user(): User
    {
        return User::factory()->create();
    }
}
Enter fullscreen mode Exit fullscreen mode

To pass the first test test_can_see_admin_home_page_if_user_is_super_admin add the following route in the routes/web.php file

Route::prefix('admin')
    ->middleware(['auth', 'permission:view admin dashboard'])
    ->name('admin.')
    ->group(static function () {
        Route::get('/', [HomeController::class, 'index'])->name('home.index');
    });
Enter fullscreen mode Exit fullscreen mode

Then register the following middleware from spate/laravel-permission in your Kernel.php file like this

protected $routeMiddleware = [
    // ...
    'role' => \Spatie\Permission\Middlewares\RoleMiddleware::class,
    'permission' => \Spatie\Permission\Middlewares\PermissionMiddleware::class,
    'role_or_permission' => \Spatie\Permission\Middlewares\RoleOrPermissionMiddleware::class,
];
Enter fullscreen mode Exit fullscreen mode

Setting up the layout

Next let's create our layout

php artisan make:view layouts.app
Enter fullscreen mode Exit fullscreen mode

And add the following

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta content="width=device-width, initial-scale=1, maximum-scale=1, shrink-to-fit=no"
          name="viewport">
    <meta name="csrf-token"
          content="{{ csrf_token() }}">
    <title>Ecommerce Dashboard - @yield('title')</title>

    <link rel="stylesheet"
          href="https://use.fontawesome.com/releases/v5.7.2/css/all.css"
          integrity="sha384-fnmOCqbTlWIlj8LyTjo7mOUStjsKC4pOpQbqyi7RrhN7udi9RwhKkMHpvLbHG9Sr"
          crossorigin="anonymous">

    @vite(['resources/sass/app.scss', 'resources/js/app.js'])
</head>

<body>
    <div id="app">
        <div class="main-wrapper">
            <div class="navbar-bg"></div>

            @include('layouts.partials.navbar')

            @include('layouts.partials.sidebar')

            <div class="main-content">
                @yield('content')
            </div>

            @include('layouts.partials.footer')
        </div>
    </div>
</body>

</html>
Enter fullscreen mode Exit fullscreen mode

And then the navbar partial

php artisan make:view layouts.partials.navbar
Enter fullscreen mode Exit fullscreen mode

and define it as follows

<nav class="navbar navbar-expand-lg main-navbar">

    <ul class="navbar-nav navbar-right ms-auto">

        <li class="dropdown dropdown-list-toggle">
            <a href="#"
               data-toggle="dropdown"
               class="nav-link notification-toggle nav-link-lg">
                <i class="far fa-bell"></i>
            </a>
            <div class="dropdown-menu dropdown-list dropdown-menu-right">

            </div>
        </li>

        <li class="dropdown">
            <a href="#"
               data-bs-toggle="dropdown"
               class="nav-link dropdown-toggle nav-link-lg nav-link-user">
                <div class="d-sm-none d-lg-inline-block">Hi, {{ auth()->user()->name }}</div>
            </a>
            <div class="dropdown-menu dropdown-menu-right">
                <div class="dropdown-title">Logged in 5 min ago</div>
                <a href="#"
                   class="dropdown-item has-icon">
                    <i class="far fa-user"></i> Profile
                </a>

                <div class="dropdown-divider"></div>

                <form action="{{ route('logout') }}"
                      method="post"
                      id="logout">
                    @csrf
                    <a onclick="document.getElementById('logout').requestSubmit()"
                       data-turbo-method="post"
                       class="dropdown-item has-icon text-danger">
                        <i class="fas fa-sign-out-alt"></i> Logout
                    </a>
                </form>

            </div>
        </li>
    </ul>
</nav>
Enter fullscreen mode Exit fullscreen mode

And for the sidebar

php artisan make:view layouts.partials.sidebar
Enter fullscreen mode Exit fullscreen mode

Define it as follows

<div class="main-sidebar sidebar-style-2">
    <aside id="sidebar-wrapper">
        <div class="sidebar-brand text-start px-3">
            <a href="{{ route('admin.home.index') }}">Ecommerce</a>
        </div>
        <div class="sidebar-brand sidebar-brand-sm px-2 text-start">
            <a href="{{ route('admin.home.index') }}">Ec</a>
        </div>
        <ul class="sidebar-menu">
            <li class="menu-header">Dashboard</li>
            <li class="nav-item @if (Route::is('admin.home.index')) active @endif">
                <a href="{{ route('admin.home.index') }}"
                   class="nav-link">
                    <i class="fas fa-fire"></i> <span>Dashboard</span>
                </a>
            </li>
        </ul>
    </aside>
</div>
Enter fullscreen mode Exit fullscreen mode

Lastly, the footer

php artisan make:view layouts.partials.footer
Enter fullscreen mode Exit fullscreen mode

with the following content

<footer class="main-footer">
    <div class="footer-left">
        Copyright {{ date('Y') }} Ecommerce. All rights reserved.
    </div>
    <div class="footer-right">
        2.3.0
    </div>
</footer>
Enter fullscreen mode Exit fullscreen mode

After this, our layout is ready for use.

Looking at our test, we asserted that viewIs('admin.home.index'), let's go ahead and create that view.

php artisan make:view admin.home.index -e layouts.app
Enter fullscreen mode Exit fullscreen mode

We also asserted that we will see 'Dashboard' so let's define our simple view like this for now

@extends('layouts.app')

@section('content')
    <section class="section">
        <div class="section-header">
            <h1>Dashboard</h1>
        </div>

        <div class="section-body">
            <h2 class="section-title">Welcome to Ecommerce Dashboard</h2>
            <p class="section-lead">This page is just an example for you to create your own page.</p>
        </div>
    </section>
@endsection
Enter fullscreen mode Exit fullscreen mode

Lastly let's tell our HomeController to return that view in the index action

<?php

namespace App\Http\Controllers\Admin;

use App\Http\Controllers\Controller;
use Illuminate\Contracts\Support\Renderable;
use Illuminate\Http\Request;
use Illuminate\Http\Response;

class HomeController extends Controller
{
    /**
     * Display the admin home page.
     *
     * @return Renderable
     */
    public function index(): Renderable
    {
        return view('admin.home.index');
    }
}
Enter fullscreen mode Exit fullscreen mode

Now let's run our tests

php artisan test --filter HomeControllerTest::test_can_see_admin_home_page_if_user_is_super_admin
Enter fullscreen mode Exit fullscreen mode

And with that all our other will pass as well

php artisan test
Enter fullscreen mode Exit fullscreen mode

Users

Now we need a way for the super admin to create other users and create and assign roles like admin, seo specialist, marketer, etc and give them permissions like edit products, etc

Let's starting by generating the UserController

php artisan make:controller Admin\\UserController -m User --test -R
Enter fullscreen mode Exit fullscreen mode

Like the responsible developers we are, let's start by writing a bunch of tests

<?php

namespace Tests\Feature\Http\Controllers\Admin;

use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithFaker;
use Spatie\Permission\Models\Permission;
use Tests\TestCase;

class UserControllerTest extends TestCase
{
    use RefreshDatabase;
    use withFaker;

    /**
     * Test that the admin user index page is accessible to super admins.
     *
     * @return void
     */
    public function test_can_see_admin_user_index_page_if_user_is_super_admin(): void
    {
        $this->actingAs($this->superAdminUser())
            ->get(route('admin.users.index'))
            ->assertStatus(200)
            ->assertViewIs('admin.users.index')
            ->assertSee('Users')
            ->assertViewHas('users');
    }

    /**
     * Test that a user cannot see the admin user index page if they are not a super admin.
     *
     * @return void
     */
    public function test_cannot_see_admin_user_index_page_if_user_is_not_super_admin(): void
    {
        $this->actingAs($this->user())
            ->get(route('admin.users.index'))
            ->assertForbidden();
    }

    /**
     * Test that the user is redirected to the login page if they are not logged in.
     *
     * @return void
     */
    public function test_cannot_see_admin_user_index_page_if_user_is_not_logged_in(): void
    {
        $this->get(route('admin.users.index'))->assertRedirect(route('login'));
    }

    /**
     * Test that the admin user index page is accessible if user has the permission
     *
     * @return void
     */
    public function test_can_see_admin_user_index_page_if_user_has_permission(): void
    {
        $user = $this->user();
        $user->givePermissionTo([
            Permission::findOrCreate('view all users'),
            Permission::findOrCreate('view admin dashboard'),
        ]);

        $this->actingAs($user)
            ->get(route('admin.users.index'))
            ->assertStatus(200)
            ->assertViewIs('admin.users.index')
            ->assertSee('Users');
    }

    /**
     * Test that the admin user create page is accessible to super admins.
     *
     * @return void
     */
    public function test_can_display_create_user_page_if_user_is_super_admin(): void
    {
        $this->actingAs($this->superAdminUser())
            ->get(route('admin.users.create'))
            ->assertStatus(200)
            ->assertViewIs('admin.users.create')
            ->assertSee('Create User');
    }

    /**
     * Test that a user cannot see the admin user create page if they are not a super admin.
     *
     * @return void
     */
    public function test_cannot_display_create_user_page_if_user_is_not_super_admin(): void
    {
        $this->actingAs($this->user())
            ->get(route('admin.users.create'))
            ->assertForbidden();
    }

    /**
     * Test that the user is redirected to the login page if they are not logged in.
     *
     * @return void
     */
    public function test_cannot_display_create_user_page_if_user_is_not_logged_in(): void
    {
        $this->get(route('admin.users.create'))->assertRedirect(route('login'));
    }

    /**
     * Test that the admin user create page is accessible if user has the permission
     *
     * @return void
     */
    public function test_can_display_create_user_page_if_user_has_permission(): void
    {
        $user = $this->user();
        $user->givePermissionTo(
            Permission::findOrCreate('create user'),
            Permission::findOrCreate('view admin dashboard'),
        );

        $this->actingAs($user)
            ->get(route('admin.users.create'))
            ->assertStatus(200)
            ->assertViewIs('admin.users.create')
            ->assertSee('Create User');
    }

    /**
     * Test that the admin user create page is not accessible if user does not have the permission
     *
     * @return void
     */
    public function test_cannot_display_create_user_page_if_user_does_not_have_permission(): void
    {
        $user = $this->user();

        $this->actingAs($user)
            ->get(route('admin.users.create'))
            ->assertForbidden();
    }

    /**
     * Test that the admin user store page is accessible to super admins.
     *
     * @return void
     */
    public function test_can_store_user_if_user_is_super_admin(): void
    {
        $this->actingAs($this->superAdminUser())
            ->post(route('admin.users.store'), [
                'name' => 'Test User',
                'email' => $this->faker()->safeEmail,
                'password' => 'password',
                'password_confirmation' => 'password',
            ])
            ->assertRedirect(route('admin.users.index'))
            ->assertSessionHas('success', 'User created successfully.')
            ->assertSessionMissing('error');

        $this->assertCount(2, User::all());
    }

    /**
     * Test that a user cannot store a user if they are not a super admin.
     *
     * @return void
     */
    public function test_cannot_store_user_if_user_is_not_super_admin(): void
    {
        $this->actingAs($this->user())
            ->post(route('admin.users.store'), [
                'name' => 'Test User',
                'email' => $this->faker()->safeEmail,
                'password' => 'password',
                'password_confirmation' => 'password',
            ])
            ->assertForbidden();
    }

    /**
     * Test that the user is redirected to the login page if they are not logged in.
     *
     * @return void
     */
    public function test_cannot_store_user_if_user_is_not_logged_in(): void
    {
        $this->post(route('admin.users.store'), [
            'name' => 'Test User',
            'email' => $this->faker()->safeEmail,
            'password' => 'password',
            'password_confirmation' => 'password',
        ])->assertRedirect(route('login'));
    }

    /**
     * Test that the admin user store page is accessible if user has the permission
     *
     * @return void
     */
    public function test_can_store_user_if_user_has_permission(): void
    {
        $user = $this->user();
        $user->givePermissionTo(
            Permission::findOrCreate('create user'),
            Permission::findOrCreate('view admin dashboard'),
        );

        $this->actingAs($user)
            ->post(route('admin.users.store'), [
                'name' => 'Test User',
                'email' => $this->faker()->safeEmail,
                'password' => 'password',
                'password_confirmation' => 'password',
            ])
            ->assertRedirect(route('admin.users.index'))
            ->assertSessionHas('success', 'User created successfully.')
            ->assertSessionMissing('error');

        $this->assertCount(2, User::all());
    }

    /**
     * Test that the admin user store page is not accessible if user does not have the permission
     *
     * @return void
     */
    public function test_cannot_store_user_if_user_does_not_have_permission(): void
    {
        $user = $this->user();

        $this->actingAs($user)
            ->post(route('admin.users.store'), [
                'name' => 'Test User',
                'email' => $this->faker()->safeEmail,
                'password' => 'password',
                'password_confirmation' => 'password',
            ])
            ->assertForbidden();
    }

    /**
     * Test that the request is validated when creating a user.
     *
     * @return void
     */
    public function test_can_validate_user_store_request(): void
    {
        $this->actingAs($this->superAdminUser())
            ->post(route('admin.users.store'), [
                'name' => '',
                'email' => '',
                'password' => '',
                'password_confirmation' => '',
            ])
            ->assertSessionHasErrors(['name', 'email', 'password']);
    }

    /**
     * Test that the admin user edit page is accessible to super admins.
     *
     * @return void
     */
    public function test_can_display_edit_user_page_if_user_is_super_admin(): void
    {
        $user = $this->user();

        $this->actingAs($this->superAdminUser())
            ->get(route('admin.users.edit', $user))
            ->assertStatus(200)
            ->assertViewIs('admin.users.edit')
            ->assertSee('Edit User');
    }

    /**
     * Test that a user cannot see the admin user edit page if they are not a super admin.
     *
     * @return void
     */

    public function test_cannot_display_edit_user_page_if_user_is_not_super_admin(): void
    {
        $user = $this->user();

        $this->actingAs($this->user())
            ->get(route('admin.users.edit', $user))
            ->assertForbidden();
    }

    /**
     * Test that the user is redirected to the login page if they are not logged in.
     *
     * @return void
     */
    public function test_cannot_display_edit_user_page_if_user_is_not_logged_in(): void
    {
        $user = $this->user();

        $this->get(route('admin.users.edit', $user))->assertRedirect(
            route('login'),
        );
    }

    /**
     * Test that the admin user edit page is accessible if user has the permission
     *
     * @return void
     */
    public function test_can_display_edit_user_page_if_user_has_permission(): void
    {
        $user = $this->user();
        $user->givePermissionTo(
            Permission::findOrCreate('update user'),
            Permission::findOrCreate('view admin dashboard'),
        );

        $this->actingAs($user)
            ->get(route('admin.users.edit', $user))
            ->assertStatus(200)
            ->assertViewIs('admin.users.edit')
            ->assertSee('Edit User');
    }

    /**
     * Test that the admin user edit page is not accessible if user does not have the permission
     *
     * @return void
     */
    public function test_cannot_display_edit_user_page_if_user_does_not_have_permission(): void
    {
        $user = $this->user();

        $this->actingAs($user)
            ->get(route('admin.users.edit', $user))
            ->assertForbidden();
    }

    /**
     * Test that the admin user update page is accessible to super admins.
     *
     * @return void
     */
    public function test_can_update_user_if_user_is_super_admin(): void
    {
        $user = $this->user();

        $this->actingAs($this->superAdminUser())
            ->put(route('admin.users.update', $user), [
                'name' => 'Test User',
                'email' => $this->faker()->safeEmail,
            ])
            ->assertRedirect(route('admin.users.index'))
            ->assertSessionHas('success', 'User updated successfully.')
            ->assertSessionMissing('error');

        $this->assertEquals('Test User', $user->fresh()->name);
        $this->assertNotEquals($user->email, $user->fresh()->email);
    }

    /**
     * Test that a user cannot update a user if they are not a super admin.
     *
     * @return void
     */
    public function test_cannot_update_user_if_user_is_not_super_admin(): void
    {
        $user = $this->user();

        $this->actingAs($this->user())
            ->put(route('admin.users.update', $user), [
                'name' => 'Test User',
                'email' => $this->faker()->safeEmail,
            ])
            ->assertForbidden();

        $this->assertNotEquals('Test User', $user->fresh()->name);
        $this->assertEquals($user->email, $user->fresh()->email);
    }

    /**
     * Test that the user is redirected to the login page if they are not logged in.
     *
     * @return void
     */
    public function test_cannot_update_user_if_user_is_not_logged_in(): void
    {
        $user = $this->user();

        $this->put(route('admin.users.update', $user), [
            'name' => 'Test User',
            'email' => $this->faker()->safeEmail,
        ])->assertRedirect(route('login'));
    }

    /**
     * Test that the admin user update page is accessible if user has the permission
     *
     * @return void
     */
    public function test_can_update_user_if_user_has_permission(): void
    {
        $user = $this->user();
        $user->givePermissionTo(
            Permission::findOrCreate('update user'),
            Permission::findOrCreate('view admin dashboard'),
        );

        $this->actingAs($user)
            ->put(route('admin.users.update', $user), [
                'name' => 'Test User',
                'email' => $this->faker()->safeEmail,
            ])
            ->assertRedirect(route('admin.users.index'))
            ->assertSessionHas('success', 'User updated successfully.')
            ->assertSessionMissing('error');

        $this->assertEquals('Test User', $user->fresh()->name);
        $this->assertNotEquals($user->email, $user->fresh()->email);
    }

    /**
     * Test that the admin user update page is not accessible if user does not have the permission
     *
     * @return void
     */
    public function test_cannot_update_user_if_user_does_not_have_permission(): void
    {
        $user = $this->user();

        $this->actingAs($user)
            ->put(route('admin.users.update', $user), [
                'name' => 'Test User',
                'email' => $this->faker()->safeEmail,
            ])
            ->assertForbidden();
    }

    /**
     * Test that the admin user delete page is accessible to super admins.
     *
     * @return void
     */
    public function test_can_delete_user_if_user_is_super_admin(): void
    {
        $user = $this->user();

        $this->actingAs($this->superAdminUser())
            ->delete(route('admin.users.destroy', $user))
            ->assertRedirect(route('admin.users.index'))
            ->assertSessionHas('success', 'User deleted successfully.')
            ->assertSessionMissing('error');
    }

    /**
     * Test that a user cannot delete a user if they are not a super admin.
     *
     * @return void
     */
    public function test_cannot_delete_user_if_user_is_not_super_admin(): void
    {
        $user = $this->user();

        $this->actingAs($this->user())
            ->delete(route('admin.users.destroy', $user))
            ->assertForbidden();
    }

    /**
     * Test that the user is redirected to the login page if they are not logged in.
     *
     * @return void
     */
    public function test_cannot_delete_user_if_user_is_not_logged_in(): void
    {
        $user = $this->user();

        $this->delete(route('admin.users.destroy', $user))->assertRedirect(
            route('login'),
        );
    }

    /**
     * Test that the admin user delete page is accessible if user has the permission
     *
     * @return void
     */
    public function test_can_delete_user_if_user_has_permission(): void
    {
        $user = $this->user();
        $user->givePermissionTo(
            Permission::findOrCreate('delete user'),
            Permission::findOrCreate('view admin dashboard'),
        );

        $this->actingAs($user)
            ->delete(route('admin.users.destroy', $user))
            ->assertRedirect(route('admin.users.index'))
            ->assertSessionHas('success', 'User deleted successfully.')
            ->assertSessionMissing('error');
    }

    /**
     * Test that the admin user delete page is not accessible if user does not have the permission
     *
     * @return void
     */
    public function test_cannot_delete_user_if_user_does_not_have_permission(): void
    {
        $user = $this->user();

        $this->actingAs($user)
            ->delete(route('admin.users.destroy', $user))
            ->assertForbidden();
    }
}
Enter fullscreen mode Exit fullscreen mode

To pass our first test let's define the routes for the users. In the admin. route group add

Route::resource('users', UserController::class);
Enter fullscreen mode Exit fullscreen mode

Next let's add the users link to the sidebar. Below the Dashboard link add

<li class="menu-header">Users</li>
<li class="nav-item @if (Route::is('admin.users.*')) active @endif">
    <a href="{{ route('admin.users.index') }}" class="nav-link">
        <i class="fas fa-users"></i> <span>Users</span>
    </a>
</li>
Enter fullscreen mode Exit fullscreen mode

Next, we asserted that the viewIs('admin.users.index') let's create that view

php artisan make:view admin.users.index
Enter fullscreen mode Exit fullscreen mode

We also asserted that we will see Users and that the view data has $users. Let's define our index view as follow

@extends('layouts.app')

@content('title')
    Users
@endcontent

@section('content')
    <section class="section">
        <div class="section-header">
            <h1>Users</h1>
            <div class="section-header-button">
                <a href="{{ route('admin.users.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.users.index') }}">Users</a></div>
                <div class="breadcrumb-item">All Users</div>
            </div>
        </div>
        <div class="section-body">
            <h2 class="section-title">Users</h2>
            <p class="section-lead">
                You can manage all users, such as editing, deleting and more.
            </p>

            <div class="row mt-4">
                <div class="col-12">
                    <div class="card">
                        <div class="card-header">
                            <h4>All Users</h4>
                        </div>
                        <div class="card-body">
                            <div class="table-responsive">
                                <table class="table table-striped">
                                    <tr>
                                        <th>Name</th>
                                        <th>Email</th>
                                        <th>Role</th>
                                        <th>Registered At</th>
                                    </tr>
                                    @foreach ($users as $user)
                                        <tr>
                                            <td>
                                                <a href="#">
                                                    <img alt="image"
                                                         src="https://res.cloudinary.com/dwinzyahj/image/upload/v1609855422/rqyxywrhl5vis0dnaenc.png"
                                                         class="rounded-circle"
                                                         width="35"
                                                         data-toggle="title"
                                                         title="">
                                                    <div class="d-inline-block ml-2">{{ $user->name }}</div>
                                                </a>

                                                <div class="table-links"
                                                     data-controller="obliterate"
                                                     data-obliterate-trash-value="1">
                                                    <a href="{{ route('admin.users.edit', $user) }}">Edit</a>
                                                    <div class="bullet"></div>
                                                    @if ($user->id !== auth()->user()->id)
                                                        <a data-action="click->obliterate#handle"
                                                           href="javascrpit:void(0)"
                                                           class="btn btn-link text-danger">Trash</a>
                                                        <form
                                                              method="POST"
                                                              action="{{ route('admin.users.destroy', $user) }}">
                                                            @csrf
                                                            @method('DELETE')
                                                        </form>
                                                    @endif
                                                </div>
                                            </td>

                                            <td>{{ $user->email }}</td>
                                            <td>{{ $user->roles?->first()?->name ?? 'None' }}</td>
                                            <td>{{ $user->created_at->diffForHumans() }}</td>
                                        </tr>
                                    @endforeach
                                </table>
                            </div>

                            <div class="float-right">
                                {{ $users->links('vendor.pagination.bootstrap-5') }}
                            </div>
                        </div>
                    </div>
                </div>
            </div>

        </div>
    </section>
@endsection
Enter fullscreen mode Exit fullscreen mode

And then publish the laravel pagination inorder to use bootstrap 5 pagination views.

Let's tell our UserController to return this view

/**
 * Display a listing of the resource.
 *
 * @return Renderable
 */
public function index(): Renderable
{
    $users = User::paginate(10);

    return view('admin.users.index', [
        'users' => $users,
    ]);
}
Enter fullscreen mode Exit fullscreen mode

Before we run our test let's generate the authorization code

php artisan authorizer:permissions:generate -m User
Enter fullscreen mode Exit fullscreen mode
php artisan authorizer:policies:generate -m User
Enter fullscreen mode Exit fullscreen mode

This will generate a UserPolicy and the permissions to create, update, view users.

Let's tell the UserController to authorize all actions by adding this constructor

/**
 * Create a new controller instance.
 */
public function __construct()
{
    $this->authorizeResource(User::class, 'user');
}
Enter fullscreen mode Exit fullscreen mode

Let's now run our test

php artisan test --filter UserControllerTest::test_can_see_admin_user_index_page_if_user_is_super_admin
Enter fullscreen mode Exit fullscreen mode

Moving on to the next test, let's create the admin.users.create view

php artisan make:view admin.users.create -e layouts.app
Enter fullscreen mode Exit fullscreen mode

And populate it with

@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 User</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.users.index') }}">Users</a></div>
                <div class="breadcrumb-item">Create</div>
            </div>
        </div>
        <div class="section-body">
            <h2 class="section-title">Create User</h2>
            <p class="section-lead">
                You can add new user accounts to the website
            </p>

            <div class="container">
                <div class="row">
                    <div class="col-12 col-md-7 ms-auto">
                        <div class="card">
                            <div class="card-header">
                                <h4>User information</h4>
                            </div>
                            <div class="card-body">
                                <form action="{{ route('admin.users.store') }}"
                                      method="POST">
                                    @csrf

                                    <div class="form-group">
                                        <label for="email"
                                               class="label form-control-label">Name</label>
                                        <input type="text"
                                               name="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="email"
                                               class="label form-control-label">Email address</label>
                                        <input type="email"
                                               name="email"
                                               class="form-control @error('email') is-invalid @enderror"
                                               value="{{ old('email') }}">
                                        @error('email')
                                            <div class="invalid-feedback">
                                                {{ $message }}
                                            </div>
                                        @enderror
                                    </div>
                                    <div class="row">
                                        <div class="col-md-6 form-group">
                                            <label for="">New password</label>
                                            <input type="password"
                                                   autocomplete="new-password"
                                                   name="password"
                                                   class="form-control @error('password') is-invalid @enderror"">
                                            @error('password')
                                                <div class="invalid-feedback">
                                                    {{ $message }}
                                                </div>
                                            @enderror
                                        </div>
                                        <div class="col-md-6 form-group">
                                            <label for="">Confirm password</label>
                                            <input type="password"
                                                   autocomplete="new-password"
                                                   name="password_confirmation"
                                                   class="form-control @error('password_confirmation') is-invalid @enderror">
                                            @error('password_confirmation')
                                                <div class="invalid-feedback">
                                                    {{ $message }}
                                                </div>
                                            @enderror
                                        </div>
                                    </div>
                                    <div class="form-group">
                                        <button type="submit"
                                                class="btn btn-primary">Create user</button>
                                    </div>
                                </form>
                            </div>
                        </div>
                    </div>
                </div>
            </div>

        </div>
    </section>
@endsection
Enter fullscreen mode Exit fullscreen mode

Let's tell our UserController to return this view

/**
 * Show the form for creating a new resource.
 *
 * @return Renderable
 */
public function create(): Renderable
{
    return view('admin.users.create');
}
Enter fullscreen mode Exit fullscreen mode

And again let's run our test

php artisan test --filter UserControllerTest::test_can_display_create_user_page_if_user_is_super_admin
Enter fullscreen mode Exit fullscreen mode

Moving on to the next test. Let's define our StoreUserRequest to validate the request before storing the user in the database

<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class StoreUserRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     *
     * @return bool
     */
    public function authorize(): bool
    {
        return $this->user()->can('create user');
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array<string, mixed>
     */
    public function rules(): array
    {
        return [
            'name' => 'required|string|max:255',
            'email' => 'required|string|email|max:255|unique:users',
            'password' => 'required|string|min:8|confirmed',
        ];
    }
}
Enter fullscreen mode Exit fullscreen mode

And then let's define our store action

use App\Actions\Fortify\CreateNewUser;

/**
 * Store a newly created resource in storage.
 *
 * @param StoreUserRequest $request
 * @param CreateNewUser $action
 * @return RedirectResponse
 */
public function store(StoreUserRequest $request, CreateNewUser $action): RedirectResponse 
{
    $action->create([
        ...$request->validated(),
        'terms' => 'on',
        'password_confirmation' => $request->password_confirmation,
    ]);

    return redirect()
        ->route('admin.users.index')
        ->with('success', 'User created successfully.');
}
Enter fullscreen mode Exit fullscreen mode

This code simple injects Fortifty's CreateNewUser action with Laravel's service container and passes the validated input to the action to create a new user. It then redirects to the index page with a flash message.

And like always, let's run our test

php artisan test --filter UserControllerTest::test_can_store_user_if_user_is_super_admin
Enter fullscreen mode Exit fullscreen mode

And then to then next test. Let's create the user edit view

php artisan make:view admin.users.edit -e layouts.app
Enter fullscreen mode Exit fullscreen mode

Populate the new view with

@extends('layouts.app')

@section('title')
    Editing {{ $user->name }}
@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>{{ $user->name }}</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.users.index') }}">Users</a></div>
                <div class="breadcrumb-item">Edit</div>
            </div>
        </div>
        <div class="section-body">
            <h2 class="section-title">Edit User</h2>
            <p class="section-lead">
                You can edit user account information like passwords, name and emails
            </p>

            <div class="container">
                <div class="row">
                    <div class="col-12 col-md-7 ms-auto">
                        <div class="card">
                            <div class="card-header">
                                <h4>Profile</h4>
                            </div>
                            <div class="card-body">
                                <form action="{{ route('admin.users.update', $user) }}"
                                      method="POST">
                                    @method('PATCH')
                                    @csrf

                                    <div class="form-group">
                                        <label for="email"
                                               class="label form-control-label">Name</label>
                                        <input type="text"
                                               name="name"
                                               class="form-control @error('name') is-invalid @enderror"
                                               value="{{ old('name', $user->name) }}">
                                        @error('name')
                                            <div class="invalid-feedback">
                                                {{ $message }}
                                            </div>
                                        @enderror
                                    </div>

                                    <div class="form-group">
                                        <label for="email"
                                               class="label form-control-label">Email address</label>
                                        <input type="email"
                                               name="email"
                                               class="form-control @error('email') is-invalid @enderror"
                                               value="{{ old('email', $user->email) }}">
                                        @error('email')
                                            <div class="invalid-feedback">
                                                {{ $message }}
                                            </div>
                                        @enderror
                                    </div>

                                    <div class="form-group">
                                        <button type="submit"
                                                class="btn btn-primary">Update</button>
                                    </div>
                                </form>
                            </div>
                        </div>
                    </div>
                </div>

            </div>
    </section>
@endsection
Enter fullscreen mode Exit fullscreen mode

Let's tell the UserController to return this view in the edit action

/**
 * Show the form for editing the specified resource.
 *
 * @param User $user
 * @return Renderable
 */
public function edit(User $user): Renderable
{
    return view('admin.users.edit', [
        'user' => $user,
    ]);
}
Enter fullscreen mode Exit fullscreen mode

Then let's run the test

php artisan test --filter UserControllerTest::test_can_display_edit_user_page_if_user_is_super_admin
Enter fullscreen mode Exit fullscreen mode

For the edit action, first, let's define the UpdateUserRequest

<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class UpdateUserRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     *
     * @return bool
     */
    public function authorize(): bool
    {
        return $this->user()->can('update user', $this->route('user'));
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array<string, mixed>
     */
    public function rules(): array
    {
        return [
            'name' => 'required|string|max:255',
            'email' =>
                'required|string|email|max:255|unique:users,email,' .
                $this->route('user')->id,
        ];
    }
}
Enter fullscreen mode Exit fullscreen mode

And then the update action in the controller

use App\Actions\Fortify\UpdateUserProfileInformation; 

/**
 * Update the specified resource in storage.
 *
 * @param UpdateUserRequest $request
 * @param User $user
 * @param UpdateUserProfileInformation $action
 * @return RedirectResponse
 */
public function update(UpdateUserRequest $request, User $user, UpdateUserProfileInformation $action): RedirectResponse
{
    $action->update($user, $request->validated());

    return redirect()
        ->route('admin.users.index')
        ->with('success', 'User updated successfully.');
}
Enter fullscreen mode Exit fullscreen mode

Just like the store action, this code takes a request and the UpdatesProfileInformation action from Fortify and calls the update method with the validated input. It then redirects to the user's index page with a flash message.

At this point we should be able to create and edit users as well as see a list of all the registered users in our ecommerce application. The only part left in our defined tests is the ability to delete users, which we will implement in the next post along which will have a nice prompt with the help of sweetalert2 and Stimulus.js.

In the next post we will be implementing the ability to delete users, the ability to create roles and assign them a set of permissions, the ability to roles to different users. If you have any questions, reach out to me on Twitter @ncubegiven_.

In the meantime, subscribe to our newsletter below and get notified when the next post in this series is published

Top comments (0)