Laravel Ecommerce Tutorial: Part 2, Users And Authorization
Given Ncube
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
Next, let's publish Fortify's resources using the vendor:publish
command:
php artisan vendor:publish --provider="Laravel\Fortify\FortifyServiceProvider"
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
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
In order to compile our bootstrap css let's add sass
yarn add --dev sass
Next, let's create a resources/sass/app.scss
file and add the following
@import "variables";
@import "bootstrap/scss/bootstrap";
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,
}),
],
});
Next, let's import the Stisla admin template. Go ahead and clone Stisla from GitHub.
git clone https://github.com/stisla/stisla.git
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";
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
After installing, let's create our auth layout
with
php artisan make:view layouts.auth
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>
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
Let's tell Fortify how to return the login response. In the FortifyServiceProvider
boot method
Fortify::loginView(static function () {
return view('auth.login');
});
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
And set it up by running
php artisan authorizer:setup
In the User
model add the following trait
use Spatie\Permission\Traits\HasRoles;
class User extends Authenticatable
{
use HasRoles;
...
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"
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.',
);
}
}
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);
}
}
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
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;
});
}
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
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');
}
}
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();
}
}
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');
});
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,
];
Setting up the layout
Next let's create our layout
php artisan make:view layouts.app
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>
And then the navbar partial
php artisan make:view layouts.partials.navbar
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>
And for the sidebar
php artisan make:view layouts.partials.sidebar
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>
Lastly, the footer
php artisan make:view layouts.partials.footer
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>
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
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
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');
}
}
Now let's run our tests
php artisan test --filter HomeControllerTest::test_can_see_admin_home_page_if_user_is_super_admin
And with that all our other will pass as well
php artisan test
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
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();
}
}
To pass our first test let's define the routes for the users. In the admin.
route group add
Route::resource('users', UserController::class);
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>
Next, we asserted that the viewIs('admin.users.index')
let's create that view
php artisan make:view admin.users.index
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
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,
]);
}
Before we run our test let's generate the authorization code
php artisan authorizer:permissions:generate -m User
php artisan authorizer:policies:generate -m User
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');
}
Let's now run our test
php artisan test --filter UserControllerTest::test_can_see_admin_user_index_page_if_user_is_super_admin
Moving on to the next test, let's create the admin.users.create
view
php artisan make:view admin.users.create -e layouts.app
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
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');
}
And again let's run our test
php artisan test --filter UserControllerTest::test_can_display_create_user_page_if_user_is_super_admin
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',
];
}
}
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.');
}
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
And then to then next test. Let's create the user edit view
php artisan make:view admin.users.edit -e layouts.app
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
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,
]);
}
Then let's run the test
php artisan test --filter UserControllerTest::test_can_display_edit_user_page_if_user_is_super_admin
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,
];
}
}
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.');
}
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
[convertkit=2542481]