VueJS Route Permissions, Security and Admin Section

Part 45 of 48 in API Driven Development With Laravel and VueJS
Dan Pastori avatar
Dan Pastori September 4th, 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.

Just a quick re-fresher of where we are at. We first planned out our user permission structure here: Planning your Laravel and VueJS SPA Application Admin Section. This gave us the following permissions:

0 -> General User
1 -> Coffee Shop Owner
2 -> Admin
3 -> Super Admin

We also came up with the idea of actions. When a user couldn’t create, update, or delete a cafe we created an action. This allows a higher permission of users to validate the action before committing to the database. We also create actions as a form of history. When a user has permissions to create, update, delete a cafe, we still create an action, it’s just already approved.

In this tutorial Laravel Gates and Policies in an API Driven SPA we set up the actual Laravel Policies (Authorization – Laravel – The PHP Framework For Web Artisans) to implement this logic in our application. These block off direct creating, editing, or deleting of cafes.

In the last tutorial Laravel Admin Routes and Security in a SPA, we were still on the server side of the app and we created some API endpoints to approve, deny and view all pending actions. We also added another policy to allow or block users from actually processing and viewing pending actions. We don’t want any user processing actions or retrieving actions that they don’t have permission to.

Now we want to provide a UI for authenticated and authorized users to process these actions appropriately. This means building an admin section for your application. This tutorial will go through how to set up that section so you can add multiple pages. In the next tutorial we will actually be adding a page to approve actions!

SPA Admin Section Considerations

Now building an admin section in a single page application brings forth a lot of different ideas and thought processes to consider.

Last week, Laravel Nova was launched (Laravel Nova). This is a beautiful administration panel that’s easily installable and configurable in your application. However, we are doing everything in a single page application method. You could install nova on a subdomain and manage the application very easily.

We will be doing our admin section through an API Driven SPA structure. Reason being, is you can use some of these principals in your application whether it’s blocking pages based on permissions within the app, or making your own admin section, the same principals exist.

The first thing to remember about a single page application, the code for calling this side of the API will be loaded into the client’s browser. That means, HTML, JS calls, Vuex stores, etc. At first this is huge red flag like “Isn’t this a massive security vulnerability?”. I was in the same boat, but going through the first two admin tutorials Planning your Laravel and VueJS SPA Application Admin Section & Laravel Gates and Policies in an API Driven SPA setting up server side policies and authorizations, I became more comfortable with this. These endpoints will be visible, but we block based on the token. Users will not see data they can not see and they will not be able to act upon entities they don’t have permission to.

That being said, even if a user was trying hard, adjusted some vuex state and gave their JS object permission to view the admin section of the application, they wouldn’t load anything because it’d be blocked server side.

That brings us to the next consideration, user experience. Of course we want to apply the appropriate permissions to the admin side as if it were a deadbolt from locking out other users. Otherwise a general user could be like “hey cool, and admin section!” and start clicking around and seeing no data rendering the screens completely worthless. Users without permissions just need to stay out.

This would be the same if you were adding a page for a different permission level in your application. For example, managing a message board. You’d want the server to only return the proper data, but you don’t want any user to see the link for management. That’d just frustrate the users.

To sum it up, we are blocking off routes on the single page application side more as a permissions based UX and handling the hard core dead bolted security on the server side where we are in full control.

Let’s get started!

Prepare Your App for Adding An Admin Section

To be straight, the admin section can be equivalent in size to your actual application. You will be managing all entities in your application and possibly permissions and meta data. That’s why Laravel Nova would be a turn key solution that fits a lot of needs. The reason I’m writing this from the ground up is to show how to do permission based views in VueJS and thinking about policies within your API.

To organize the code for the front end side of the administration section, I created the following folders (buckets) to drop our code into:

  • /resources/assets/js/api/admin
  • /resources/assets/js/components/admin
  • /resources/assets/js/modules/admin
  • /resources/assets/js/pages/admin

This will make sure we don’t mix up app components with our admin components.

The next piece of planning we will do is make an Admin.vue layout. Remember when we built a page layout for the app Building a Page Layout for Vue Router – Server Side Up? We are going to be doing the same thing for the admin section. Since we have 2 layouts now, I created a directory /resources/assets/js/layouts/ and moved our Layout.vue file into it from the pages directory. I also added an Admin.vue file into that directory with the following code:

<style lang="scss">
  @import '~@/abstracts/_variables.scss';
</style>

<template>
  <div id="admin-layout">
    <success-notification></success-notification>
    <error-notification></error-notification>

  </div>
</template>

<script>
  import SuccessNotification from '../components/global/SuccessNotification.vue';
  import ErrorNotification from '../components/global/ErrorNotification.vue';

  export default {
    components: {
        SuccessNotification,
        ErrorNotification
    }
  }
</script>

This will wrap all of our admin pages similar to the way the layout wraps all of our layout pages. For now, leave it as is. All that it does is have a success and error notification. We will be adding a router view and other components later, we are just setting The router view will be where we load all of the view information for the sub views.

The next step to prepare is to make sure that the app layout is referenced correctly in the /resources/assets/js/routes.js file. All you have to do is adjust the layout route to load the component from the layouts directory like this:

component: Vue.component( 'Layout', require( './layouts/Layout.vue' ) ),

We are now ready to start building!

Adding Your Admin Section

Now that we have a layout, let’s add a quick admin section to the application.

First open up /resources/js/routes.js and find the routes[] array of your Vue Router. After all of your routes, on the highest level (which should be after your app layout) add the following code:

{
  path: '/admin',
  name: 'admin',
  component: Vue.component( 'Admin', require('./layouts/Admin.vue' ) ),
  beforeEnter: requireAuth,
}

What this does is make a simple admin section with a route of /admin that applies the Admin.vue layout we created. Also, before entering the route, we are requiring authentication. This is an admin section right? We don’t want guests to see this! Not that they’d have permissions anyway, that’s locked down on the server side ;).

But what about permissions? We talked about blocking general users, from accessing this page. We also have 3 permission groups that will see different sections of the admin section, how will we handle this?

That’s where the magic of vue router comes in and the adjustment of the requireAuth() method we made.

Applying Vue Router Route Permissions Security

So remember our requireAuth() method that blocks pages from un authorized users? Let’s revisit that.

We will be blocking off parts of our admin section, using the requireAuth() method, that we created here: Vue Router Navigation Guards with Vuex – Server Side Up. To refresh, this method submits an action to load the authenticated user from the API. Once loaded, we check to see if there is a user authenticated. If so, allow access, if not, redirect back to the /cafes route.

This works great when it’s just authenticated or not, but we need more security. We need to block routes based on the permission level of the authenticated user. Not only does the user have to be logged in, the user has to have a permission. We want to block off sections based off of the MINIMUM level of security. A little for shadowing, we will be adding an /admin/actions route which allows approval or denial of actions (I bet you could have guessed since the last tutorials have all been about adding these actions). The minimum level of security to access this would be a coffee shop owner. They will see only the actions that they have permission for. An admin will see all actions.

How will we know which routes need a higher permission and which routes don’t or just require a general user? Vue Router has a really sweet field you can add to your routes called meta and that’s what we will use (Route Meta Fields | Vue Router). Now check this out!

First, open your /resources/assets/js/routes.js file and look at the route named newcafe. This route already exists and is the route used to add a cafe. It also needs to have an authenticated user to access this route. However, the minimum level of security for this route is a user. Add the following to your route:

{
  path: 'new',
  name: 'newcafe',
  component: Vue.component( 'NewCafe', require( './pages/NewCafe.vue' ) ),
  beforeEnter: requireAuth,
  meta: {
    permission: 'user'
  }
},

See the meta in there? That will get passed to the requireAuthfunction in the to parameter. This means, we can check the permission level AGAINST the authenticated user to see if we should allow access. Sweet eh? Let’s adjust our routes array to look like this:

routes: [
  {
    path: '/',
    redirect: { name: 'cafes' },
    name: 'layout',
    component: Vue.component( 'Layout', require( './layouts/Layout.vue' ) ),
    children: [
      {
        path: 'cafes',
        name: 'cafes',
        component: Vue.component( 'Home', require( './pages/Home.vue' ) ),
        children: [
          {
            path: 'new',
            name: 'newcafe',
            component: Vue.component( 'NewCafe', require( './pages/NewCafe.vue' ) ),
            beforeEnter: requireAuth,
            meta: {
              permission: 'user'
            }
          },
          {
            path: ':slug',
            name: 'cafe',
            component: Vue.component( 'Cafe', require( './pages/Cafe.vue' ) )
          },
        ]
      },
      {
        path: 'cafes/:slug/edit',
        name: 'editcafe',
        component: Vue.component( 'EditCafe', require( './pages/EditCafe.vue' ) ),
        beforeEnter: requireAuth,
        meta: {
          permission: 'user'
        }
      },
      {
        path: 'profile',
        name: 'profile',
        component: Vue.component( 'Profile', require( './pages/Profile.vue' ) ),
        beforeEnter: requireAuth,
        meta: {
          permission: 'user'
        }
      },
      /*
        Catch Alls
      */
      { path: '_=_', redirect: '/' }
    ]
  },
  {
    path: '/admin',
    name: 'admin',
    component: Vue.component( 'Admin', require('./layouts/Admin.vue' ) ),
    beforeEnter: requireAuth,
    meta: {
      permission: 'owner'
    },
    children: [

      /*
        Catch Alls
      */
      { path: '_=_', redirect: '/' }
    ]
  },
]

Not only did we block off our existing requireAuth methods with a permission appropriate to what the user should be, we added a permission level of at least an owner to access the admin side. This is because a coffee shop owner can access this at any point, we can narrow down security from there based off of higher level security. We will also be filling in the children section of our admin route with the pages of our admin section as we create them.

Now that we have all of our routes set up with the minimum level of permission required, let’s adjust our requireAuth() method to handle this.

Accounting for Route Permissions with requireAuth()

In the /resources/assets/js/routes.js file we have our requireAuth() method that, as of right now, is like hey, are you logged in or not?

We need to adjust this to account for permissions. Within the requireAuth() method, we have a sub method called proceed(). That’s going to be where we focus our attention. The proceed() method determines if the user should proceed or not to the route. This gets called once the user has been loaded and stored in our Vuex module.

I’m going to show the code and then explain it, so adjust your proceed() method like this:

/*
  Determines where we should send the user.
*/
function proceed () {
  /*
    If the user has been loaded determine where we should
    send the user.
  */
  if ( store.getters.getUserLoadStatus() == 2 ) {
    /*
      If the user is not empty, that means there's a user
      authenticated we allow them to continue. Otherwise, we
      send the user back to the home page.
    */
    if( store.getters.getUser != '' ){
      switch( to.meta.permission ){
        /*
          If the route that requires authentication is a user, then we continue.
          All users can access these routes
        */
        case 'user':
          next();
        break;
        /*
          If the route that requires authentication is an owner and the permission
          the user has is greater than or equal to 1 (an owner or higher), we allow
          access. Otherwise we redirect back to the cafes.
        */
        case 'owner':
          if( store.getters.getUser.permission >= 1 ){
            next();
          }else{
            next('/cafes');
          }
        break;
        /*
          If the route that requires authentication is an admin and the permission
          the user has is greater than or equal to 2 (an owner or higher), we allow
          access. Otherwise we redirect back to the cafes.
        */
        case 'admin':
          if( store.getters.getUser.permission >= 2 ){
            next();
          }else{
            next('/cafes');
          }
        break;
        /*
          If the route that requires authentication is a super admin and the permission
          the user has is equal to 3 (a super admin), we allow
          access. Otherwise we redirect back to the cafes.
        */
        case 'super-admin':
          if( store.getters.getUser.permission == 3 ){
            next();
          }else{
            next('/cafes');
          }
        break;
      }
    }else{
      next('/cafes');
    }
  }
}

That’s a whole heck of a lot of code isn’t it? It’s really just a switch statement that determines if the user has access or not!

We first check to see if there is an authenticated user, that was there from the beginning:

if( store.getters.getUser != '' ){

}else{

}

If there is no user, back to the /cafes route ya go!

Next, since there is a user, we will switch based off of the route’s permission. The route they are going to which is in the to variable. To access the permission, you just access to.meta.permission and it gives the string we applied to the route! So now, we switch based off of that, and then check to see if the user’s corresponding permission matches up.

For example, let’s check the owner permission which would allow general access to the admin section:

switch(to.meta.permission){
...

case 'owner':
  if( store.getters.getUser.permission >= 1 ){
    next();
  }else{
    next('/cafes');
  }
break;

That checks the permission of the route. If it is an owner permission, we check to see if the user’s permission is >=1 meaning they are an owner or higher. If they are, allow access and call next() which takes them to the next route. If not, then they must be a permission level 0 a general user, so send them back to the /cafes screen.

This just repeats for the permissions in the system, blocking and allowing based on the permission necessary for the route and if they match with the user’s permission. We now can add any route needed and apply a meta field which has the permission necessary to access the route and it will be locked down through this system! This is a huge portion of what we do for the security on the VueJS side. Of course if you have any questions, feel free to shoot me a comment!

Now that we have our permissions in place, let’s get some of the admin section ready to rock and roll! In the next tutorial, we will actually be adding a page to the admin section to approve the actions we set up! If you have any questions about route security and user permissions, feel free to leave a comment below! In our book, we will be doing much more complex examples and a more thorough dive into VueJS Permissions. Sign up here: https://serversideup.us2.list-manage.com/subscribe?u=a27137fc57d223f1cc7b986db&id=1276f15943 for more information when it is released!

Keep Reading
View the course View the Course API Driven Development With Laravel and VueJS
Up Next → VueJS App Admin Screens

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.