DEV Community

Cover image for Build a twitter clone with Flask and React | PART 2
arnu515
arnu515

Posted on • Updated on

Build a twitter clone with Flask and React | PART 2

IF YOU HAVEN'T READ PART ONE YET, CLICK HERE
This is a 3 parter series. This is the second part.

Let's continue where we left off.

PART 2 - ADDING FUNCTIONALITY

Creating the home page

Let's create the page that we see AFTER we've logged in. I'm gonna create a new component called MainPage.jsx.

// src/components/MainPage.jsx
import React from "react";

class MainPage extends React.Component {
    render() {
        return (
            <React.Fragment>
                <div
                    className="w3-container w3-jumbo"
                    style={{ margin: "3rem", paddingLeft: "1rem" }}>
                    Tweets
                </div>
            </React.Fragment>
        );
    }
}

export default MainPage;

Enter fullscreen mode Exit fullscreen mode

For displaying a Tweet, let's create a separate TweetItem.jsx component. This component will be a stateless functional component.

// src/components/TweetItem.jsx
import React from "react";

function TweetItem(props) {
    return (
        <div
            className="w3-card w3-border w3-border-gray w3-round-large"
            style={{ marginTop: "2rem" }}>
            <div className="w3-container" style={{ padding: "2rem" }}>
                <h2 className="w3-opacity w3-xxlarge">{props.title}</h2>
                <div dangerouslySetInnerHTML={{ __html: props.content }}></div>
            </div>
            <footer className="w3-container w3-center w3-large">
                <button className="w3-button" style={{ marginRight: "2rem" }}>
                    Like
                </button>
                <button className="w3-button" style={{ marginRight: "2rem" }}>
                    Retweet
                </button>
                <button className="w3-button">Reply</button>
            </footer>
        </div>
    );
}

export default TweetItem;
Enter fullscreen mode Exit fullscreen mode

The dangerouslySetInnerHTML attribute added to the <div> element allows us to render the HTML from a string. And like its name suggests, it is dangerous, because any hacker can add <script> tags and execute malicious code. We are setting this attribute because we are going to use a WYSIWYG editor to allow the user to post their tweets with formatting. The WYSIWYG editor we're gonna use has precautions to prevent XSS Attacks.

Now, let's make a few dummy tweets to see how this goes. Update your MainPage.jsx to look like this:

import React from "react";
import TweetItem from "./TweetItem";

class MainPage extends React.Component {
    render() {
        let tweets = [
            {
                title: "Hello, world!",
                content: "<h3>Just gonna type html here!</h3>",
            },
            { title: "Tweet", content: "<code>Code!</code>" },
            {
                title: "Nice!",
                content:
                    "<a href='https://www.youtube.com/watch?v=dQw4w9WgXcQ'>Here's a link! I need to use single quotes for the href.</a>",
            },
            {
                title: "Hello, world!",
                content:
                    "<div>Typing <strong>using</strong> <em>more</em> <u>than</u> <sup>one</sup> <sub>html</sub> <del>tag</del>!</div>",
            },
        ];
        return (
            <React.Fragment>
                <div
                    className="w3-container w3-jumbo"
                    style={{ margin: "3rem", paddingLeft: "1rem" }}>
                    Tweets
                </div>
                <div className="w3-container">
                    {tweets.map((item, index) => {
                        return (
                            <TweetItem
                                title={item.title}
                                content={item.content}
                                key={index}
                            />
                        );
                    })}
                </div>
            </React.Fragment>
        );
    }
}

export default MainPage;
Enter fullscreen mode Exit fullscreen mode

As you can see, I'm iterating through every tweet in an array. I can use html tags to style the content. This is what your website should look like:
website


Adding a tweets model

Awesome! But, static data won't do! We need to get data from the database, but, we don't have any way to add tweets to our database! So, let's create a Tweet model like we created the Users model. Add this to app.py:

class Tweet(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    uid = db.Column(db.Integer, db.ForeignKey("user.id"))
    user = db.relationship('User', foreign_keys=uid)
    title = db.Column(db.String(256))
    content = db.Column(db.String(2048))
Enter fullscreen mode Exit fullscreen mode

So, if you see up there, I have added a new table (or model) called Tweet, and also, let's rename the class Users to User, I forgot that in the last part :P. Now, let's add some CRUD functions.


def getTweets():
    tweets = Tweet.query.all()
    return [{"id": i.id, "title": i.title, "content": i.content, "user": getUser(i.uid)} for i in tweets]

def getUserTweets(uid):
    tweets = Tweet.query.all()
    return [{"id": item.id, "userid": item.user_id, "title": item.title, "content": item.content} for item in filter(lambda i: i.user_id == uid, tweets)]

def addTweet(title, content, uid):
    if (title and content and uid):
        try:
            user = list(filter(lambda i: i.id == uid, User.query.all()))[0]
            twt = Tweet(title=title, content=content, user=user)
            db.session.add(twt)
            db.session.commit()
            return True
        except Exception as e:
            print(e)
            return False
    else:
        return False

def delTweet(tid):
    try:
        tweet = Tweet.query.get(tid)
        db.session.delete(tweet)
        db.session.commit()
        return True
    except Exception as e:
        print(e)
        return False
Enter fullscreen mode Exit fullscreen mode

I also made a few changes to the User class.

class User(db.Model):
    id = db.Column(db.Integer, primary_key = True) # primary_key makes it so that this value is unique and can be used to identify this record.
    username = db.Column(db.String(24))
    email = db.Column(db.String(64))
    pwd = db.Column(db.String(64))

    # Constructor
    def __init__(self, username, email, pwd):
        self.username = username
        self.email = email
        self.pwd = pwd

def getUsers():
    users = User.query.all()
    return [{"id": i.id, "username": i.username, "email": i.email, "password": i.pwd} for i in users]

def getUser(uid):
    users = User.query.all()
    user = list(filter(lambda x: x.id == uid, users))[0]
    return {"id": user.id, "username": user.username, "email": user.email, "password": user.pwd}

def addUser(username, email, pwd):
    try:
        user = User(username, email, pwd)
        db.session.add(user)
        db.session.commit()
        return True
    except Exception as e:
        print(e)
        return False

def removeUser(uid):
    try:
        user = User.query.get(uid)
        db.session.delete(user)
        db.session.commit()
        return True
    except Exception as e:
        print(e)
        return False
Enter fullscreen mode Exit fullscreen mode

Now, we can add some temporary routes and test if everything works. But first, since we made some changes to our Model, we need to reset the database. Find the file twitter.db and delete it. Now, type:

python -i app.py
Enter fullscreen mode Exit fullscreen mode

and press ^C to terminate it. You should be in the python console now. Type:

import app
app.db.create_all()
Enter fullscreen mode Exit fullscreen mode

And this should create twitter.db.

Now, let's add a route for adding a tweet and getting all tweets.

@app.route("/api/tweets")
def get_tweets():
    return jsonify(getTweets())

@app.route("/api/addtweet", methods=["POST"])
def add_tweet():
    try:
        title = request.json["title"]
        content = request.json["content"]
        uid = request.json["uid"]
        addTweet(title, content, uid)
        return jsonify({"success": "true"})
    except Exception as e:
        print(e)
        return jsonify({"error": "Invalid form"})
Enter fullscreen mode Exit fullscreen mode

Finally, let's test it. Make sure you already have a registered user. Type this command:

curl -X POST -H "Content-Type: application/json" -d '{"title": "a", "content": "e", "uid": 1}' "http://localhost:5000/api/addtweet"
Enter fullscreen mode Exit fullscreen mode

If all is good, you should get {"success": true} as an output.
Now, let's list the tweets:

curl "http://localhost:5000/api/tweets" 
Enter fullscreen mode Exit fullscreen mode

If your output looks similar to this, you're good!

[
  {
    "content": "e", 
    "id": 1, 
    "title": "a", 
    "user": {
      "email": "sasdasd@asdasd.sads", 
      "id": 1, 
      "password": "as", 
      "username": "df"
    }
  }
]
Enter fullscreen mode Exit fullscreen mode

Let's also add a delete route, so that we can delete tweets.

@app.route("/api/deletetweet", methods=["DELETE"])
def delete_tweet():
    try:
        tid = request.json["tid"]
        delTweet(tid)
        return jsonify({"success": "true"})
    except:
        return jsonify({"error": "Invalid form"})
Enter fullscreen mode Exit fullscreen mode

Of course, we have to test it!

curl -X DELETE -H "Content-Type: application/json" -d '{"tid": 1}' "http://localhost:5000/api/deletetweet"
Enter fullscreen mode Exit fullscreen mode
curl "http://localhost:5000/api/tweets"
# OUTPUT: []
Enter fullscreen mode Exit fullscreen mode

Securing our API with JWT

Let's say you decide to make your API public. Or someone finds out your API routes. He can then perform many post requests and possibly impersonate users and add tweets on their behalf. Nobody want's that right? So, let's add some authentication to our API using JWT.

JWT stands for Json Web Token. It allows us to verify each user if they've logged in. You can read more about it here To add JWT to your application, you need to install flask-jwt-extended:

pip install flask-jwt-extended
Enter fullscreen mode Exit fullscreen mode

We're using the extended version because it is easier to use.

Import JWT

from flask_jwt_extended import JWTManager, create_access_token, jwt_required, get_jwt_identity
Enter fullscreen mode Exit fullscreen mode

Now, change your Login route to return a json web token instead of true.

@app.route("/api/login", methods=["POST"])
def login():
    try:
        email = request.json["email"]
        password = request.json["pwd"]
        if (email and password):
            user = list(filter(lambda x: x["email"] == email and x["password"] == password, getUsers()))
            # Check if user exists
            if len(user) == 1:
                token = create_access_token(identity=user[0]["id"])
                return jsonify({"token": token})
            else:
                return jsonify({"error": "Invalid credentials"})
        else:
            return jsonify({"error": "Invalid form"})
    except Exception as e:
        print(e)
        return jsonify({"error": "Invalid form"})
Enter fullscreen mode Exit fullscreen mode

Before we run this code and test it out, we need to initialise JWT for our app like we did for CORS. Type this under where you declared app.

app = Flask(__name__)
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///twitter.db"
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
app.config["JWT_SECRET_KEY"] = "myawesomesecretisnevergonnagiveyouup"
CORS(app)
JWTManager(app)
Enter fullscreen mode Exit fullscreen mode

When you publish your website, you may want to make your secret more secure and/or put it in an environment variable. We'll cover that in the third part. Also, I added the SQLALCHEMY_TRACK_MODIFICATIONS value in the config to remove the annoying error we get in the console when we start our app. Now, if you try and login, you should get a token.

curl -X POST -H "Content-Type: application/json" -d '{"email": "email@domain.com", "pwd": "password"}' "http://localhost:5000/api/login"
Enter fullscreen mode Exit fullscreen mode

Replace the data with whatever you've registered with
And this should be your output:

{
  "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE1OTIwNDE2NDgsIm5iZiI6MTU5MjA0MTY0OCwianRpIjoiMjNiZWViMTEtOWI4Mi00MDY3LWExODMtZDkyMzAyNDM4OGU2IiwiZXhwIjoxNTkyMDQyNTQ4LCJpZGVudGl0eSI6MiwiZnJlc2giOmZhbHNlLCJ0eXBlIjoiYWNjZXNzIn0.0zxftxUINCzhlJEfy1CJZtoFbzlS0Fowm66F5JuM49E"
}
Enter fullscreen mode Exit fullscreen mode

If so, nice! Now, let's make some of our api routes protected. Protected routes are routes that require you to have an Authorization header (Yes, with a z, no matter where you live) to your request in order for it to go through. Let's add the decorator @jwt_required in our tweet routes.

@app.route("/api/tweets")
@jwt_required
def get_tweets():
    return jsonify(getTweets())

@app.route("/api/addtweet", methods=["POST"])
@jwt_required
def add_tweet():
    try:
        title = request.json["title"]
        content = request.json["content"]
        uid = request.json["uid"]
        addTweet(title, content, uid)
        return jsonify({"success": "true"})
    except Exception as e:
        print(e)
        return jsonify({"error": "Invalid form"})

@app.route("/api/deletetweet", methods=["DELETE"])
@jwt_required
def delete_tweet():
    try:
        tid = request.json["tid"]
        delTweet(tid)
        return jsonify({"success": "true"})
    except:
        return jsonify({"error": "Invalid form"})
Enter fullscreen mode Exit fullscreen mode

And now, when you try to get tweets, you get this error:

$ curl "http://localhost:5000/api/tweets"
{
  "msg": "Missing Authorization Header"
}
Enter fullscreen mode Exit fullscreen mode

To fix this, we add a -H attribute and set it to Bearer <YourToken>, so, for me, the new command is:

curl -H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE1OTIwNDE2NDgsIm5iZiI6MTU5MjA0MTY0OCwianRpIjoiMjNiZWViMTEtOWI4Mi00MDY3LWExODMtZDkyMzAyNDM4OGU2IiwiZXhwIjoxNTkyMDQyNTQ4LCJpZGVudGl0eSI6MiwiZnJlc2giOmZhbHNlLCJ0eXBlIjoiYWNjZXNzIn0.0zxftxUINCzhlJEfy1CJZtoFbzlS0Fowm66F5JuM49E" "http://localhost:5000/api/tweets"
Enter fullscreen mode Exit fullscreen mode

If you're using Insomnia or Postman, you need to add a Header with a name of Authorization and value of Bearer <JWT> to your request

And you should get a valid response. Awesome! I feel like we don't need to protect the GET route, so I won't. Anyway, here's what your code should look like:

from flask import Flask, request, jsonify
from flask_sqlalchemy import SQLAlchemy
from flask_cors import CORS
import re
from flask_jwt_extended import JWTManager, create_access_token, jwt_required, get_jwt_identity

app = Flask(__name__)
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///twitter.db"
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
app.config["JWT_SECRET_KEY"] = "myawesomesecretisnevergonnagiveyouup"
CORS(app)
JWTManager(app)

# DB
db = SQLAlchemy(app)
class User(db.Model):
    id = db.Column(db.Integer, primary_key = True) # primary_key makes it so that this value is unique and can be used to identify this record.
    username = db.Column(db.String(24))
    email = db.Column(db.String(64))
    pwd = db.Column(db.String(64))

    # Constructor
    def __init__(self, username, email, pwd):
        self.username = username
        self.email = email
        self.pwd = pwd

def getUsers():
    users = User.query.all()
    return [{"id": i.id, "username": i.username, "email": i.email, "password": i.pwd} for i in users]

def getUser(uid):
    users = User.query.all()
    user = list(filter(lambda x: x.id == uid, users))[0]
    return {"id": user.id, "username": user.username, "email": user.email, "password": user.pwd}

def addUser(username, email, pwd):
    try:
        user = User(username, email, pwd)
        db.session.add(user)
        db.session.commit()
        return True
    except Exception as e:
        print(e)
        return False

def removeUser(uid):
    try:
        user = User.query.get(uid)
        db.session.delete(user)
        db.session.commit()
        return True
    except Exception as e:
        print(e)
        return False

class Tweet(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    uid = db.Column(db.Integer, db.ForeignKey("user.id"))
    user = db.relationship('User', foreign_keys=uid)
    title = db.Column(db.String(256))
    content = db.Column(db.String(2048))

def getTweets():
    tweets = Tweet.query.all()
    return [{"id": i.id, "title": i.title, "content": i.content, "user": getUser(i.uid)} for i in tweets]

def getUserTweets(uid):
    tweets = Tweet.query.all()
    return [{"id": item.id, "userid": item.user_id, "title": item.title, "content": item.content} for item in filter(lambda i: i.user_id == uid, tweets)]

def addTweet(title, content, uid):
    try:
        user = list(filter(lambda i: i.id == uid, User.query.all()))[0]
        twt = Tweet(title=title, content=content, user=user)
        db.session.add(twt)
        db.session.commit()
        return True
    except Exception as e:
        print(e)
        return False

def delTweet(tid):
    try:
        tweet = Tweet.query.get(tid)
        db.session.delete(tweet)
        db.session.commit()
        return True
    except Exception as e:
        print(e)
        return False

# ROUTES
@app.route("/api/login", methods=["POST"])
def login():
    try:
        email = request.json["email"]
        password = request.json["pwd"]
        if (email and password):
            user = list(filter(lambda x: x["email"] == email and x["password"] == password, getUsers()))
            # Check if user exists
            if len(user) == 1:
                token = create_access_token(identity=user[0]["id"])
                return jsonify({"token": token})
            else:
                return jsonify({"error": "Invalid credentials"})
        else:
            return jsonify({"error": "Invalid form"})
    except Exception as e:
        print(e)
        return jsonify({"error": "Invalid form"})

@app.route("/api/register", methods=["POST"])
def register():
    try:
        email = request.json["email"]
        email = email.lower()
        password = request.json["pwd"]
        username = request.json["username"]
        # Check to see if user already exists
        users = getUsers()
        if(len(list(filter(lambda x: x["email"] == email, users))) == 1):
            return jsonify({"error": "Invalid form"})
        # Email validation check
        if not re.match(r"[\w\._]{5,}@\w{3,}.\w{2,4}", email):
            return jsonify({"error": "Invalid email"})
        addUser(username, email, password)
        return jsonify({"success": True})
    except Exception as e:
        print(e)
        return jsonify({"error": "Invalid form"})

@app.route("/api/tweets")
def get_tweets():
    return jsonify(getTweets())

@app.route("/api/addtweet", methods=["POST"])
@jwt_required
def add_tweet():
    try:
        title = request.json["title"]
        content = request.json["content"]
        uid = request.json["uid"]
        addTweet(title, content, uid)
        return jsonify({"success": "true"})
    except Exception as e:
        print(e)
        return jsonify({"error": "Invalid form"})

@app.route("/api/deletetweet", methods=["DELETE"])
@jwt_required
def delete_tweet():
    try:
        tid = request.json["tid"]
        delTweet(tid)
        return jsonify({"success": "true"})
    except:
        return jsonify({"error": "Invalid form"})


if __name__ == "__main__":
    app.run(debug=True)
Enter fullscreen mode Exit fullscreen mode

Now we're ready to connect it to the frontend!


Connecting frontend to backend

First, we have to make it so that the user can only see the main page if they log in, so, change the default page from MainPage to Home. Let's create a login.js file that will allow us to handle login events. What this file will do is that it will help us add JWT to the localstorage, check if JWT has expired, and log a person out.

// src/login.js
import Axios from "axios";

async function login(email, pwd) {
    const res =await Axios.post("http://localhost:5000/api/login", {email, pwd});
    const {data} = await res;
    if (data.error) {
        return data.error
    } else {
        localStorage.setItem("token", data.token);
        return true
    }
}

export {login};
Enter fullscreen mode Exit fullscreen mode

Now, we have to implement the login function in our Login.jsx

// src/components/Login.jsx
import React, { Component } from "react";
import axios from "axios";
import Alert from "./Alert";
import {login} from "../login";

class Login extends Component {
    state = { err: "" };

    login = (e) => {
        e.preventDefault();
        login(document.getElementById("email").value,
            document.getElementById("password").value).then(r => {
            if (r === true) {
                this.setState({login: true})
            } else {
                this.setState({err: r})
            }
        })
    };

    render() {
        return (
            <div className="w3-card-4" style={{ margin: "2rem" }}>
                <div className="w3-container w3-blue w3-center w3-xlarge">
                    LOGIN
                </div>
                <div className="w3-container">
                    {this.state.err.length > 0 && (
                        <Alert
                            message={`Check your form and try again! (${this.state.err})`}
                        />
                    )}
                    <form onSubmit={this.login}>
                        <p>
                            <label htmlFor="email">Email</label>
                            <input
                                type="email"
                                className="w3-input w3-border"
                                id="email"
                            />
                        </p>
                        <p>
                            <label htmlFor="password">Password</label>
                            <input
                                type="password"
                                className="w3-input w3-border"
                                id="password"
                            />
                        </p>
                        <p>
                            <button type="submit" className="w3-button w3-blue">
                                Login
                            </button>
                            {this.state.login && "You're logged in!"}
                        </p>
                    </form>
                </div>
            </div>
        );
    }
}

export default Login;
Enter fullscreen mode Exit fullscreen mode

Now, if we login, we can see the message You're logged in!. But, to check if JWT was added to our browser's local storage, let's open the console and type localStorage. If you see a token, success! But, there's still one thing missing - If the user is logged in, we need to show the tweets. If not, we need to show the home page.
Let's add a check function to our login.js:

// src/login.js
function check() {
    if (localStorage.getItem("token")) {
        return true;
    } else {
        return false;
    }
}

export {login, check};
Enter fullscreen mode Exit fullscreen mode

This is a very basic check. In the next part, we will add tokens that will expire and also upgrade our check to see if the token is valid or not.

We can now add this check functionality to our App.jsx

// src/components/App.jsx
<Route path="/" exact component={check() ? MainPage : Home} />
Enter fullscreen mode Exit fullscreen mode

Also, let's make the Login page redirect to the Home page and the register page redirect to our login page.

// src/components/Login.jsx
login = (e) => {
        e.preventDefault();
        login(document.getElementById("email").value,
            document.getElementById("password").value).then(r => {
            if (r === true) {
                window.location = "/"
            } else {
                this.setState({err: r})
            }
        })
    };
Enter fullscreen mode Exit fullscreen mode
// src/components/Register.jsx
register = (e) => {
        e.preventDefault();
        axios
            .post("http://localhost:5000/api/register", {
                email: document.getElementById("email").value,
                username: document.getElementById("username").value,
                pwd: document.getElementById("password").value,
            })
            .then((res) => {
                if (res.data.error) {
                    this.setState({ err: res.data.error });
                } else {
                    window.location = "/login"
                }
            });
    };
Enter fullscreen mode Exit fullscreen mode

Nice! Now, let's work on the tweets


Fetching tweets from our database

Since our MainPage.jsx is a class-component, we can add a function called componentDidMount() to our class. This function fires when the module renders. Let's make it fetch data from the database. Also, just before I forget, let's add this line anywhere above scripts to our package.json:

"proxy": "http://localhost:5000",
Enter fullscreen mode Exit fullscreen mode

So now, instead of writing http://localhost:5000 everytime in our API calls, we can only specify the path. This will be useful later when we deploy. So, find any Axios calls in the frontend and remove http://localhost:5000 from them. Eg:

// src/login.js
async function login(email, pwd) {
    const res =await Axios.post("/api/login", {email, pwd});
    const {data} = await res;
    if (data.error) {
        return data.error
    } else {
        localStorage.setItem("token", data.token);
        return true
    }
}
Enter fullscreen mode Exit fullscreen mode

NOTE: You need to restart your server to see the effect

Now, back to our MainPage.jsx

// src/components/MainPage.jsx
import React from "react";
import TweetItem from "./TweetItem";
import Axios from "axios";

class MainPage extends React.Component {
    state = {tweets: []}

    componentDidMount() {
        Axios.get("/api/tweets").then(res => {
            this.setState({tweets: res.data})
        });
    }

    render() {
        return (
            <React.Fragment>
                <div
                    className="w3-container w3-jumbo"
                    style={{ margin: "3rem", paddingLeft: "1rem" }}>
                    Tweets
                </div>
                <div className="w3-container">
                    {this.state.tweets.length === 0 ? <p className="w3-xlarge w3-opacity" style={{marginLeft: "2rem"}}>No tweets! Create one</p> : this.state.tweets.map((item, index) => {
                        return (
                            <TweetItem
                                title={item.title}
                                content={item.content}
                                key={index}
                            />
                        );
                    })}
                </div>
            </React.Fragment>
        );
    }
}

export default MainPage;
Enter fullscreen mode Exit fullscreen mode

If you have no tweets, you should see this.
Alt Text

Let's add a tweet:

 curl -X POST -H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE1OTIxMTc4NTAsIm5iZiI6MTU5MjExNzg1MCwianRpIjoiYmEzMzA1ZWItNjFlNS00ZWQ5LTg2MTgtN2JiMDRkNTAyZTBiIiwiZXhwIjoxNTkyMTE4NzUwLCJpZGVudGl0eSI6MiwiZnJlc2giOmZhbHNlLCJ0eXBlIjoiYWNjZXNzIn0.emhpKPeHYMS3Vk4hOZ_Y0R1herf7vygp9jpRUQnCIao" -H "Content-Type: application/json" -d '{"title": "abcd", "content": "<p>xyz</p>", "uid": 1}' http://localhost:5000/api/addtweet
Enter fullscreen mode Exit fullscreen mode

Now, let's refresh our page. And we see:
Alt Text
Awesome!


Improving the login system

Flask-JWT by default expires all login tokens in 15 minutes. We need to check for the expiration of these tokens and refresh them if they expire. Let's also add a logout functionality.

// src/login.js
import Axios from "axios";

async function login(email, pwd) {
    const res = await Axios.post("/api/login", {email, pwd});
    const {data} = await res;
    if (data.error) {
        return data.error
    } else {
        localStorage.setItem("token", data.token);
        localStorage.setItem("refreshToken", data.refreshToken);
        return true
    }
}

async function check() {
    const token = localStorage.getItem("token")
    try {
        const res = await Axios.post("/api/checkiftokenexpire", {}, {
            headers: {
                Authorization: "Bearer " + token
            }
        })
        const {data} = await res;
        return data.success
    } catch {
        console.log("p")
        const refresh_token = localStorage.getItem("refreshToken")
        if (!refresh_token) {
            localStorage.removeItem("token")
            return false;
        }
        Axios.post("/api/refreshtoken", {}, {
            headers: {
                Authorization: `Bearer ${refresh_token}`
            }
        }).then(res => {
            localStorage.setItem("token", res.data.token)
        })
        return true;
    }
}

function logout() {
    if (localStorage.getItem("token")) {
        const token = localStorage.getItem("token")
        Axios.post("/api/logout/access", {}, {
            headers: {
                Authorization: `Bearer ${token}`
            }
        }).then(res => {
            if (res.data.error) {
                console.error(res.data.error)
            } else {
                localStorage.removeItem("token")
            }
        })
    }
    if (localStorage.getItem("refreshToken")) {
        const refreshToken = localStorage.getItem("refreshToken")
        Axios.post("/api/logout/refresh", {}, {
            headers: {
                Authorization: `Bearer ${refreshToken}`
            }
        }).then(res => {
            if (res.data.error) {
                console.error(res.data.error)
            } else {
                localStorage.removeItem("refreshToken")
            }
        })
    }
    localStorage.clear();
    setTimeout(() => window.location = "/", 500)
}

export {login, check, logout};
Enter fullscreen mode Exit fullscreen mode
// src/components/App.jsx
import React from "react";
import Home from "./Home";
import Navbar from "./Navbar";
import Login from "./Login";
import Register from "./Register";
import { BrowserRouter as Router, Route, Switch } from "react-router-dom";
import MainPage from "./MainPage";
import {check} from "../login";
import Logout from "./Logout";

function App() {
    let [login, setLogin] = React.useState(false);

    check().then(r => setLogin(r))

    return (
        <React.Fragment>
            <Navbar />
            <Router>
                <Route path="/" exact>
                    {login ? <MainPage/> : <Home/>}
                </Route>
                <Route path="/login" exact component={Login} />
                <Route path="/register" exact component={Register} />
                <Route path="/logout" exact component={Logout} />
            </Router>
        </React.Fragment>
    );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

Let's create the logout component that we used in our app:

import React from "react";
import {logout} from "../login";

class Logout extends React.Component {
    componentDidMount() {
        logout()
    }

    render() {
        return (
            <div className="w3-container w3-xlarge">
                <p>Please wait, logging you out...</p>
            </div>
        )
    }
}

export default Logout;
Enter fullscreen mode Exit fullscreen mode
// src/components/Login.jsx
import React, {Component} from "react";
import axios from "axios";
import Alert from "./Alert";
import {login, check} from "../login";

class Login extends Component {
    state = {err: ""};

    componentDidMount() {
        check().then(r => {if (r) {
            window.location = "/"
        }})
    }

    login = (e) => {
        e.preventDefault();
        login(document.getElementById("email").value,
            document.getElementById("password").value).then(r => {
            if (r === true) {
                window.location = "/"
            } else {
                this.setState({err: r})
            }
        })
    };

    render() {
        return (
            <div className="w3-card-4" style={{margin: "2rem"}}>
                <div className="w3-container w3-blue w3-center w3-xlarge">
                    LOGIN
                </div>
                <div className="w3-container">
                    {this.state.err.length > 0 && (
                        <Alert
                            message={`Check your form and try again! (${this.state.err})`}
                        />
                    )}
                    <form onSubmit={this.login}>
                        <p>
                            <label htmlFor="email">Email</label>
                            <input
                                type="email"
                                className="w3-input w3-border"
                                id="email"
                            />
                        </p>
                        <p>
                            <label htmlFor="password">Password</label>
                            <input
                                type="password"
                                className="w3-input w3-border"
                                id="password"
                            />
                        </p>
                        <p>
                            <button type="submit" className="w3-button w3-blue">
                                Login
                            </button>
                        </p>
                    </form>
                </div>
            </div>
        );
    }
}

export default Login;
Enter fullscreen mode Exit fullscreen mode

And finally, app.py

from flask import Flask, request, jsonify
from flask_sqlalchemy import SQLAlchemy
from flask_cors import CORS
import re
from flask_jwt_extended import JWTManager, create_access_token, jwt_required, get_jwt_identity, \
    jwt_refresh_token_required, create_refresh_token, get_raw_jwt

app = Flask(__name__)
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///twitter.db"
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
db = SQLAlchemy(app)
app.config["JWT_SECRET_KEY"] = "myawesomesecretisnevergonnagiveyouup"
app.config["JWT_BLACKLIST_ENABLED"] = True
app.config["JWT_BLACKLIST_TOKEN_CHECKS"] = ["access", "refresh"]
jwt = JWTManager(app)
CORS(app)

# DB
class User(db.Model):
    id = db.Column(db.Integer,
                   primary_key=True)  # primary_key makes it so that this value is unique and can be used to identify this record.
    username = db.Column(db.String(24))
    email = db.Column(db.String(64))
    pwd = db.Column(db.String(64))

    # Constructor
    def __init__(self, username, email, pwd):
        self.username = username
        self.email = email
        self.pwd = pwd


def getUsers():
    users = User.query.all()
    return [{"id": i.id, "username": i.username, "email": i.email, "password": i.pwd} for i in users]


def getUser(uid):
    users = User.query.all()
    user = list(filter(lambda x: x.id == uid, users))[0]
    return {"id": user.id, "username": user.username, "email": user.email, "password": user.pwd}


def addUser(username, email, pwd):
    try:
        user = User(username, email, pwd)
        db.session.add(user)
        db.session.commit()
        return True
    except Exception as e:
        print(e)
        return False


def removeUser(uid):
    try:
        user = User.query.get(uid)
        db.session.delete(user)
        db.session.commit()
        return True
    except Exception as e:
        print(e)
        return False


class Tweet(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    uid = db.Column(db.Integer, db.ForeignKey("user.id"))
    user = db.relationship('User', foreign_keys=uid)
    title = db.Column(db.String(256))
    content = db.Column(db.String(2048))


def getTweets():
    tweets = Tweet.query.all()
    return [{"id": i.id, "title": i.title, "content": i.content, "user": getUser(i.uid)} for i in tweets]


def getUserTweets(uid):
    tweets = Tweet.query.all()
    return [{"id": item.id, "userid": item.user_id, "title": item.title, "content": item.content} for item in
            filter(lambda i: i.user_id == uid, tweets)]


def addTweet(title, content, uid):
    try:
        user = list(filter(lambda i: i.id == uid, User.query.all()))[0]
        twt = Tweet(title=title, content=content, user=user)
        db.session.add(twt)
        db.session.commit()
        return True
    except Exception as e:
        print(e)
        return False


def delTweet(tid):
    try:
        tweet = Tweet.query.get(tid)
        db.session.delete(tweet)
        db.session.commit()
        return True
    except Exception as e:
        print(e)
        return False


class InvalidToken(db.Model):
    __tablename__ = "invalid_tokens"
    id = db.Column(db.Integer, primary_key=True)
    jti = db.Column(db.String)

    def save(self):
        db.session.add(self)
        db.session.commit()

    @classmethod
    def is_invalid(cls, jti):
        q = cls.query.filter_by(jti=jti).first()
        return bool(q)


@jwt.token_in_blacklist_loader
def check_if_blacklisted_token(decrypted):
    jti = decrypted["jti"]
    return InvalidToken.is_invalid(jti)


# ROUTES
@app.route("/api/login", methods=["POST"])
def login():
    try:
        email = request.json["email"]
        password = request.json["pwd"]
        if email and password:
            user = list(filter(lambda x: x["email"] == email and x["password"] == password, getUsers()))
            # Check if user exists
            if len(user) == 1:
                token = create_access_token(identity=user[0]["id"])
                refresh_token = create_refresh_token(identity=user[0]["id"])
                return jsonify({"token": token, "refreshToken": refresh_token})
            else:
                return jsonify({"error": "Invalid credentials"})
        else:
            return jsonify({"error": "Invalid form"})
    except Exception as e:
        print(e)
        return jsonify({"error": "Invalid form"})


@app.route("/api/register", methods=["POST"])
def register():
    try:
        email = request.json["email"]
        email = email.lower()
        password = request.json["pwd"]
        username = request.json["username"]
        # Check to see if user already exists
        users = getUsers()
        if (len(list(filter(lambda x: x["email"] == email, users))) == 1):
            return jsonify({"error": "Invalid form"})
        # Email validation check
        if not re.match(r"[\w\._]{5,}@\w{3,}.\w{2,4}", email):
            return jsonify({"error": "Invalid email"})
        addUser(username, email, password)
        return jsonify({"success": True})
    except Exception as e:
        print(e)
        return jsonify({"error": "Invalid form"})


@app.route("/api/checkiftokenexpire", methods=["POST"])
@jwt_required
def check_if_token_expire():
    print(get_jwt_identity())
    return jsonify({"success": True})


@app.route("/api/refreshtoken", methods=["POST"])
@jwt_refresh_token_required
def refresh():
    identity = get_jwt_identity()
    token = create_access_token(identity=identity)
    return jsonify({"token": token})


@app.route("/api/logout/access", methods=["POST"])
@jwt_required
def access_logout():
    jti = get_raw_jwt()["jti"]
    try:
        invalid_token = InvalidToken(jti=jti)
        invalid_token.save()
        return jsonify({"success": True})
    except Exception as e:
        print(e)
        return {"error": e}


@app.route("/api/logout/refresh", methods=["POST"])
@jwt_required
def refresh_logout():
    jti = get_raw_jwt()["jti"]
    try:
        invalid_token = InvalidToken(jti=jti)
        invalid_token.save()
        return jsonify({"success": True})
    except Exception as e:
        print(e)
        return {"error": e}


@app.route("/api/tweets")
def get_tweets():
    return jsonify(getTweets())


@app.route("/api/addtweet", methods=["POST"])
@jwt_required
def add_tweet():
    try:
        title = request.json["title"]
        content = request.json["content"]
        uid = request.json["uid"]
        addTweet(title, content, uid)
        return jsonify({"success": "true"})
    except Exception as e:
        print(e)
        return jsonify({"error": "Invalid form"})


@app.route("/api/deletetweet", methods=["DELETE"])
@jwt_required
def delete_tweet():
    try:
        tid = request.json["tid"]
        delTweet(tid)
        return jsonify({"success": "true"})
    except:
        return jsonify({"error": "Invalid form"})


if __name__ == "__main__":
    app.run(debug=True)
Enter fullscreen mode Exit fullscreen mode

Whew! That should finish up the login work.


Allow users to create tweets

Now, let's allow users to create tweets. First, we need a form where users can enter their tweets. I choose to design a modal that will appear on the click of a button. You can choose to do the same or create a new page to make a tweet. For the modal, let's create a new component called AddTweet.jsx

// src/components/AddTweet.jsx
import React from "react";

function AddTweet() {
    return (<div className="w3-modal w3-animate-opacity" id="addTweet">
        <div className="w3-modal-content w3-card">
            <header className="w3-container w3-blue">
                <span className="w3-button w3-display-topright w3-hover-none w3-hover-text-white" onClick={() => {
                    document.getElementById("addTweet").style.display = "none"
                }}>X</span>
                <h2>Add tweet</h2>
            </header>
            <form className="w3-container">
                <div className="w3-section">
                    <label htmlFor="title">Title</label>
                    <input type="text" id="title" className="w3-input w3-border w3-margin-bottom"/>
                    <textarea cols="30" rows="10"/>
                </div>
            </form>
        </div>
    </div>)
}

export default AddTweet
Enter fullscreen mode Exit fullscreen mode

And let's add a button to MainPage.jsx to open this model

// src/components/MainPage.jsx
import AddTweet from "./AddTweet";

// ...

<div
                    className="w3-container w3-jumbo"
                    style={{ margin: "3rem", paddingLeft: "1rem" }}>
                    <h1>Tweets</h1>
                    <button className="w3-button w3-blue w3-large" onClick={() => {
                        document.getElementById("addTweet").style.display = "block"
                    }}>Add tweet</button>
                </div>
                <AddTweet />
/...
Enter fullscreen mode Exit fullscreen mode

And this is what our website should look like:
Alt Text

But what about that fancy pants WYSIWYG editor?

Well, first, we need one. There are many choices out there. There's TinyMCE, the one I recommend. It also has react support. But, if you don't like TinyMCE, there's Froala, used by companies like Amazon and IBM (they say). Also, there's Editor.js, CKEditor 4, (Quill)[https://quilljs.com/] and many more. You can just search for a WYSIWYG editor or use BBCode or Markdown, like this site.

I'm gonna use TinyMCE because it has React support.
First, head over to tiny.cloud and create an account (don't worry, TinyMCE is free for indivisuals!). Now, you should be in your dashboard. Now, we need to install @tinymce/tinymce-react in our frontend

npm i @tinymce/tinymce-react
Enter fullscreen mode Exit fullscreen mode

Now that TinyMCE is installed, let's use it in our website.

// src/components/AddTweet.jssx
import React from "react";
import {Editor} from "@tinymce/tinymce-react/lib/cjs/main/ts";

function AddTweet() {

    let [content, setContent] = React.useState("");

    return (<div className="w3-modal w3-animate-opacity" id="addTweet">
        <div className="w3-modal-content w3-card">
            <header className="w3-container w3-blue">
                <span className="w3-button w3-display-topright w3-hover-none w3-hover-text-white" onClick={() => {
                    document.getElementById("addTweet").style.display = "none"
                }}>X</span>
                <h2>Add tweet</h2>
            </header>
            <form className="w3-container">
                <div className="w3-section">
                    <p>
                        <label htmlFor="title">Title</label>
                        <input type="text" id="title" className="w3-input w3-border w3-margin-bottom"/>
                    </p>
                    <Editor
                        initialValue="<p>This is the initial content of the editor</p>"
                        init={{
                            height: 300,
                            menubar: false,
                            statusbar: false,
                            toolbar_mode: "sliding",
                            plugins: [
                                'advlist autolink lists link image imagetools media emoticons preview anchor',
                                'searchreplace visualblocks code fullscreen',
                                'insertdatetime media table paste code help wordcount'
                            ],
                            toolbar:
                                'undo redo | formatselect | bold italic underline strikethrough | image anchor media | \
                                alignleft aligncenter alignright alignjustify | \
                                outdent indent | bulllist numlist | fullscreen preview | emoticons help',
                            contextmenu: "bold italic underline indent outdent help"
                        }}
                    />
                    <p>
                        <button type="submit" className="w3-button w3-blue">Post</button>
                    </p>
                </div>
            </form>
        </div>
    </div>)
}

export default AddTweet
Enter fullscreen mode Exit fullscreen mode

And here's what our website should look like:
Alt Text

Ahh, much better. But what about that little warning up there? To fix that, we need to add our apikey to our Editor. Open your TinyMCE Dashboard and copy your api key. Then, add this line as a prop to your editor:

apiKey: 'your-api-key'
Enter fullscreen mode Exit fullscreen mode

This should now supress the warnings. If not, check out your Approved domains

Now we need to add the functionality of posting. First, let's make a change to the addtweets route in app.py.

@app.route("/api/addtweet", methods=["POST"])
@jwt_required
def add_tweet():
    try:
        title = request.json["title"]
        content = request.json["content"]
        uid = get_jwt_identity() # The line that changed
        addTweet(title, content, uid)
        return jsonify({"success": "true"})
    except Exception as e:
        print(e)
        return jsonify({"error": "Invalid form"})
Enter fullscreen mode Exit fullscreen mode

Instead of giving the uid in the post request, we can get it from the JWT.

Now, let's get the content from the TinyMCE editor and post it to our database. (Also, I decided to convert AddTweet to a class component.

// src/components/AddTweet.jsx
import React from "react";
import {Editor} from "@tinymce/tinymce-react/lib/cjs/main/ts";
import Axios from "axios";

class AddTweet extends React.Component {
    state = {content: ""}

    handleEditorChange = (content, editor) => {
        console.log(content)
        this.setState({content})
    }

    submitForm = (e) => {
        e.preventDefault()
        Axios.post("/api/addtweet", {
            title: document.getElementById("title").value,
            content: this.state.content
        }, {
            headers: {
                Authorization: "Bearer " + localStorage.getItem("token")
            }
        }).then(res => {
            if (res.data.success) {
                window.location.reload()
            }
        })
    }

    render() {
        return (<div className="w3-modal w3-animate-opacity" id="addTweet">
            <div className="w3-modal-content w3-card">
                <header className="w3-container w3-blue">
                <span className="w3-button w3-display-topright w3-hover-none w3-hover-text-white" onClick={() => {
                    document.getElementById("addTweet").style.display = "none"
                }}>X</span>
                    <h2>Add tweet</h2>
                </header>
                <form className="w3-container" onSubmit={this.submitForm}>
                    <div className="w3-section">
                        <p>
                            <label htmlFor="title">Title</label>
                            <input type="text" id="title" className="w3-input w3-border w3-margin-bottom"/>
                        </p>
                        <Editor
                            initialValue="<p>This is the initial content of the editor</p>"
                            init={{
                                height: 300,
                                menubar: false,
                                statusbar: false,
                                toolbar_mode: "sliding",
                                plugins: [
                                    'advlist autolink lists link image imagetools media emoticons preview anchor',
                                    'searchreplace visualblocks code fullscreen',
                                    'insertdatetime media table paste code help wordcount'
                                ],
                                toolbar:
                                    'undo redo | formatselect | bold italic underline strikethrough | image anchor media | \
                                    alignleft aligncenter alignright alignjustify | \
                                    outdent indent | bulllist numlist | fullscreen preview | emoticons help',
                                contextmenu: "bold italic underline indent outdent help"
                            }}
                            onEditorChange={this.handleEditorChange}
                        />
                        <p>
                            <button type="submit" className="w3-button w3-blue">Post</button>
                        </p>
                    </div>
                </form>
            </div>
        </div>)
    }
}

export default AddTweet
Enter fullscreen mode Exit fullscreen mode

And now, when we post the tweet, Hurrah! The tweet appears. But, there is a problem. New tweets appear at the bottom. The solution is very simple! We can simply reverse the array in MainPage.jsx. Just change componentDidMount to this:

componentDidMount() {
        Axios.get("/api/tweets").then(res => {
            this.setState({tweets: res.data.reverse()})
        });
    }
Enter fullscreen mode Exit fullscreen mode

And we're done boys!

But wait! What about deleting and updating tweets and users?
No time for that in this part. I'll cover that in the next one.


Anyway this has been Part 2. Cya! And of course, the code is available on Github

UPDATE: Part 3 is now out

Top comments (3)

Collapse
 
arnu515 profile image
arnu515
Collapse
 
jujue profile image
juju-e

Thanks man you're a saver

Collapse
 
arnu515 profile image
arnu515

You're welcome!