Vue Router Permission Recipes and Laravel Policies Examples

Part 47 of 48 in API Driven Development With Laravel and VueJS
Dan Pastori avatar
Dan Pastori September 18th, 2018
⚡️ Updated content is available We learned a lot since we originally wrote this article. We now have this updated for Laravel 10, Vue 3/NuxtJS 3, and Capacitor 3.

Up until this point, we have been working on building an administration section for https://roastandbrew.coffee. Now that we’ve gotten this far with permissions in place both on the Laravel API side and the Vue Router front end, I wanted to go through a few different permission recipes and examples that you can use in your own applications.

For more information on how our permission structure is set up, check out Planning your Laravel and VueJS SPA Application Admin Section. In this tutorial, we won’t be going through all of the specifics for each function (ex. adding JS API routes, new Vuex modules), these have been covered in previous tutorials. We want to focus on creating certain permission based scenarios that help limit user access that can be replicated in your application. Of course, if any more detail is needed, feel free to reach out in the comment section below and I can provide further detail.

Permission to Update Parts of Entities

So this example involves limiting what can be updated on an entity based on user permission. We’ve went through examples where either the entity can be updated or it can not be. What if you want only parts of the entity to be updated by one permission, and add a different permission to other parts? In this example, we want coffee shop owners to update the coffee shop meta data (name, website, etc.) but we want admins and super admins to change owners. Now it wouldn’t make sense to have two separate screens for this, we should have it nice and organized where more information can be edited if the user is a higher permission level.

Let’s get started.

So we’ve added an administration route for companies at /admin/companies. All of the modules, routes, and API code can be viewed here: GitHub – serversideup/roastandbrew. Essentially, the screen lists all of the companies visible by the user. This means if the user is an admin or super admin, all companies in the app are listed where if they were an owner, only the companies that are owned are listed. You can then click on the company, view the cafes and edit the company level information. From the /admin/companies/{company} page, you can click on an individual cafe and edit the cafe data.

Let’s focus on the /admin/companies/{company} route. This is where we want to block coffee shop owners from adding or removing owners, but show an option for admins and super admins to add and remove owners.

Open up the /resources/assets/js/pages/admin/Company.vue file. In this file, we load the company being viewed/edited. Where we want to focus is:

<div class="grid-x">
  <div class="large-8 medium-12 cell">
    <label>Owners</label>
    <div class="no-owners" v-show="owners.length == 0">N/A</div>
    <div class="owner" v-for="(owner, key) in owners">
      <router-link v-if="user.permission > 1" :to="{name: 'admin-user', params: { 'id': owner.id } }">{{ owner.name }}</router-link>
      <span v-if="user.permission == 1">{{ owner.name }}</span>

      <a class="remove-owner" v-if="user.permission > 1" v-on:click="removeOwner( key )">Remove</a>
    </div>

    <div class="user-selection-container" v-if="user.permission > 1">
      <input type="text" class="new-owner" v-model="newOwner" v-on:keyup="searchUsers()" placeholder="Add An Owner"/>

      <div class="user-autocomplete-container" v-show="newOwner.length > 0 && showAutocomplete">
        <div class="user-autocomplete" v-for="user in newOwnerResults" v-on:click="selectUser( user )">
          <span class="user-name">{{ user.name }}</span>
        </div>
      </div>
    </div>
  </div>
</div>

What this section does is shows the owners on the coffee shop which is cool. The owner should see if there are other owners on the coffee shop.

However we hide the Remove button when the user has a permission less than 1. We also hide the input to search users if the permission of the loaded user is less than 1. Now this is nice and great on the front end, but we need to make it more secure. As I’ve mentioned before, this is front end code. Essentially the highest level of confidence you can have is by blocking this code, it makes the UX easier for the authenticated user. They could twist and change the front end javascript at will. You will need to block off the server side as well for actual security.

Now when the user on the front end submits this form, if they are an owner, they should be able to adjust what they want about the meta, but if they are an admin or super admin they should be able to add and remove owners.

Let’s look at the route that handles a company update. If you open /routes/api.php you will see the route:

/*
  |-------------------------------------------------------------------------------
  | Updates An Individual Company
  |-------------------------------------------------------------------------------
  | URL:            /api/v1/admin/companies/{id}
  | Controller:     API\Admin\CompaniesController@putUpdateCompany
  | Method:         PUT
  | Description:    Updates an individual company.
  */
  Route::put('/companies/{company}', 'API\Admin\CompaniesController@putUpdateCompany')
       ->middleware('can:update,company');

Now this route is wrapped in the owner middleware so to at least access the route you must be an owner. The next middleware applied is through our app/Policies/CompanyPolicy.php which is “can the user update the company”? If they can, then they must be an owner of the company, so let them access the route. The route is handled by app/Http/Controllers/API/Admin/CompaniesController@putUpdateCompany. If you look at that method, we have a CompanyService that handles the updating of the company.

Open app/Services/CompanyService.php and find the updateCompany() method. This is where we will block access to users trying to update the owners that don’t have permission.

To do this, we wrapped the functionality in a policy check like this:

if( Auth::user()->can('updateOwners', $company ) ){
    ...
}

and passed the company to the method.

In our app/Policies/CompanyPolicy.php we added the following method:

/**
* If the user is an admin they can update owners on a company.
*
* @param \App\Models\User $user
*/
public function updateOwners( User $user ){
  if( $user->permission == 2 || $user->permission == 3 ){
    return true;
  }else{
    return false;
  }
}

What this does is check to see if the user (first parameter) has a permission level of 2 or 3. That means they are an admin or a super admin. If so, we let them update the owners.

So what we did is blocked the owner adjustment on the front end side if the user didn’t have permissions. On the API side, we blocked the user permanently through the middleware first to filter out users who weren’t at least owners. Then in the company policy, we made a policy to determine whether the users can update owners or not by determining if the user was an admin or super admin. We then applied that policy in the method for that commits changes to the database.

This way, EVEN if a user submits owners with no permissions to change them, they won’t be committed to the database and the code will be ignored.

There are a lot of times where situations like this may occur. Even more places in our app. If you browse through the code, we don’t want admins promoting to super admins, but we want admins to promote up to an admin. A very similar functionality takes place where we have a policy that either allows or blocks whether or not the user can promote to and admin/super admin, or not.

Super Admin Only Pages

Sometimes you only want the top level permission to update certain entities. In this case, we are blocking off all brew method management to super admins only.

We first block off the front end page very similar to blocking off by owner. We have a meta property in the route that sets the permission to be super-admin like this:

{
  path: 'brew-methods',
  name: 'admin-brew-methods',
  component: Vue.component( 'AdminBrewMethods', require( './pages/admin/BrewMethods.vue' ) ),
  meta: {
    permission: 'super-admin'
  }
},
{
  path: 'brew-methods/:id',
  name: 'admin-brew-method',
  component: Vue.component( 'AdminBrewMethod', require( './pages/admin/BrewMethod.vue' ) ),
  meta: {
    permission: 'super-admin'
  }
},

From there, we block every time that the current logged in user is trying to access the page if they are not a super admin. Next, we need to block off the admin side API routes to only allow super admin access. In this case we created a middleware to block the routes and no policy. This is useful because we simply want to block the entire route if the user is not an admin. We could have done a policy, but I like using policies more for fine grain control.

If you open up app/Http/Middleware/SuperAdmin.php you will see the middleware that blocks the route. The method looks like:

/**
 * Handle an incoming request.
 *
 * @param  \Illuminate\Http\Request  $request
 * @param  \Closure  $next
 * @param  string|null  $guard
 * @return mixed
 */
public function handle($request, Closure $next, $guard = null)
{
    /*
      Any user with a permission less than 3 is not a super admin and
      receives a 403 un authorized action response.
    */
    if (Auth::user()->permission < 3 ) {
        abort(403, 'Unauthorized action.');
    }

    return $next($request);
}

This simply blocks any user that’s not a super admin. Since we apply this to a route in our api.php routes file, it also automatically blocks any user that’s not authenticated.

Next, we will dive a little more in depth on the dynamic registration of Vuex modules.

Vuex Module Registration Based On Permission

In the last tutorial VueJS App Admin Screens we touched on dynamically registering vue modules for the administration side. This allows us to only register the modules if the user is an admin and they need to use admin data. We un-register these modules when the user navigates back to the app.

The next step of this, now that we have a few more examples of permissions, is to piece together the modules needed when the user has permission.

If we open our resources/assets/js/layouts/Admin.vue file, we will be working on importing our modules and dynamically registering them based on the authenticated user’s permission level.

We will be working with two admin level modules. The users module which requires at least an admin and the brewMethods module which requires a super admin.

First, we will import the modules to be used like this:

import { users } from '../modules/admin/users.js';
import { brewMethods } from '../modules/admin/brewMethods.js';

Next, in our created lifecycle hook, we will add the following code:

/*
  Checks to see if the user has permissions and if the
  Vuex users module is loaded.
*/
if( !this.$store._modules.get(['admin', 'users'] ) && this.user.permission >= 2 ){
  this.$store.registerModule( ['admin', 'users'], users );
}

/*
  Checks to see if the user has permissions and if the
  Vuex brew methods module is loaded.
*/
if( !this.$store._modules.get(['admin', 'brewMethods'] ) && this.user.permission == 3 ){
  this.$store.registerModule( ['admin', 'brewMethods'], brewMethods );
}

What this does, is check the user’s permissions and registers the module only if they have permission. The reason I like doing this is it prevents unauthorized users from easily seeing the data structure. We also clear all of the data on the admin side when the user navigates back to the application. These are both things someone can tweak on the front end with Javascript code (why we secure the API), but it also helps just with UX and cleanliness of not loading what we don’t need.

Updates

Just a few updates that don’t require much of a tutorial, but will mention for those following along.

First, we now allow entire companies to be deleted. When a user on the admin side deletes a company, the company will no longer appear in the autocomplete on the front end and all cafes will be flagged as deleted as well.

We also updated the front end query to not return companies that have been deleted when adding and editing a cafe.

When we delete a company, we flag all of the cafes associated with that company to be deleted as well. This maintains consistency between the state of the company and the cafe. Cafes cans still be deleted separately in case of closure or other reasons, but if an entire company is deleted, then obviously the cafes should be as well.

To Sum Up

It’s been quite a process adding an administration section to the app. Especially since we are still keeping the single page application functionality. However, there’s a lot more we can add and we’ve set up the framework to allow for easy addition of more features and updates.

If you have any questions about how to add more admin functionality to your application, please reach out in the comments below.

For more examples, and a larger scaled application, we are writing book about how to do API Driven Development within a Single Page Application and will go through a much more detailed process. Sign up for notifications about the development of the book here: Server Side Up General List.

Keep Reading
View the course View the Course API Driven Development With Laravel and VueJS
Up Next → Sorting in VueJS Components and Vuex State

Support future content

The Ultimate Guide to Building APIs and Single-Page Applications with Laravel + VueJS + Capacitor book cover.

Psst... any earnings that we make off of our book is being reinvested to bringing you more content. If you like what you read, consider getting our book or get sweet perks by becoming a sponsor.

Written By Dan

Dan Pastori avatar Dan Pastori

Builder, creator, and maker. Dan Pastori is a Laravel certified developer with over 10 years experience in full stack development. When you aren't finding Dan exploring new techniques in programming, catch him at the beach or hiking in the National Parks.

Like this? Subscribe

We're privacy advocates. We will never spam you and we only want to send you emails that you actually want to receive. One-click unsubscribes are instantly honored.