26
loading...
This website collects cookies to deliver better user experience
php -v
composer create-project laravel/laravel laravel-rbac-tutorial
cd laravel-rbac-tutorial
.env
:DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=laravel
DB_USERNAME=root
DB_PASSWORD=
php artisan migrate
users
table.composer require laravel/ui
php artisan ui bootstrap --auth
app/Http/Controllers/Auth
with the controllers needed to implement authentication. It will also add the necessary view files resources/views
to add pages like login and register.npm install && npm run dev
dev
script again:npm run dev
app/Providers/RouteServiceProvider.php
and change the value for the constant HOME
:public const HOME = '/';
php artisan serve
localhost:8000
. You'll see the login form.php artisan make:migration create_posts_table
database/migrations
with the file name's suffix create_posts_table
.<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreatePostsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('posts', function (Blueprint $table) {
$table->id();
$table->string('title');
$table->longText('content');
$table->foreignId('user_id')->constrained()->cascadeOnUpdate()->cascadeOnDelete();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('posts');
}
}
posts
table:php artisan migrate
app/Models/Post.php
with the following content:<?php
/**
* Created by Reliese Model.
*/
namespace App\Models;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Model;
/**
* Class Post
*
* @property int $id
* @property string $title
* @property string $content
* @property int $user_id
* @property Carbon|null $created_at
* @property Carbon|null $updated_at
*
* @property User $user
*
* @package App\Models
*/
class Post extends Model
{
protected $table = 'posts';
protected $casts = [
'user_id' => 'int'
];
protected $fillable = [
'title',
'content',
'user_id'
];
public function user()
{
return $this->belongsTo(User::class);
}
}
php artisan make:controller PostController
app/Http/Controllers/PostController.php
. Open it and add a constructor method:/**
* Create a new controller instance.
*
* @return void
*/
public function __construct()
{
$this->middleware('auth');
}
auth
middleware to all methods in this controller. This means that the user must be logged in before accessing any of the routes that point at this controller.postForm
function that renders the post form view:public function postForm ($id = null) {
/** @var User $user */
$user = Auth::user();
$post = null;
if ($id) {
/** @var Post $post */
$post = Post::query()->find($id);
if (!$post || $post->user->id !== $user->id) {
return response()->redirectTo('/');
}
}
return view('post-form', ['post' => $post]);
}
id
paramter, then retrieves the post based on that ID. It also validates that the post exists and belongs to the current logged-in user. This is because this method will handle the request for both creating a post and editing a post.resources/views/post-form.blade.php
with the following content:@extends('layouts.app')
@push('head_scripts')
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/trix.css">
@endpush
@section('content')
<div class="container">
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card">
<div class="card-header">{{ $post ? __('Edit Post') :__('New Post') }}</div>
<div class="card-body">
<form method="POST" action="#">
@csrf
@error('post')
<div class="alert alert-danger">{{ $message }}</div>
@enderror
<div class="form-group">
<label for="title">{{ __('Title') }}</label>
<input type="text" name="title" id="title" placeholder="Title" required
value="{{ $post ? $post->title : old('title') }}" class="form-control @error('title') is-invalid @enderror" />
@error('title')
<span class="invalid-feedback">{{ $message }}</span>
@enderror
</div>
<div class="form-group">
<label for="content">{{ __('Content') }}</label>
@error('content')
<span class="invalid-feedback">{{ $message }}</span>
@enderror
<input id="content" type="hidden" name="content" value="{{ $post ? $post->content : old('content') }}">
<trix-editor input="content"></trix-editor>
</div>
<div class="form-group">
<button type="submit" class="btn btn-primary">{{ __('Submit') }}</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/trix.js"></script>
@endsection
resources/views/layouts/app.blade.php
add the following menu item in the ul
under the comment <!-- Left Side Of Navbar -->
:<li class="nav-item">
<a class="nav-link" href="{{ route('post.form') }}">{{ __('New Post') }}</a>
</li>
routes/web.php
:Route::get('/post/{id?}', [PostController::class, 'postForm'])->name('post.form');
composer require silber/bouncer v1.0.0-rc.10
app/Models/User.php
make the following changes:use Silber\Bouncer\Database\HasRolesAndAbilities;
class User extends Authenticatable
{
use HasApiTokens, HasFactory, Notifiable, HasRolesAndAbilities;
php artisan vendor:publish --tag="bouncer.migrations"
php artisan migrate
app/Http/Controllers/PostController.php
add the new savePost
method:public function savePost (Request $request, $id = null) {
/** @var $user */
$user = Auth::user();
Validator::validate($request->all(), [
'title' => 'required|min:1',
'content' => 'required|min:1'
]);
//all valid, validate id if not null
/** @var Post $post */
if ($id) {
$post = Post::query()->find($id);
if (!$post) {
return back()->withErrors(['post' => __('Post does not exist')]);
}
} else {
$post = new Post();
}
//set data
$post->title = $request->get('title');
$post->content = $request->get('content');
if (!$post->user) {
$post->user()->associate($user);
}
$post->save();
if (!$id) {
Bouncer::allow($user)->toManage($post);
}
return response()->redirectToRoute('post.form', ['id' => $post->id]);
}
title
and content
were entered in the form. Then, if the optional parameter id
is passed to the method you validate if it exists and if it belongs to this user. This is similar to the postForm
method.title
and content
. Then, if the post is new you set the current user as the owner of the post with $post->user()->associate($user);
.if (!$id) {
Bouncer::allow($user)->toManage($post);
}
Bouncer
facade, you can use functions like allow
, which takes a user model. Then, you can give different types of permissions to the user. By using toManage
, you give the user all sorts of management permissions over the $post
instance.routes/web.php
:Route::post('/post/{id?}', [PostController::class, 'savePost'])->name('post.save');
resources/views/post-form.blade.php
:<form method="POST" action="{{ route('post.save', ['id' => $post ? $post->id : null]) }}">
title
and content
fields then clicking Submit, you'll be redirected back to the form with the content filled in which means the post has been added.index
method in app/Http/Controller/HomeController.php
to the following:/**
* Show the application dashboard.
*
* @return \Illuminate\Contracts\Support\Renderable
*/
public function index()
{
/** @var User $user */
$user = Auth::user();
//get all posts
$posts = Post::query()->where('user_id', $user->id)->get();
return view('home', ['posts' => $posts]);
}
resources/views/home.blade.php
to the following:@extends('layouts.app')
@section('content')
<div class="container">
<div class="row justify-content-center">
<div class="col-md-8">
<h1>{{ __('My Posts') }}</h1>
@forelse ($posts as $post)
<div class="card">
<div class="card-header">{{ $post->title }}</div>
<div class="card-body">
{!! $post->content !!}
</div>
<div class="card-footer">
{{ __('By ' . $post->user->name) }} -
<a href="{{ route('post.form', ['id' => $post->id]) }}">{{ __('Edit') }}</a>
</div>
</div>
@empty
<div class="card">
<div class="card-body">
{{ __('You have no posts') }}
</div>
</div>
@endforelse
</div>
</div>
</div>
@endsection
resources/views/home.blade.php
change the element with class .card-footer
that holds the Edit link to the following:<div class="card-footer">
{{ __('By ' . $post->user->name) }} -
<a href="{{ route('post.form', ['id' => $post->id]) }}">{{ __('Edit') }}</a> -
<a href="{{ route('post.access', ['id' => $post->id, 'type' => 'view']) }}">{{ __('Change view access...') }}</a> -
<a href="{{ route('post.access', ['id' => $post->id, 'type' => 'edit']) }}">{{ __('Change edit access...') }}</a>
</div>
app/Http/Controllers/PostController.php
add a new method accessForm
:public function accessForm ($id, $type) {
/** @var App/Models/User $user */
$user = Auth::user();
/** @var Post $post */
$post = Post::query()->find($id);
if (!$post || $post->user->id !== $user->id) {
return response()->redirectTo('/');
}
//get all users
$users = User::query()->where('id', '!=', $user->id)->get();
return view('post-access', ['post' => $post, 'users' => $users, 'type' => $type]);
}
id
which is the post ID, and type
which is the type of access. The type of access can be view or edit.post-access
which we'll create now.resources/views/post-access.blade.php
with the following content:@extends('layouts.app')
@section('content')
<div class="container">
<div class="row justify-content-center">
<div class="col-md-8">
<h1>{{ __("Change " . ucwords($type) . " Access to Post") }}</h1>
<div class="card">
<div class="card-body">
<form method="POST" action={{ route('post.access.save', ['id' => $post->id, 'type' => $type]) }}>
@csrf
@forelse ($users as $user)
<div class="form-group">
<label for="user_{{ $user->id }}">
<input type="checkbox" name="users[]" id="user_{{ $user->id }}" value="{{ $user->id }}"
class="form-control d-inline mr-3" @if ($user->can($type, $post)) checked @endif
style="width: fit-content; vertical-align: middle;" />
<span>{{ $user->name }}</span>
</label>
</div>
@empty
{{ __('There are no users') }}
@endforelse
<div class="form-group">
<button type="submit" class="btn btn-primary">{{ __('Save') }}</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
@endsection
app/Http/Controllers/PostController.php
add the new method saveAccess
:public function saveAccess (Request $request, $id, $type) {
/** @var User $user */
$user = Auth::user();
/** @var Post $post */
$post = Post::query()->find($id);
if (!$post || $post->user->id !== $user->id) {
return response()->redirectTo('/');
}
$users = $request->get('users', []);
$disallowedUserNotIn = $users;
$disallowedUserNotIn[] = $user->id;
//disallow users not checked
$disallowedUsers = User::query()->whereNotIn('id', $disallowedUserNotIn)->get();
/** @var User $disallowedUser */
foreach ($disallowedUsers as $disallowedUser) {
$disallowedUser->disallow($type, $post);
}
//allow checked users
$allowedUsers = User::query()->whereIn('id', $users)->get();
/** @var User $allowedUser */
foreach($allowedUsers as $allowedUser) {
$allowedUser->allow($type, $post);
}
return back();
}
id
and type
, same as accessForm
. You also validate the post by checking that it exists and that it belongs to the current user.request
. There are 2 actions to do here: disallow unchecked users to perform the action type
on the post, and allow checked users to perform the action type
on the post.$users
which holds the checked user IDs. Then, you loop over them to perform the following method on each of them:$disallowedUser->disallow($type, $post);
HasRolesAndAbilities
is added to a model, which we did earlier to the model User
, a set of methods are added to that m0del. One of them is disallow
which disallows the user a certain ability and you can specify a model to be more specific about what that ability is disabled on.$type
on the post $post
.$users
array and that should be granted the ability to perform action $type
on them. You loop over them and perform the following method:$allowedUser->allow($type, $post);
disallow
, allow
is another method that is added by the trait HasRolesAndAbilities
. It allows the user to have the ability $type
either in general or on a given model that is specified as a second parameter.$type
on the post $post
.routes/web.php
:Route::get('/post/access/{id}/{type}', [PostController::class, 'accessForm'])->name('post.access');
Route::post('/post/access/{id}/{type}', [PostController::class, 'saveAccess'])->name('post.access.save');
postForm
which allows users who have the permission to edit or view the post to access the page:if (!$post || ($post->user->id !== $user->id && !$user->can('edit', $post) && !$user->can('view', $post))) {
return response()->redirectTo('/');
}
savePost
:if (!$post || ($post->user->id !== $user->id && !$user->can('edit', $post))) {
return back()->withErrors(['post' => __('Post does not exist')]);
}
app/Http/Controllers/HomeController.php
in the index
method change the method to also retrieve the posts that the user has edit or view permissions on:/**
* Show the application dashboard.
*
* @return \Illuminate\Contracts\Support\Renderable
*/
public function index()
{
/** @var User $user */
$user = Auth::user();
//get all posts
$posts = Post::query()->where('user_id', $user->id);
//get posts that the user is allowed to view or edit
$postIds = [];
$abilities = $user->getAbilities();
/** @var \Silber\Bouncer\Database\Ability */
foreach ($abilities as $ability) {
$postIds[] = $ability->entity_id;
}
$posts = $posts->orWhereIn('id', $postIds)->get();
return view('home', ['posts' => $posts]);
}
getAbilities
method that is added on the User
model like allow
and disallow
. Each ability holds the id of the model it represents under entity_id
. We use that to get the ID of posts that the user has view or edit permissions on and retrieve them to show them on the home page.resources/views/home.blade.php
change the element with class .card-footer
to the following:<div class="card-footer">
{{ __('By ' . $post->user->name) }} -
<a href="{{ route('post.form', ['id' => $post->id]) }}">{{ __('View') }}</a>
@can('manage', $post)
- <a href="{{ route('post.access', ['id' => $post->id, 'type' => 'view']) }}">{{ __('Change view access...') }}</a> -
<a href="{{ route('post.access', ['id' => $post->id, 'type' => 'edit']) }}">{{ __('Change edit access...') }}</a>
@endcan
</div>
@can
blade directive which accepts the ability name and optionally a model instance. In this case, we check if the current user can manage
the post $post
.resources/views/post-form.blade.php
to ensure that if the user has view permission only they can't make edits on the post. This means that the form will become read-only.card
to the following:<div class="card-header">{{ $post ? __('View Post') :__('New Post') }}</div>
<div class="card-body">
<form method="POST" action="{{ route('post.save', ['id' => $post ? $post->id : null]) }}">
@csrf
@error('post')
<div class="alert alert-danger">{{ $message }}</div>
@enderror
@if ($post && !Auth::user()->can('edit', $post))
<div class="alert alert-info">{{ __('You have view permissions only') }}</div>
@endif
<div class="form-group">
<label for="title">{{ __('Title') }}</label>
<input type="text" name="title" id="title" placeholder="Title" required
value="{{ $post ? $post->title : old('title') }}" class="form-control @error('title') is-invalid @enderror"
@if($post && !Auth::user()->can('edit', $post)) disabled="true" @endif />
@error('title')
<span class="invalid-feedback">{{ $message }}</span>
@enderror
</div>
<div class="form-group">
<label for="content">{{ __('Content') }}</label>
@error('content')
<span class="invalid-feedback">{{ $message }}</span>
@enderror
@if($post && !Auth::user()->can('edit', $post))
{!! $post->content !!}
@else
<input id="content" type="hidden" name="content" value="{{ $post ? $post->content : old('content') }}">
<trix-editor input="content"></trix-editor>
@endif
</div>
@if(!$post || Auth::user()->can('edit', $post))
<div class="form-group">
<button type="submit" class="btn btn-primary">{{ __('Submit') }}</button>
</div>
@endif
</form>
</div>
title
input disabled, shows the content of the post readable rather than in an editor, and hides the submit button when the user does not have edit
permissions.