Laravel Multi Auth using different tables [part 3: Admin Permissions]
Links:
The codes of this article here on GitHub;
Appropriate YouTube video;
YouTube video-lessons Playlist;
You can download the public resources from here;
Part 1: User authentication;
Part 2: Admin authentication;
Introduction:
In the 1-st part of this article I’ve described how to make Laravel (5.6, 5.7, 5.8) default User Authentication System (login, register, etc), using “users” table (migrated from default migration).
In previous 2-nd part I’ve described how to make Admin Authentication System, using “admins” table.
In this 3-rd part I’ll install, configure and use this popular Spatie’s package for clients permissions. Exactly I want to have multiple admins with different roles of permissions.
So let’s get started!
Installing and Configuration of “spatie/laravel-permission”:
As written in repo readme file, run this command:composer require spatie/laravel-permission
The service provider will automatically get registered. Or you may manually add the service provider in your “config/app.php” file: (I’ll not)
'providers' => [
// ...
Spatie\Permission\PermissionServiceProvider::class,
];
Optional (I’ll do):
Run config publishing command:php artisan vendor:publish --provider="Spatie/Permission/PermissionServiceProvider" --tag="config"
This will generate “config/permission.php”, which will looks like this after some changes:
<?php
return [
'models' => [
'permission' => Spatie\Permission\Models\Permission::class,
'role' => Spatie\Permission\Models\Role::class,
],
'table_names' => [
'roles' => 'roles',
'permissions' => 'permissions',
'model_has_permissions' => 'model_has_permissions',
'model_has_roles' => 'model_has_roles',
'role_has_permissions' => 'role_has_permissions',
],
'column_names' => [
'model_morph_key' => 'model_id',
],
'display_permission_in_exception' => false,
'cache' => [
'expiration_time' => 24 * 60, // \DateInterval::createFromDateString('24 hours'),
'key' => 'spatie.permission.cache',
'model_key' => 'name',
'store' => 'default',
],
];
Run migration publishing command:php artisan vendor:publish --provider="Spatie/Permission/PermissionServiceProvider" --tag="migrations"
After generation of migration file, we can run the migration to create new tables for further usage of spatie’s package.php artisan migrate
Now we’ll add the “Spatie\Permission\Traits\HasRoles” trait to our “app\Models\Admin.php” Model like this:
namespace App\Models;
use Illuminate\Notifications\Notifiable;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Spatie\Permission\Traits\HasRoles;
class Admin extends Authenticatable
{
use Notifiable, HasRoles;
After changes in public function “render()” of “app\Exceptions\Handler.php” we’ll have this:
public function render($request, Exception $exception)
{
if ($exception instanceof UnauthorizedException) {
if(Auth::user() instanceof Admin){
return redirect()->route('admin.dashboard');
}
return redirect()->route('front');
}
return parent::render($request, $exception);
}
Note: don’t forget to import “Spatie\Permission\Exceptions\UnauthorizedException” class.
In “app\Http\Kernel.php” make sure you’ve uncommented all lines (actually “\Illuminate\Session\Middleware\AuthenticateSession::class,”, which may commented by default). Add “admin” element to the “$middlewareGroups” protected array property. Add “‘role” element to the “$routeMiddleware” protected array property. So we’ll have this:
<?php
namespace App\Http;
use Illuminate\Foundation\Http\Kernel as HttpKernel;
use App\Http\Middleware\CheckForMaintenanceMode;
use App\Http\Middleware\TrimStrings;
use App\Http\Middleware\TrustProxies;
use Illuminate\Foundation\Http\Middleware\ValidatePostSize;
use Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull;
use App\Http\Middleware\EncryptCookies;
use Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse;
use Illuminate\Session\Middleware\StartSession;
use Illuminate\View\Middleware\ShareErrorsFromSession;
use App\Http\Middleware\VerifyCsrfToken;
use Illuminate\Routing\Middleware\SubstituteBindings;
use Illuminate\Session\Middleware\AuthenticateSession;
use App\Http\Middleware\Authenticate;
use Illuminate\Auth\Middleware\AuthenticateWithBasicAuth;
use Illuminate\Http\Middleware\SetCacheHeaders;
use Illuminate\Auth\Middleware\Authorize;
use App\Http\Middleware\RedirectIfAuthenticated;
use Illuminate\Routing\Middleware\ValidateSignature;
use Illuminate\Routing\Middleware\ThrottleRequests;
use Illuminate\Auth\Middleware\EnsureEmailIsVerified;
use Spatie\Permission\Middlewares\PermissionMiddleware;
use Spatie\Permission\Middlewares\RoleMiddleware;
class Kernel extends HttpKernel
{
protected $middleware = [
CheckForMaintenanceMode::class,
ValidatePostSize::class,
TrimStrings::class,
ConvertEmptyStringsToNull::class,
TrustProxies::class,
];
protected $middlewareGroups = [
'web' => [
EncryptCookies::class,
AddQueuedCookiesToResponse::class,
StartSession::class,
AuthenticateSession::class, //ss commented by default
ShareErrorsFromSession::class,
VerifyCsrfToken::class,
SubstituteBindings::class,
],
'admin' => [
EncryptCookies::class,
AddQueuedCookiesToResponse::class,
StartSession::class,
AuthenticateSession::class,
ShareErrorsFromSession::class,
VerifyCsrfToken::class,
SubstituteBindings::class,
],
'api' => [
'throttle:60,1',
'bindings',
],
];
protected $routeMiddleware = [
'auth' => Authenticate::class,
'auth.basic' => AuthenticateWithBasicAuth::class,
'bindings' => SubstituteBindings::class,
'cache.headers' => SetCacheHeaders::class,
'can' => Authorize::class,
'guest' => RedirectIfAuthenticated::class,
'signed' => ValidateSignature::class,
'throttle' => ThrottleRequests::class,
'verified' => EnsureEmailIsVerified::class,
'role' => RoleMiddleware::class,
// 'permission' => PermissionMiddleware::class,
];
protected $middlewarePriority = [
StartSession::class,
ShareErrorsFromSession::class,
Authenticate::class,
AuthenticateSession::class,
SubstituteBindings::class,
Authorize::class,
];
}
Seeders for Clients:
It could be better if we had some admins with different permissions. Let’s say, we need to have 3 types of admins: “administrator”, “moderator”, “manager” with different permissions, which will stored in the same “admins” table.
For that we need to have an Admin Seeder, for generating admins before working with it. Also it will be better if we’ll create a User Seeder too, for registering a test user for us at same time.
This will be our “database\seeds\AdminSeeder.php”:
<?php
use App\Models\Admin;
use Illuminate\Database\Seeder;
use Spatie\Permission\Models\Role;
class AdminSeeder extends Seeder
{
public function run()
{
foreach (config('project.admin.roles') as $role) {
Role::firstOrCreate([
'guard_name' => 'admin',
'name' => $role
]);
};
$admins = [
[
'role' => 'administrator',
'name' => 'Admin',
'email' => 'administrator@gmail.com',
'password' => 'secret',
],
[
'role' => 'moderator',
'name' => 'Moderator',
'email' => 'moderator@gmail.com',
'password' => 'secret',
],
[
'role' => 'manager',
'name' => 'Manager',
'email' => 'manager@gmail.com',
'password' => 'secret',
],
];
foreach ($admins as $admin) {
$exist = Admin::where('email', $admin['email'])->first();
if(empty($exist)){
$super_admin = Admin::firstOrCreate([
'name' => $admin['name'],
'email' => $admin['email'],
'password' => bcrypt($admin['password']),
]);
$super_admin->assignRole($admin['role']);
}
}
}
}
And this is a “database\seeds\UserSeeder.php” User seeder:
<?php
use App\Models\User;
use Illuminate\Database\Seeder;
class UserSeeder extends Seeder
{
public function run()
{
User::firstOrCreate([
'name' => config('project.seed.dev_name'),
'email' => config('project.seed.dev_email'),
'password' => bcrypt(config('project.seed.dev_password')),
]);
}
}
For registering theese seeds, we should have our main “database\seeds\DatabaseSeeder.php” seeder like this:
<?php
use Illuminate\Database\Seeder;
class DatabaseSeeder extends Seeder
{
public function run()
{
$this->call(AdminSeeder::class);
$this->call(UserSeeder::class);
}
}
As you can see in seeds, we have something like “config(‘project.***’)”. It means, we need to create a new “config/project.php” config file and store there some staff.
<?php
return [
'seed' => [
'dev_name' => env('DEV_NAME', 'test'),
'dev_email' => env('DEV_EMAIL', 'test@gmail.com'),
'dev_password' => env('DEV_PASSWORD', 'secret'),
],
'admin' => [
'prefix' => env('ADMIN_PREFIX', 'admin'),
'roles' => [
'administrator' => 'administrator',
'moderator' => 'moderator',
'manager' => 'manager',
],
],
];
Environment Variables:
As we seen on last previous step, we’ve used “env()” helper function to get environment variables from “.env” file.
For this article I’ll use some credentials in “.env”. I’ll show now only the part of “.env.example”, but in your “.env” you can change the values of variables as you need:
DEV_NAME="Test"
DEV_EMAIL="test@gmail.com"
DEV_PASSWORD="secret"
ADMIN_PREFIX="admin"
After these config + .env changes we must refresh our app caches. Also if you want to start build your app from scratch, then run following commands (I’ll do so):
php artisan clearcaches
php artisan droptables
php artisan migrate
php artisan db:seed
About Power of Permissions:
Now in this step let’s see, how and why we can use the power of our installed package.
As we know, we have 3 types of admins with different permissions. Let’s assume, that we want to have some pages in Admin Panel, which can see only specific admins. It means, that some routes should not be available for specific admins. Our package give us that possibility…
We’ll create a Users listing page in Admin Panel, which will be available for “administrator” and “moderator” admins, but not for “manager” admins.
For Users listing page in Admin Panel we need to create a new controller “app\Http\Controllers\Admin\UserController.php”:
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Support\Facades\View;
class UserController extends Controller
{
public function __construct()
{
$this->middleware('auth:admin');
View::share('action', 'no_add');
View::share('nav', 'users');
}
public function index()
{
$users = User::all();
return view('admin.pages.users.index', [
'users' => $users,
]);
}
}
After adding new routes in “admin.php” for admins with different roles, we’ll have this:
<?php
Route::group([
'namespace' => 'Auth',
], function () {
// Authentication Routes...
Route::get('login', 'LoginController@showLoginForm')->name('login_page');
Route::post('login', 'LoginController@login')->name('login');
Route::post('logout', 'LoginController@logout')->name('logout');
});
Route::group([
'middleware' => [
'auth:admin',
],
], function () {
// for all admins
Route::get('/', 'AdminController@index')->name('dashboard');
Route::get('home', 'AdminController@index')->name('dashboard');
Route::get('dashboard', 'AdminController@index')->name('dashboard');
// for administrator
Route::group(['middleware' => ['role:administrator']], function () {
//
});
// for moderators
Route::group(['middleware' => ['role:administrator|moderator']], function () {
// users
Route::group(['prefix' => 'users', 'as' => 'users.',], function () {
Route::get('all', 'UserController@index')->name('index');
});
});
// for managers
Route::group(['middleware' => ['role:administrator|moderator|manager']], function () {
//
});
});
After adding new page block in our Admin template, we’ll have this in our “resources\views\admin\partials\aside.blade.php”:
<aside class="main-sidebar">
<section class="sidebar">
<!-- sidebar menu: : style can be found in sidebar.less -->
<ul class="sidebar-menu" data-widget="tree">
@php($admin_role = Auth::user()->getRoleNames()->first())
<li class="header">{{ $admin_role }}</li>
<li class="{{ $nav == 'dashboard' ? 'active' : '' }}"><a href="{{ route('admin.dashboard') }}"><i class="fa fa-dashboard"></i> <span>Dashboard</span></a></li>
{{--//ss roles in "config/project.admin.roles"--}}
@if($admin_role == 'administrator' || $admin_role == 'moderator')
<li class="{{ $nav == 'users' ? 'active' : '' }}"><a href="{{ route('admin.users.index') }}"><i class="fa fa-user"></i> <span>Users</span></a></li>
@endif
</ul>
</section>
</aside>
Let’s list all users in Users listing page.
resources\views\admin\pages\users\index.php
@extends('admin.layouts.app')
@section('custom_styles')
@endsection
@section('content')
<div class="row">
<div class="col-md-6">
<div class="box">
<div class="box-header with-border">
<h3 class="box-title">Users Table</h3>
</div>
<div class="box-body">
<table class="table table-bordered">
<thead>
<tr>
<th style="width: 10px">Num.</th>
<th>Name</th>
</tr>
</thead>
<tbody>
@foreach($users as $k => $user)
<tr>
<td>{{ $k + 1 }}</td>
<td>{{ $user->name }}</td>
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
</div>
</div>
@endsection
@section('custom_scripts')
@endsection
Manageable Admin Panel URI-prefix address:
We already talked about manageable URI admin panel addresses. What that means?
Let’s assume we built multiauth app for our customer, and it’s like this, as our app at current moment. But when customer knew about “/admin” URL-path of Admin Panel, he wanted to change that and have “/panel-dashboard” instead.
What can we do? May we need to find all the usages of “/admin” in whole app, and replace them with “/panel-dashboard”?
I hope, that you wont do that. Cuz this process may be repeated again and again. So you don’t need to find all that usages and change them in every time.
We’ll find all that matches and replace them with manageable/dynamic variable, which can have appropriate segment-path for current time.
We need to initialize protected property “$redirectTo” of “app\Http\Controllers\Admin\Auth\LoginController.php” class. As we can’t init that on definition, we just need to do that in constructor. After initializing it will be like:
protected $redirectTo = '/admin/dashboard';
public function __construct()
{
$this->redirectTo = config('project.admin.prefix') . '/dashboard';
$this->middleware('guest:admin')->except('logout');
}
Also in Route Service Provider we need to replace admin path prefix in “mapAdminRoutes()” protected function. After changes it will be like:
protected function mapAdminRoutes()
{
Route::middleware('web')
->as('admin.')
->prefix(config('project.admin.prefix'))
->namespace($this->namespace . '\Admin')
->group(base_path('routes/admin.php'));
}
We already have the value of “config(‘project.admin.prefix’)”, which is will grabbed from “.env” ADMIN_PREFIX variable (it’s “admin” by default).
So we just can replace the value in “.env” like this:
ADMIN_PREFIX="panel-dashboard"
And after that we must to refresh our caches (using one of these 3 commands in below):
php artisan clearcaches
php artisan optimize
php artisan config:cache
After all these changes we can just change/manage our Admin Panel main URI-path from one place. So it’s done!
Note:
Don’t forget to recreate/clear/refresh app caches after “.env” or config files changes.
Current Results:
After these all steps described on top, we should get something like this for admin panel:
Note:
This image on top is just screenshot of “/admin/users/all” URI page logged in via “administrator” or via “moderator” admins. This page will not available for “manager” admins. Cuz it restricted from two places:
1. from routes (see “admin.php”):
...
// for moderators
Route::group([
'middleware' => ['role:administrator|moderator'],
], function () {
// users
Route::group(['prefix'=>'users','as'=>'users.'], function () {
Route::get('all', 'UserController@index')->name('index');
});
});
...
2. and from blade (see “aside.blade.php”) as link:
...
@php($admin_role = Auth::user()->getRoleNames()->first())
...
@if($admin_role == 'administrator' || $admin_role == 'moderator')
...<a href="{{ route('admin.users.index') }}">Users</a>...
@endif
...
About this 3-rd part:
In this part we’ve integrated and used popular package for Laravel Admin Permissions/Roles. We also made a working example for that.
Also we’ve made “/<ADMIN_PREFIX>” URI address manageable from “.env”.
So, we can consider that our main purpose are completed!
Thanks!