Saturday, July 27

Managing User Permissions in Vue using CASL

There is one thing we can all agree on, no matter what language or platform we prefer for building applications — there has to be some form of control and access levels in our applications to ensure it runs smoothly. This is why the concept of user permission will quickly become commonplace for you once you build your first application.

In server-side languages, user permissions can be done with little or no fuss. You can use sessions to hold a user’s information and there would be over a hundred libraries begging for the opportunity to help you manage what the user sees and when the user sees it. You can manage complex permission logic with the aid of a robust database.

For JavaScript, this becomes a little tricky, given that all you may have to achieve this localStorage. In this tutorial, we will explore how we can manage user permission for our JavaScript application using CASL.

What is CASL

CASL is an authorization JavaScript library which lets us define what resources a given type of user can access. CASL forces us to think about permissions in terms of abilities — what a user can or cannot do vs roles — who is this user. In defining the abilities of a user, the user role can be composed.

Getting Started

We will use an authenticated Vue application we previously created so we can speed things up. In Vue Authentication And Route Handling Using Vue Router, we had created an application with different user types. For this tutorial, we will extend the application to add a page with blog posts that can only be edited by the creator.

Clone The Repository For The Project

$ git clone https://github.com/christiannwamba/vue-auth-handling

Install Dependencies

$ npm install

Install CASL

$ npm install @casl/vue @casl/ability

We have all the basics we need setup now. Let’s proceed to make the components for our application. We are working off an existing project, so this will save us a lot of time. We need to add 2 new components to the project to enable us to create blog posts and view blog posts.

The BlogManager

First, create a file BlogManager.vue in the ./scr/components and add the following to it:

<template>
    <div class="hello">
        <h1>Create New Blog</h1>
        <form @submit="create">
            <input class="form-input" type="text" placeholder="Blog Title..." v-model="blog_title">
            <textarea class="form-input" v-model="blog_body" placeholder="Type content here"></textarea>
            <button>Create</button>
            <br/>
        </form>
    </div>
</template>

This creates a simple HTML page with a form for our application. This is the form for creating a new blog post.

We need to create the data attributes to bind the form fields:

[...]
<script>
  export default {
      data () {
          return {
              blog_title: null,
              blog_body: null
          }
      },
  }
</script>

Let’s create the method that handles form submission:

<script>
  export default {
      [...]
      methods : {
          create(e){
              e.preventDefault()
              let user = JSON.parse(localStorage.getItem('user'))
              this.$http.defaults.headers.common['x-access-token'] = localStorage.jwt
          }
      }
  }
</script>

We have created the method and parsed the user string we stored in localStorage. This user string will come in handy when we are sending our form data to the server. We also setup the default headers for our http request handler — axios. Some of our endpoints require an access token to work, which is why we need to set it.

In the Vue Authentication … tutorial, we had explained how we made axios globally accessible by all our Vue components.

Now, let’s send the blog post data to our server:

<script>
  export default {
      [...]
      methods : {
          create(e){
              [...]
              this.$http.post('http://localhost:3000/blog', {
                  blog_title: this.blog_title,
                  blog_body: this.blog_body,
                  created_by : user.id
              })
              .then(response => {
                  alert(response.data.message)
                  this.blog_title = null
                  this.blog_body  = null
              })
              .catch(function (error) {
                  console.error(error.response);
              });
          }
      }
  }
</script>

After we get a successful response, we set the form fields to null so that the user can create a new blog right away, if they wanted.

Let’s add some styles to make the page look pretty ?

<style scoped>
h1, h2 {
    font-weight: normal;
}
button {
    border-radius: 2px;
    font-size: 14px;
    padding: 5px 20px;
    border: none;
    background: #43bbe6;
    color : #ffffff;
    font-weight: 600;
    cursor: pointer;
    transition: 0.2s all;
}
button:hover {
    background: #239be6;
    transition: 0.2s all;
}
.form-input {
    min-width: 50%;
    border: 1px #eee solid;
    padding: 10px 10px;
    margin-bottom: 10px;
}
textarea {
    resize: none;
    height: 6em;
}
</style>

The Blog

We need to make a simple component for displaying the blog posts we create. Create a file Blog.vue in the ./scr/components and add the following to it:

<template>
    <div class="hello">
        <h1>Welcome to blog page</h1>
        <div class="blog" v-for="blog,index in blogs" @key="index">
            <h2>{{blog.title}}</h2>
            <p>{{blog.body}}</p>
        </div>
    </div>
</template>

In the code block above, we loop through the blogs we retrieved from the server and display them using Vue’s v-for loop construct.

Let’s add the script to fetch the data:

[...]
<script>
    export default {
        data () {
            return {
                blogs: []
            }
        },
        mounted(){
            this.$http.get('http://localhost:3000/blog')
            .then(response => {
                this.blogs = response.data.blogs
            })
            .catch(function (error) {
                console.error(error.response)
            });
        }
    }
</script>

It’s important we defined the blogs attribute as an empty array. This is to prevent the page from throwing errors when it loads.

Also, we used mounted() as against beforeMount() so that our users can see the blog page even before the content is loaded. If for any reason a network error causes a delay in the content being loaded, our users would not be starring at a blank page loading forever.

Now, let’s add some styles to spice things up ?

<style scoped>
h1, h2 {
    font-weight: normal;
}
.blog {
    width: 60%;
    border: 1px #eee solid;
    padding: 20px;
    padding-top: 0px;
    display: table;
    margin: 0 auto;
    margin-bottom: 20px;
    text-align: left;
}
.blog h2 {
    text-decoration: underline;
}
.delete {
    border-radius: 2px;
    background: #aaa;
    height: 24px;
    min-width: 50px;
    padding: 4px 7px;
    color: #ffffff;
    font-size: 14px;
    font-weight: 900;
    border: none;
    cursor: pointer;
    transition: 0.2s all;
}
.delete:hover {
    background: #ff0000;
    transition: 0.2s all;
}
</style>

Updating Server Scripts

We have made some significant changes to the frontend of our application. We need to make corresponding changes to the server to support it.

Db Manager

From the ./server directory, open the db.js file and add the following:

[...]
class Db {
    constructor(file) {
        [...]
        this.createBlogTable()
    }
    [...]
}

You would notice we have a this.createTable() method in the class constructor that creates the user table. We also want to create the blog table if it does not exist whenever or wherever our Db class is called.

Let’s add the createBlogTable method:

[...]
class Db {
    [...]
    createBlogTable() {
        const sql = `
            CREATE TABLE IF NOT EXISTS blog (
            id integer PRIMARY KEY, 
            title text NOT NULL, 
            body text NOT NULL,
            created_by integer NOT NULL)
        `
        return this.db.run(sql);
    }
}

Let’s add the method for selecting all blogs:

[...]
class Db {
    [...]
    selectAllBlog(callback) {
        return this.db.all(`SELECT * FROM blog`, function(err,rows){
            callback(err,rows)
        })
    }
}

Let’s also add the method for adding a new blog:

[...]
class Db {
    [...]
    insertBlog(blog, callback) {
        return this.db.run(
            'INSERT INTO blog (title,body,created_by) VALUES (?,?,?)',
            blog, (err) => {
                callback(err)
            }
        )
    }
}

Then the method for updating a blog:

[...]
class Db {
    [...]
    updateBlog(blog_id, data, callback) {
        return this.db.run(
            `UPDATE blog SET title = ?, body = ?) where id = ${blog_id}`,
            data, (err) => {
                    callback(err)
            }
        )
    }
}

Finally, the method for deleting a blog:

[...]
class Db {
    [...]
    deleteBlog(blog_id, callback) {
        return this.db.run(
            'DELETE FROM blog where id = ?', blog_id, (err) => {
                callback(err)
            }
        )
    }
}

Our database manager script is now up-to-date. Let’s update the app server script.

The App Server Script

The changes we will make here will expose endpoints for our server that our application can plug into. First, the endpoint for viewing all blogs.

Open the app.js file and add the following:

[...]
router.get('/blog', function(req, res) {
    db.selectAllBlog((err, blogs) => {
        if (err) return res.status(500).send("There was a problem getting blogs")

        res.status(200).send({ blogs: blogs });
    }); 
});
[...]

As you can see, we use the selectAllBlog method we created in the database manager script and pass a callback method that should handle what happens when the data is retrieved.

Next, let’s add the method for creating a new blog:

[...]
router.post('/blog', function(req, res) {
    let token = req.headers['x-access-token'];
    if (!token) return res.status(401).send({ auth: false, message: 'No token provided.' });

    jwt.verify(token, config.secret, function(err) {
        if (err) return res.status(500).send({ auth: false, message: 'Failed to authenticate token.' });

        db.insertBlog(
            [
                req.body.blog_title,
                req.body.blog_body,
                req.body.created_by,
            ],
            function (err) {
                if (err) return res.status(500).send("Blog could not be created.")

                res.status(201).send({ message : "Blog created successfully" });
            }
        ); 
    });
});
[...]

We first check if the access token is available. If it is missing, we deny the request instantly before anything else. If however, the access token is not empty, we try and verify it’s validity. If it is expired, we deny the request. If it does not exist, we also deny the request. If it is valid, we proceed to add the blog post information to the database and return a success message.

We can do more here in the way of validating the data but that would make the code block too long and complex, defeating the aim of this tutorial.

Finally, let’s add the method for deleting a blog post:

[...]
router.delete('/blog/:id', function(req, res) {
    let token = req.headers['x-access-token'];
    if (!token) return res.status(401).send({ auth: false, message: 'No token provided.' });

    jwt.verify(token, config.secret, function(err) {
        if (err) return res.status(500).send({ auth: false, message: 'Failed to authenticate token.' });

        db.deleteBlog(
            req.params.id,
            function (err) {
                if (err) return res.status(500).send("There was a problem.")

                res.status(200).send({ message : "Blog deleted successfully" });
            }
        ); 
    });
});
[...]

And that concludes the updates we have to do on the server.

Setting Up CASL

We have everything ready to use CASL in our application. Let’s start by defining the ability users can have. In the ./src directory, create another directory config which would hold our ability configurations. Inside the config directory you just created, create the file ability.js and add the following:

import { AbilityBuilder } from '@casl/ability'

var user = JSON.parse(localStorage.getItem('user'))
function subjectName(item) {
    if (!item || typeof item === 'string' || !user) {
            return item
    }
    else if(item.created_by === user.id || user.is_admin === 1){
            return 'Blog'
    }
}

Before you say “What is user doing here again”, just be a little patient as I explain. Now, the function — subjectName will be used by CASL’s AbilityBuilder to determine who would have the ability to perform certain actions.

The first thing we check is that whenever we want to check the user’s ability to carry out an action on anything, two things must be true:

  1. The thing to perform an action on is not empty
  2. The user exists

In the case of our blog, we confirm that the above conditions are true, then we move on to check if the blog was created by the user or if we have an admin user at hand. If that is true, we return “Blog”, which is the subjectName we will use to assign abilities.

Now, let’s assign some abilities ? :

[...]
export default AbilityBuilder.define({ subjectName }, can => {
  can(['read'], 'all')
  if(user) can(['create'], 'all')
  can(['delete'], 'Blog')
})

The can(['read'], 'all') method basically means the following;

The user who meets this requirement can read all things that exists inside this application. Be it blogs, comments, whitepapers — anything at all.

With is explanation in mind, you will observe that can(['delete'], 'Blog') means this user can delete this blog.

If it all doesn’t make sense to you, wait till we make use of it.

Add CASL Plugin To Vue Setup

To make our ability configurations globally accessible, we need to add it to our Vue setup. Open the ./src/main.js file and edit as follows:

[...]
import ability from './config/ability'
import { abilitiesPlugin } from '@casl/vue'

Vue.prototype.$http = Axios;

Vue.config.productionTip = false
Vue.use(abilitiesPlugin, ability)
[...]

We have imported the CASL’s abilitiesPlugin and the ability configurations we made above. By telling Vue to use it, we have effectively made the ability configurations globally accessible using the $can method the abilitiesPlugin will expose.

Use The Abilities In Our Blog Component

Now that we have defined abilities, let’s try them out with our blog component. Open the Blog.vue file and add the following:

<template>
    <div class="hello">
        [...]
        <div class="blog" v-for="blog,index in blogs" @key="index">
            [...]
            <button class="delete" v-if="$can('delete',blog)" @click="destroy(blog)">Delete</button>
        </div>
    </div>
</template>
[...]

We have added a delete button to each blog and will only display it if the user has the permission to delete a particular blog post. Recall that we will only allow a blog creator or an admin user to delete a blog post, and no other type of user.

Now, let’s add the method to handle delete when a user clicks it:

[...]
<script>
  export default {
      [...]
      methods : {
          destroy(blog){
              this.$http.defaults.headers.common['x-access-token'] = localStorage.jwt
              this.$http.delete(`http://localhost:3000/blog/${blog.id}`)
              .then(response => {
                  console.log(response)
              })
              .catch(function (error) {
                  console.error(error.response);
              });
          }
      }
  }
</script>
[...]

Now, when our users are browsing our blog page, their view will be something like this

A user who is not authenticated

An authenticated user who has some blog posts

Admin sees all as he has the power to do and undo

Update Vue Router Links

This is the last thing we need to do for our application to be ready. Open the ./src/router/index.js file and add the following to it:

[...]
let router = new Router({
  mode: 'history',
  routes: [
    [...]
    {
        path: '/blog',
        name: 'blog',
        component: Blog
    },
    {
        path: '/blog/create',
        name: 'blogmanager',
        component: BlogManager,
        meta: { 
            requiresAuth: true
        }
    },
  ]
})
[...]

And that’s it.

Run The Application

To run the application, you need to open the directory where it is located on your terminal. Then run the following command:

$ npm run dev

Also, open the directory on another instance of your terminal so we can start the server as well:

$ npm run server

Conclusion

In this tutorial, we have seen how to manage user permissions in our Vue application using CASL. How CASL abilities work was explained in details, to enable us to compose the unique permissions our applications will likely have.

So, try and create something awesome with it and share with me.


Source: Scotch.io

0 0 votes
Article Rating
Subscribe
Notify of
guest

0 Comments
Inline Feedbacks
View all comments
0
Would love your thoughts, please comment.x
()
x