Take care of your users!

Hey! Welcome back!

Do you remember my friend Anthony? The one working in a big corporate company who wants his own Slack clone?

In my previous article, we’ve seen how to put the first bricks of a Slack clone on top of Kuzzle.io with Vue.js. We managed to send and receive messages, but we accidentally forgot to show the user avatar along with each message. I’ve shown what we’ve done to my friend and he answered:

He no happy. No no no.

Note. As seen in Part 1, all the code we will use here is available in the GitHub repo I’ve set up. Just clone it and checkout the tag part-2.

The Simplest Avatar Ever

We keep things simple at this point, so

showing the avatar will consist in simply enabling the user to set the username and make that username visible next to the message. No profile picture nor fancy stuff, sorry dude.

First, add the following text field (along with some styling) next to the InputBar component into the App component:

<template>
<div class="main">
<div class="messages">
<messages
:messages="MessageStore.state.messages">
</messages>
</div>
<input-bar
:store="MessageStore">
</input-bar>
</div>
<div class="footer">
<div class="user-menu">

<input
class="user-menu-username input-box-text"
type="text"
placeholder="Username">
</input>

</div>
</div>

</template>

This allows the user to type his/her username before sending the message. Oh, I can hear you (and my friend Anthony as well) saying “WAT”…

You’re right, let’s tackle this proper user login later. But right now we have to stick to this dumb solution.

Cross-component communication?

Well, anyway: once our user has typed his username, we want it to be conveyed along with his messages, right? But wait, messages are shown within the Message component so… this smells like cross-component communication! Do you remember how we solved this problem in Part 1?

Yes! Let’s go on and create a user.js file within the src/store directory, which will simply contain the following code:

export default {
state: {
username: null
}
}

Although creating a whole store for just one variable may seem overkill, however this is a good way to split concerns:

The username is shared between the text field we’ve just added and the Message component, so it should live in a separate entity, accessible to both. Plus, this store is going to be enriched later, when we work on the login mechanism.

So, let’s bind the content of the text field to the username variable in our store by adding the following to the App component:

<template>
<div class="main">
<div class="messages">
<messages
:messages="MessageStore.state.messages">
</messages>
</div>
<input-bar
:store="MessageStore">
</input-bar>
</div>
<div class="footer">
<div class="user-menu">
<input
class="user-menu-username input-box-text"
type="text"
placeholder="Username"
v-model="UserStore.state.username">
</input>
</div>
</div>
</template>
<script>
import InputBar from './components/InputBar'
import Messages from './components/Messages'
import MessageStore from './store/messages'
import UserStore from './store/user'
export default {
data () {
return {
MessageStore,
UserStore
}
},
components: {
InputBar,
Messages
},
created () {
MessageStore.subscribeMessages()
}
}
</script>

In the script tag we imported the UserStore and exposed it to the template via the data attribute, then we bound the username variable to the input field via the v-model directive.

You remember this, don’t you?

Then, we want the username to be sent along with the message that is typed in the InputBar. So, guess who should be aware of it? Yes, the InputBar component itself. The best way to pass down a value to a component is via the props.

Remain on the App component and add to the input-bar tag the following line:

<input-bar
:store="MessageStore"
:current-user="UserStore.state">
</input-bar>

Then enable the InputBar component to accept it and pass it along to the message store:

<script>
export default {
props: ['store', 'currentUser'],
data () {
return {
newMessage: null
}
},
methods: {
onEnter () {
if (this.newMessage.length > 0) {
this.store.sendMessage(this.newMessage, this.currentUser)
this.newMessage = null
}
}
}
}
</script>

Then, in src/store/messages.js:

sendMessage (content, user) {
let message = {content, user, date: Date.now()}
kuzzle
.dataCollectionFactory('messages')
.createDocument(message)
},

And finally, in src/components/Message.vue:

<template><div class="message">
<span class="message-username">{{message.user.username}}</span>
<span class="message-content">{{message.content}}</span>
</div>
</template>

And… We’re done! :)

What about… A real Login?

We just made a stupid text field that allows you to randomly change your username while you’re typing your messages. “Look ma, I’m Luca! Look ma, I’m Greg! Look ma, I’m Steve Ballmer!”

Awesome. So, what about adopting a real-life solution like… A Login page?

A little disclaimer

I hope this won’t sound too dramatic to my friend Anthony but… You just can’t rely on a client-side login system. What we are going to do is tell our Single Page Application not to show the chat window if the user isn’t logged-in but this is very easily hackable since it’s all client-side JS.

The only thing you should rely on is your server-side security layer, which we’re going to see in a while.

“Don’t worry, I have a plan…”

From now on, we are going to have two pages: the Login page and the Chat page. If the user is not logged in, the Login page is displayed. Otherwise, it is the Chat page.

Who determines whether the user is logged-in or not? Our buddy Kuzzle, on the server side! In Kuzzle, a logged session corresponds to a JWT token that is sent to the client.

So this basically boils down to saying “if we have a token, we show the Chat page, otherwise we show the Login page”.

Sweet: ‘nuff talk, let’s code!

Introducing the Vue Router

In implementation terms, what we call here “pages” are actually covered by the notion of “routes” in Vue. A route, corresponds in Vue (and in all common SPA libraries) to a URL. So, following our architecture, we have:

  • /login to access our Login page and
  • /chat to access our Chat page.

The Vue Router allows us to say “Display the Login component when the URL matches the /login pattern” and “Display the Chat component when the URL matches the /chat pattern”.

One interesting thing is that we can add preconditions to be satisfied before a route is actually triggered. This enables us to say “if we have no user, redirect /chat to /login”, preventing any non-logged access to the Chat page.

Ok, ok, ok. First thing, super important: install the router:

$ npm install --save vue-router

Now let’s define our routes in a new file src/routes.js:

import Chat from './Chat'
import Login from './Login'
export default function (router) {
router.map({
'/': {
name: 'chat',
component: Chat,
auth: true
},
'/login': {
name: 'login',
component: Login
}
})
}

This looks great but hey, those routes use two components we currently don’t have! Let’s fix that.

The Login Component

Create your Login component in src/Login.vue containing the following code:

<template>
<form id="login" method="post" @submit.prevent="login">
<span class="title">The Awesome Slack Clone</span>
<div id="block">
<input v-model="username" type="text" name="username" id="username" placeholder="Username" class="input-box-text" required/>
<input v-model="password" type="password" name="password" id="password" placeholder="Password" class="input-box-text" required />
</div>
<button type="submit" class="login">Login</button>
<p class="error" v-if="state.errorMessage">{{state.errorMessage}}</p>
</form>
</template>
<script>
import {} from './style/login.css';
import UserStore from './store/user';
export default {
data () {
return {
username: null,
password: null,
state: UserStore.state
}
},
methods: {
login () {
UserStore.login(this.username, this.password);
}
}
}
</script>

I’ve highlighted in bold the Vue-specific code in the template. One interesting thing here is the submit.prevent event modifier, which means “instead of submitting the form the legacy HTML way, call the login method”. The rest is stuff you surely remember from Part 1.

Note. If you want your login page to be awesome and stylish, take this file and save it as src/style/login.css.

On the other hand, the login method of the component calls a login method on the UserStore. Let’s define it in src/store/user.js:

export default {
state: {
errorMessage: null,
username: null
},
login (username, password) {
this.state.username = username
router.go({name: 'chat'})

}
}

We just assign the given username to state and router.go to the chat route. Pretty dummy, for the moment, but this is the place where we talk to Kuzzle server to actually perform the login. We’ll rock the place later.

Note. Make sure to add the errorMessage attribute to state, it’s bound to the template to show possible login errors.

The Chat and App Components

This is pure refactoring. Let’s take things out of the App component and put them in the new src/Chat.vue component. The easiest way to do this is to rename src/App.vue to src/Chat.vue, then take the App component and leave it as follows:

<template>
<router-view></router-view>
</template>
<script>
export default {
replace: false
}
</script>

OMG.

App is now pretty empty, yes. Except for this weird router-view thing. No, really, I’m not joking: it really works. Why? Because router-view means “display here the component that corresponds to the current route”.

Let’s repeat it altogether:

Display here the component that corresponds to the current route.

Note. The “replace: false” option in the App Component is used to prevent Vue to replace the body tag with App (see below).

Tie-up things together

To enable the Vue Router to work, we have to make some changes to the src/main.js file to make it look as follows:

import {} from './style/global.css'
import Vue from 'vue'
import App from './App'
import VueRouter from 'vue-router'
import configRouter from './routes'
Vue.use(VueRouter)
export var router = new VueRouter()
configRouter(router)
router.start(App, 'body')

Here, we instantiate the Vue Router then the Router is in charge of starting the application when it is ready.

Note. It is important to tell Vue to use the Router before instantiating it.

Come on, try it! Go to http://localhost:8080/#!/login put a random username and push the login button. Notice that the username you provided is now displayed in the username text field on the bottom left: do you remember why? Data binding… The User Store?

Yes? Yeah! So, let’s wipe out that input and replace it with a proper span (in src/Chat.vue):

<span class="user-menu-username">
{{UserStore.state.username}}
</span>

Ok, now let’s go real-life for real

Who wants a Login page that logs everyone in without checking their identity?

Well…

Let’s start by preventing the access to the chat route when we’re not logged in. Make the following changes to src/routes.js:

import Chat from './Chat'
import Login from './Login'
import UserStore from './store/user'
export default function (router) {
router.map({
'/': {
name: 'chat',
component: Chat,
auth: true
},
'/login': {
name: 'login',
component: Login
}
})
router.beforeEach(function (transition) {
if (transition.to.auth && !UserStore.isAuthenticated()) {
transition.redirect('/login')
} else {
transition.next()
}
})

}

Here, we’re basically saying “Before accessing each route that needs authentication, ensure the user is logged, otherwise redirect to login”.

The isAuthenticated method is defined as follows in src/store/user.js:

isAuthenticated () {
return Boolean(this.state.username)
}

Now, let’s set up our back-end with a first-class login system.

Create the Users in Kuzzle

We won’t get very deep here in Kuzzle’s User Rights Management, so we will create a pair of users via the Back Office, a handy UI to perform this kind of operations.

If starting Kuzzle seemed easy to you in Part 1, starting the Back Office and connecting it to a running Kuzzle is even easier:

$ docker run --link my-kuzzle-container-name:kuzzle -p 3000:3000 kuzzleio/bo

Just replace my-kuzzle-container-name with the actual name of your Kuzzle container, that you can see by typing:

$ docker ps | grep kuzzleio/kuzzle

If you access your BO at http://localhost:3000/ you’ll most likely see this page:

An empty Kuzzle has no users in it, and the Anonymous user has all the rights. That’s not good. The Back Office helps you creating the first Administrator and reset the rights of the Anonymous user.

So go on: create your first administrator (for better security it is recommended to check the check-box at the bottom), then log in the Back Office. Welcome home, son!

Now head to the “Security” > “Users tab” in the left sidebar (or at http://localhost:3000/#/user/browse): you should now see a list containing just one user… Yourself!

I decided to call myself bobby, ok? It’s not easy to be Luca all the time.

Ok, first user… done. Then, guess what? Click on the “Add user” button!

You’re going to land on the User creation form, where you have to fill in the name of your new user (I’ve put “anthony” because I want to chat with my friend) and add the following JSON to the “User content” textarea:

{
"profile": "admin",
"password": "verySecretPassword"
}

Write the password in clear text, it will be encrypted once the user is created. Oh, and we’ll see what profile exactly means in another article.

Then click on the “Create” button at the bottom and say hello to your new user!

You should see the new user in the list. Go on and logout to check if you can login as your new user.

Real Login For Real, baby!

Now, all you have to do is make src/store/user.js look like the following:

import {router} from ‘../main’
import kuzzle from '../services/kuzzle'
export default {
state: {
errorMessage: null,
username: null
},

login (username, password) {
this.state.errorMessage = null
kuzzle.
login('local', {username, password}, '1h', (error, response) => {
if (error) {
this.state.errorMessage = error.message
return
}
if (response.jwt) {
this.state.username = username
router.go({name: 'chat'})
}
})

},
isAuthenticated () {
return Boolean(this.state.username)
}
}

So, what’s happening here? Nothing weird, really.

  • We ask Kuzzle to login with the ‘local’ strategy (i.e. users are stored locally, you can install other strategies github or ‘facebook’), providing the credentials and specifying we want a 1h session;
  • If there is an error, we display it;
  • If we don’t have an error and the response from Kuzzle contains a JWT Token, then we assume the login succeeded and we can navigate to the chat route.

Now go on, fellow: you can perform your first real login in your Slack Clone! You can open two windows and make your two users chat!

The JWT Token is your friend

Oh, right. My friend Anthony said that the login system works but… It’s not persistent when he refreshes the page. True.

That’s because we don’t keep track of the session, even if we have all the means to do it…

Remember that Kuzzle sent us that nice tiny token in the response to the Login? That’s actually very handy, since the token can be used to keep track of the the user’s session.

What if… When we receive the token, we could store it somewhere and recover it after the page is reloaded? Like… A cookie?

Yes! But we have something better. The Web API provides us with the marvellous window.sessionStorage which is basically the same thing as cookies but with a consistent API. And stop it: it’s available since IE8.

So, first thing, store the JWT in the sessionStorage as follows:

login (username, password) {
this.state.errorMessage = null
kuzzle.login('local', {username, password}, '1h', (error, response) => {
if (error) {
this.state.errorMessage = error.message
return
}
if (response.jwt) {
window.sessionStorage.setItem('jwt', response.jwt)
router.go({name: 'chat'})
}
})
},

Now, imagine we’re at page start and we found the JWT in the sessionStorage: how do we recover the user name? Easy. We call the whoAmI method of Kuzzle SDK.

Add the getCurrentUser method to src/store/user.js like as follows:

getCurrentUser (cb) {
var jwt = window.sessionStorage.getItem('jwt')
if (!jwt) {
cb('No current user.')
kuzzle.setJwtToken(undefined)
return false
}
kuzzle.setJwtToken(jwt)
kuzzle
.whoAmI((error, kuzzleUser) => {
if (error) {
window.sessionStorage.removeItem('jwt')
kuzzle.setJwtToken(undefined)
cb(error)
return false
}
this.state.username = kuzzleUser.id
cb(null, kuzzleUser)
})
}

Here, we check the existence of the JWT Token and set it into our Kuzzle SDK object via the setJwtToken method. Then we request Kuzzle the user that corresponds to the given token via the whoAmI method. If there’s an error, we reset everything and consider the token as expired or malformed, otherwise, we store the username and call the callback.

Why there’s a callback? Because the whoAmI method is asynchronous (it makes a call to the Kuzzle server over the network).

But wait, wait: we haven’t yet finished. Almost.

To avoid duplicating code, we now call getCurrentUser from the login method, like as follows:

login (username, password) {
this.state.errorMessage = null
kuzzle.login('local', {username, password}, '1h', (error, response) => {
if (error) {
this.state.errorMessage = error.message
return
}
if (response.jwt) {
window.sessionStorage.setItem('jwt', response.jwt)
this.getCurrentUser((error, user) => {
if (error) {
console.error(error.message)
return
}
router.go({name: 'chat'})
})

}
})
},

And, last but not least, we need to try recovering the currently logged user at application startup. So make the src/main.js look like the following:

import {} from './style/global.css'
import Vue from 'vue'
import App from './App'
import VueRouter from 'vue-router'
import configRouter from './routes'
import userStore from './store/user'
Vue.use(VueRouter)
export var router = new VueRouter()
configRouter(router)
userStore.getCurrentUser(() => {
console.log('Starting...')
router.start(App, 'body')
})

Note that, since getCurrentUser calls the callback even when no JWT token is found, the app always starts.

Now go on: login to your app and refresh your page :) Yeah, it just works.

Logout

Oh, my friend Anthony is completely depressed! He created 150 users for all his co-workers and would like to test them all but… No logout!

Are you going to take-up the challenge? See you in Part 3, featuring

  • Create and Browse Channels!
  • Manage User Rights!
  • Create Your Own Integration Bot!

Please, feel free to leave any feedback! I’d love to hear from you, guys!

--

--

Luca Marchesini
Building a Slack clone with Vue.js and Kuzzle.io

(very curious) Full Stack Web Engineer and Folktales Storyteller. Traveler and Parmesan cheese dealer.