Now that we have authorization for the back-end, let's add it for the front-end, too. If the user cannot delete the post, then he shouldn't even see the delete button.
For front-end permissions, we will use the package CASL Vue. First, let's install it.
npm install @casl/vue @casl/ability
To get started, we need to import abilitiesPlugin
and ability
from the services in the main app.js
.
resources/js/app.js:
import './bootstrap'; import { createApp, onMounted } from 'vue' import router from './routes/index'import VueSweetalert2 from 'vue-sweetalert2';import useAuth from './composables/auth';import { abilitiesPlugin } from '@casl/vue'; import ability from './services/ability'; createApp({ setup() { const { getUser } = useAuth() onMounted(getUser) }}) .use(router) .use(VueSweetalert2) .use(abilitiesPlugin, ability) .mount('#app')
And what is inside this /services/ability
? You define the abilities there, and one of the sections in the documentation is about ability builder. And we can copy the code in that services file.
resources/js/services/ability.js:
import { AbilityBuilder, Ability } from '@casl/ability' const { can, cannot, build } = new AbilityBuilder(Ability); export default build();
But instead of defining can and cannot here, we will define them based on the API call to the /abilities
API endpoint. Now let's build the /abilities
API route.
routes/api.php:
Route::group(['middleware' => 'auth:sanctum'], function() { Route::apiResource('posts', PostController::class); Route::get('categories', [CategoryController::class, 'index']); Route::get('/user', function (Request $request) { return $request->user(); }); Route::get('abilities', function(Request $request) { return $request->user()->roles()->with('permissions') ->get() ->pluck('permissions') ->flatten() ->pluck('name') ->unique() ->values() ->toArray(); }); });
We get the authenticated user's roles with permissions. Then we pluck to have only permissions, and using other Collection methods, we get the unique permissions in the array list.
Now we need to use this API call in the auth Composable. For this, we will create a specific method getAbilities()
.
resources/js/composables/auth.js:
import { ref, reactive, inject } from 'vue'import { useRouter } from 'vue-router';import { AbilityBuilder, Ability } from '@casl/ability'; import { ABILITY_TOKEN } from '@casl/vue'; const user = reactive({ name: '', email: '',}) export default function useAuth() { const processing = ref(false) const validationErrors = ref({}) const router = useRouter() const swal = inject('$swal') const ability = inject(ABILITY_TOKEN) const loginForm = reactive({ email: '', password: '', remember: false }) // ... const getAbilities = async() => { axios.get('/api/abilities') .then(response => { const permissions = response.data const { can, rules } = new AbilityBuilder(Ability) can(permissions) ability.update(rules) }) } return { loginForm, validationErrors, processing, submitLogin, user, getUser, logout, getAbilities }}
After a successful HTTP GET request, we assign all the permissions to a variable from the response. Then we add all the permissions into a can
method and update the abilities.
To understand more about how this package works refer to the documentation.
And we call that getAbilities
in the loginUser
before router.push
. And because we did await
for the getAbilities
now the loginUser
becomes async and also we need to add await
for the router.push
.
import { ref, reactive, inject } from 'vue'import { useRouter } from 'vue-router';import { AbilityBuilder, Ability } from '@casl/ability';import { ABILITY_TOKEN } from '@casl/vue'; const user = reactive({ name: '', email: '',}) export default function useAuth() { // ... const loginUser = (response) => { const loginUser = async (response) => { user.name = response.data.name user.email = response.data.email localStorage.setItem('loggedIn', JSON.stringify(true)) await getAbilities() router.push({ name: 'posts.index' }) await router.push({ name: 'posts.index' }) } // ...}
And finally, we can get to the PostsIndex
Vue component to hide the Edit
and Delete
actions if the user doesn't have permission for that action.
The syntax for the v-if
directive is v-if="can('permission name here')"
.
resources/js/components/Posts/Index.vue:
<template> <div class="overflow-hidden overflow-x-auto p-6 bg-white border-gray-200"> <div class="min-w-full align-middle"> <div class="mb-4 grid lg:grid-cols-4"> <input v-model="search_global" type="text" placeholder="Search..." class="inline-block mt-1 w-full rounded-md shadow-sm border-gray-300 focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50"> </div> <table class="min-w-full divide-y divide-gray-200 border"> // ... <tbody class="bg-white divide-y divide-gray-200 divide-solid"> <tr v-for="post in posts.data"> // ... <td class="px-6 py-4 whitespace-no-wrap text-sm leading-5 text-gray-900"> <router-link :to="{ name: 'posts.edit', params: { id: post.id } }">Edit</router-link> <a href="#" @click.prevent="deletePost(post.id)" class="ml-2">Delete</a> <router-link v-if="can('posts.update')" :to="{ name: 'posts.edit', params: { id: post.id } }">Edit</router-link> <a href="#" v-if="can('posts.delete')" @click.prevent="deletePost(post.id)" class="ml-2">Delete</a> </td> </tr> </tbody> </table> <TailwindPagination :data="posts" @pagination-change-page="page => getPosts(page, search_category)" class="mt-4" /> </div> </div></template> <script setup>// ...</script>
But we don't have that can
defined yet! Let's add that.
resources/js/components/Posts/Index.vue:
<script setup>import { onMounted, ref, watch } from "vue";import { TailwindPagination } from 'laravel-vue-pagination';import usePosts from "@/composables/posts";import useCategories from "@/composables/categories";import { useAbility } from '@casl/vue' const search_category = ref('')const search_id = ref('')const search_title = ref('')const search_content = ref('')const search_global = ref('')const orderColumn = ref('created_at')const orderDirection = ref('desc')const { posts, getPosts, deletePost } = usePosts()const { categories, getCategories } = useCategories()const { can } = useAbility() // ...</script>
Now, if you log in with the editor
role, you should see that the Delete
action isn't showed anymore.
So yeah, that's it for this course!
Repository is available on GitHub here.