Build a chat app in Flask and Vue with sentiment analysis - Part 2: Building the chat interface
You will need Node 8.9+ and Python 3.6+ installed on your machine.
In this part, we are going to design/create the database, build the chat interface and also implement the login and registration endpoints. The code is available on GitHub.
This is the second part of this tutorial series. In the first part, we set up our working environment and also created the project. We were able to communicate with Flask from the Vue app.
Creating the chat interface
Vue enables us to build reusable components which make up our app user interface. We’ll split the app UI into smaller components so we can build them separately:
In the image above we have:
- The area with the red border is our base component which will house other components. This is the entire app component area that Vue creates by default. The file is located in
src/App.vue
. - [1] -
Users
component. This component will be responsible for listing all our registered users. - [2] -
Messages
component. This component renders messages. - [3] -
MessageInput
component. This is the input form for sending messages. - [4] -
NavBar
component. This is the navigation bar at the top of the app.
If you go through the Vue app in the project root folder, you will see a couple of files already created.
Since we are building a one-to-one private chat, we need a way to uniquely identify every user of our app. We’ll do so using their username. This means they have to log in to use the app. Once they are logged in, they’d be able to see the chat interface above.
We’ll also create a Login
component which will build up the login page.
Before we start building these components, let’s add Bootstrap-vue to speed up the design process. Bootstrap-vue project already structures our normal Bootstrap into components which will be easy for us. You can read more on the documentation here.
Adding Bootstrap-vue
Add bootstrap-vue using Vue command from the one-to-one
root folder:
$ vue add bootstrap-vue
📦 Installing vue-cli-plugin-bootstrap-vue...
+ vue-cli-plugin-bootstrap-vue@0.1.0
added 1 package from 1 contributor and audited 13896 packages in 71.714s
found 0 vulnerabilities
✔ Successfully installed plugin: vue-cli-plugin-bootstrap-vue
? Use babel/polyfill? No
🚀 Invoking generator for vue-cli-plugin-bootstrap-vue...
📦 Installing additional dependencies...
Vue will handle all the configuration settings for us. If the command ran successfully, you will notice a new folder named plugins in src
folder. In this folder, you will also find a file named bootstrap-vue.js
that imports the Bootstrap files.
App.vue component
As mentioned earlier, the src/App.vue
file is the main entry components housing all other components in the app. This means we’re going to import every other component to this file.
Also, we are using the single file component structure approach to create our components, which have three sections:
<template>
section. This section holds all markup, basically our HTML markup.<script>
section. This is where our JavaScript code resides.<style>
section. For adding styles such as CSS.
A component file looks like this:
<template>
<div>
Some HTML markup
</div>
</template>
<script>
console.log("Some JavaScript code");
</script>
<style>
/* Styles */
.style {
color: lime;
}
</style>
Now, replace the content in the <template>
section of the src/App.vue
file with the below markup:
<template>
<div id="app">
<Login />
<b-container>
<NavBar :logged_user="logged_user_username" />
<b-row class="main-area">
<b-col cols="4" class="users">
<Users />
</b-col>
<b-col cols="8" class="messages-area">
<div class="messages-main">
<div
v-if="!current_chat_channel"
class="select-chat text-center"
>
Select a user to start chatting...
</div>
<Messages
v-else
:active_chat="active_chat_id"
:messages="messages[current_chat_channel]"
/>
</div>
<MessageInput />
</b-col>
</b-row>
</b-container>
</div>
</template>
This is our whole chat interface we have defined with some HTML, the bootstrap-vue components and our app components, which we have broken down in the image above earlier.
We’re yet to create the files for our components, let’s do so now. Create the below files in the src/components/
folder:
- Login.vue
- NavBar.vue
- Users.vue
- MessageInput.vue
- Messages.vue
Next, import and register the components. Replace the content in the <script>
section in src/App.vue
file with the below code:
<script>
import MessageInput from "./components/MessageInput.vue";
import Messages from "./components/Messages.vue";
import NavBar from "./components/NavBar.vue";
import Login from "./components/Login.vue";
import Users from "./components/Users.vue";
import Pusher from "pusher-js";
// Declare pusher variable so it's global to this file.
let pusher;
export default {
name: "app",
components: {
MessageInput,
NavBar,
Messages,
Users,
Login
},
data: function() {},
methods: {},
};
</script>
When you import a component, you need to let Vue know of the component, which is what we did in the components: {
block. We also imported the Pusher JavaScript library, which we are going to use to communicate with Pusher from our client.
Next, add some styling. Update the styles in the <style>
section with the below code in src/App.vue
:
<style>
.messages-main {
overflow-y: scroll;
height: 90%;
}
.users {
padding: 0px !important;
border: 1px solid gray;
}
.no-margin {
margin: 0px;
}
.messages-area {
border: 1px solid gray;
padding: 0px !important;
max-height: calc(100vh - 4em) !important;
}
.input-message {
height: 40px;
}
.active {
background: #17a2b8 !important;
border: #17a2b8 !important;
}
.select-chat {
margin-top: 35vh;
padding: 8px;
}
.main-area {
margin: 0px;
min-height: calc(100vh - 5em) !important;
}
.logged_user {
color: white;
}
</style>
Now load up the Vue app in your browser again to see what we have. You should notice the page is almost empty but without any error:
That’s a good sign 🙂.
The components we have created are all empty. The next thing we will do is to build up the components and then observe the chat interface come to life as we build along.
You can leave the page open on your browser and observe the changes to the page when we update the component files.
Login.vue
Add the below code to src/components/Login.vue
:
<template>
<div class="login">
<div v-if="proccessing" class="text-center"> Please wait... </div>
<div v-if="message" class="text-center"> {{message}} </div>
<b-form-input
v-model="username"
type="text"
class="input-form"
placeholder="Username">
</b-form-input>
<b-form-input
v-model="password"
class="input-form"
type="password"
placeholder="Password">
</b-form-input>
<b-button
v-on:click="login"
variant="primary"
class="btn-block"
>
Log me in
</b-button>
</div>
</template>
<script>
export default {
name: "Login",
data() {
return {
username: "",
password: "",
proccessing: false,
message: ""
};
},
};
</script>
<style scoped>
.login {
width: 500px;
border: 1px solid #cccccc;
background-color: #ffffff;
margin: auto;
margin-top: 200px;
padding: 20px;
}
.input-form {
margin-bottom: 9px;
}
</style>
Here we added two input fields: username and password and a button to submit the form. Then we bind the input fields to our data. In the <script>
section, we exported an object defining data for the component.
NavBar.vue
Next, add the content for the NavBar component to src/components/NavBar.vue
:
<template>
<b-navbar toggleable="md" type="dark" variant="info" class="nav-bar">
<b-navbar-toggle target="nav_collapse"></b-navbar-toggle>
<b-navbar-brand href="#">ChitChat</b-navbar-brand>
<b-collapse is-nav id="nav_collapse">
<b-navbar-nav class="ml-auto logged_user" >
Welcome back {{logged_user}}
</b-navbar-nav>
</b-collapse>
</b-navbar>
</template>
<script>
export default {
name: "NavBar",
props: {
logged_user: String
}
};
</script>
<style scoped>
.nav-bar {
border-bottom: 1px solid #17a2b8;
}
</style>
Users.vue
Add the below code to src/components/Users.vue
for the Users component:
<template>
<div style="margin-top: 0px;">
<div v-for="(user, id) in users" v-bind:key="id">
<div
v-bind:class="[activeUser == user.id ? 'user active' : 'user']"
v-on:click="chat(user.id)"
>
{{user.userName}}
<span v-if="user.has_new_message" class="has_new_message">New message</span>
</div>
</div>
</div>
</template>
<script>
export default {
name: "Users",
props: {
users: Array
},
data() {
return {
activeUser: null
};
},
methods: {
chat: function(id) {
this.activeUser = id;
this.$emit("chat", id);
}
}
};
</script>
<style scoped>
.user {
margin: 0px !important;
padding: 10px 4px 10px 8px;
border-bottom: 1px solid gray;
}
.active {
background: #17a2b8;
color: white;
}
.has_new_message {
background-color: #17a2b8;
border-radius: 4px;
display: inline-block;
color: white;
margin-bottom: -4px;
font-size: 10px;
margin: 4px;
padding: 3px;
font-weight: bolder;
}
</style>
Notice the v-for directive we are using to render our users. The users
array will be passed from the src/App.vue
component as property.
We are also using the v-on
(v-on:click="chat(user.id)"
) directive to listen for click events when a user is clicked. If a user is clicked, we then call the chat function we have defined in the methods property.
Also, in the chat function, we are emitting an event to src/App.vue
so that the chat for that user can be initialized.
Also, <span v-if="user.has_new_message" class="has_new_message"
will display
the text: “New message” on the users tab whenever they get a message from a user they are not currently chatting with.
Messages.vue
Add the below code to src/components/Messages.vue
for the Messages component:
<template>
<div>
<div v-for="(message, id) in messages" v-bind:key="id">
<div class="chat-message col-md-5"
v-bind:class="[(message.from_user == active_chat) ? 'to-message' : 'from-message offset-md-7']">
{{message.message}}
</div>
</div>
</div>
</template>
<script>
export default {
name: "Messages",
props: {
messages: Array,
active_chat: Number
}
};
</script>
<style>
.from-message {
background: #17a2b8;
color: white;
border-radius: 3px;
padding: 8px 2px;
margin-bottom: 4px;
}
.to-message {
background: rgb(201, 209, 209);
color: rgb(41, 53, 52);
border-radius: 3px;
padding: 8px 2px;
margin-bottom: 4px;
}
</style>
MessageInput.vue
Add the below code to src/components/MessageInput.vue
for the MessageInput component:
<template>
<div class="message-input">
<b-form-input
v-model="message_input"
type="text"
placeholder="Enter your message"
v-on:keyup.enter.native="send_message"
>
</b-form-input>
</div>
</template>
<script>
export default {
name: "MessageInput",
data() {
return {
message_input: ""
};
},
methods: {
send_message() {
this.$emit("send_message", this.message_input);
this.message_input = "";
}
}
};
</script>
<style scoped>
.message-input {
position: absolute;
bottom: 0px;
width: 100%;
}
</style>
We now have our chat interface.
Don’t worry that the login form and the chat area are together, we’ll fix it in the next part.
The database
We will use SQLite for the database.
For our chat app, we’ll create three tables:
- users — This will hold all the users in the application.
- channels — Once two users start a conversation, we will create a new channel for them and store the channel name to the database. So that for subsequent conversation, we don’t need to create a new channel for them.
- messages — We’ll store every conversation to this table. We don’t want a situation where a user logs out and logs in again to find out their previous messages are missing.
Add the below code to api/database.py
to prepare the database connection:
from sqlalchemy import create_engine
from sqlalchemy.orm import scoped_session, sessionmaker
from sqlalchemy.ext.declarative import declarative_base
engine = create_engine('sqlite:///database.db', convert_unicode=True)
db_session = scoped_session(sessionmaker(autocommit=False,
autoflush=False,
bind=engine))
Base = declarative_base()
Base.query = db_session.query_property()
def init_db():
import models
Base.metadata.create_all(bind=engine)
We are using SQLAlchemy to initialize our database connection.
In the init_db()
function, we imported our models and finally call Base.metadata.create_all
to create all the tables specified in the model’s file.
Create the models
Let’s create a model for easy CRUD operations.
Add the model definition to api/models.py
:
from sqlalchemy import Column, Integer, String, Text, ForeignKey
from database import Base
class User(Base):
__tablename__ = 'users'
id = Column(Integer, primary_key=True)
username = Column(String(50), unique=True)
password = Column(String(128))
def __init__(self, username=None, password=None):
self.username = username
self.password = password
def __repr__(self):
return '<User %r>' % (self.username)
class Channel(Base):
__tablename__ = 'channels'
id = Column(Integer, primary_key=True)
name = Column(String(60))
from_user = Column(Integer, ForeignKey('users.id'))
to_user = Column(Integer, ForeignKey('users.id'))
class Message(Base):
__tablename__ = 'messages'
id = Column(Integer, primary_key=True)
message = Column(Text)
from_user = Column(Integer, ForeignKey('users.id'))
to_user = Column(Integer, ForeignKey('users.id'))
channel_id = Column(Integer, ForeignKey('channels.id'))
In this file, we create three classes which define the structure of the tables our app will be using.
Now import the files to api/app.py
:
# ./api/app.py
# [...]
from database import db_session
from models import User, Channel, Message
# [...]
Next, let’s close the connection to the database once an operation is complete. Add the following code to api/app.py
after app = Flask(__name__)
line:
@app.teardown_appcontext
def shutdown_session(exception=None):
db_session.remove()
Create the database and tables
Now, let’s create the database and tables. Open up a new command window and change your directory to the project’s root folder, activate your virtualenv and then run the below commands:
# Go to the Flask app
$ cd api
# Activate your virtualenv
# Enter python interactive shell
$ python
>>> from database import init_db
>>> init_db()
If there is no error, a new file named database.db
will be created in the api
folder.
⚠️ You might get an error if your virtualenv is not activated. You need to activate your virtualenv before running the above command.
Authentication Route
We are almost done. We need some endpoints for adding and authenticating our users.
The Werkzeug is a Python utility library which Flask depends on. Since we have installed Flask, we’ll also have access to the library.
Import the function for generating and checking password hash from the Werkzeug library to api/app.py
file:
#app.py
#[...]
from werkzeug.security import generate_password_hash, check_password_hash
#[...]
Register
Next, add the route for adding new users to api/app.py
:
@app.route('/api/register', methods=["POST"])
def register():
data = request.get_json()
username = data.get("username")
password = generate_password_hash(data.get("password"))
try:
new_user = User(username=username, password=password)
db_session.add(new_user)
db_session.commit()
except:
return jsonify({
"status": "error",
"message": "Could not add user"
})
return jsonify({
"status": "success",
"message": "User added successfully"
}), 201
Here we created a new route named /api/register
which is only available via a POST request. It will accept JSON object containing the new user details - username and password.
Finally, we added the user to the database. If an error occurred while adding, we inform the user with a JSON response:
return jsonify({
"status": "error",
"message": "Could not add user"
})
Else we respond with a success message:
return jsonify({
"status": "success",
"message": "User added successfully"
}), 201
Using a REST client like Postman, you can now register a new user:
To register users using Postman, open up the Postman app then,
- Close the first pop up that appears.
- Select POST as the request method.
- Add http://localhost:5000/api/register to the request URL field.
- Click on the Body tab right below the URL field.
- Then choose raw in the options that appear after the Body tab you just selected.
- In the same line with the raw option, select JSON (application/json) in the drop down.
- Now, add the user’s information you want to register to the text field that appears after the option you just selected:
{
"username": "<username>",
"password": "<password>"
}
Make sure to replace <username>
and <password>
placeholders with the information of the user you want to register.
- Finally, click on the Send button to send the request to the server.
Register a couple of users and note down their credentials. We’ll use it to test the app later.
Login
One way of securing APIs and single page applications is by using JWT. It’s an encrypted token generated to securely transfer information between services. To implement JWT for our app, we will use the Flask-JWT-extended package. We have already installed the package so we’ll go ahead and use it.
Import the package and configure it to use Flask app in api/app.py
:
from flask_jwt_extended import (
JWTManager, jwt_required, create_access_token,
get_jwt_identity
)
From the package, we imported the following functions:
- JWTManager — The Python class for configuring the package to use Flask app config.
- jwt_required — A decorator for authenticating our routes.
- created_access_token — A function for generating a token.
- get_jwt_identity — A function for getting the identity (in our case the username) from a token.
Next, let’s configure the package to use Flask app config. Add the below code to api/app.py
immediately after app = Flask(__name__)
line:
app.config['JWT_SECRET_KEY'] = 'something-super-secret' # Change this!
jwt = JWTManager(app)
Next, add the login route to api/app.py
:
@app.route('/api/login', methods=["POST"])
def login():
data = request.get_json()
username = data.get("username")
password = data.get("password")
user = User.query.filter_by(username=username).first()
if not user or not check_password_hash(user.password, password):
return jsonify({
"status": "failed",
"message": "Failed getting user"
}), 401
# Generate a token
access_token = create_access_token(identity=username)
return jsonify({
"status": "success",
"message": "login successful",
"data": {
"id": user.id,
"token": access_token,
"username": user.username
}
}), 200
With this, we now have our login route - /api/login
which is available via a POST requests. The route expects a expects a JSON object to be passed along that contains the details of the user.
To test the login route, use the details of the user you just registered.
Conclusion
In this tutorial, we created our chat interface that was composed of Vue components. We also created the database for the chat application using SQLAlchemy.
Finally, we created a login and registration endpoint and also setup JWT for authenticating users and protecting our routes.
8 September 2018
by Gideon Onwuka