Build a twitter clone with Flask and React | PART 2
Tags: tutorial, react, webdev, python
Published: 2020-06-14 17:08:14

**IF YOU HAVEN'T READ PART ONE YET, [CLICK HERE](https://dev.to/arnu515/build-a-fullstack-twitter-clone-using-flask-and-react-1j72)**
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`.
```javascript
// 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;

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

```javascript
// 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;
```

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:
```javascript
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;
```
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](https://dev-to-uploads.s3.amazonaws.com/i/zl9egd26x9airol3033b.png)

---

## 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`:

```python
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))
```
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.

```python

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
```

I also made a few changes to the `User` class.
```python
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
```
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:
```shell
python -i app.py
```
and press `^C` to terminate it. You should be in the python console now. Type:
```python
import app
app.db.create_all()
```
And this should create `twitter.db`.

Now, let's add a route for adding a tweet and getting all tweets.
```python
@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"})
```
Finally, let's test it. Make sure you already have a registered user. Type this command:
```shell
curl -X POST -H "Content-Type: application/json" -d '{"title": "a", "content": "e", "uid": 1}' "http://localhost:5000/api/addtweet"
```
If all is good, you should get `{"success": true}` as an output.
Now, let's list the tweets:
```shell
curl "http://localhost:5000/api/tweets" 
```
If your output looks similar to this, you're good!
```shell
[
  {
    "content": "e", 
    "id": 1, 
    "title": "a", 
    "user": {
      "email": "sasdasd@asdasd.sads", 
      "id": 1, 
      "password": "as", 
      "username": "df"
    }
  }
]
```

Let's also add a delete route, so that we can delete tweets.
```python
@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"})
```
Of course, we have to test it!
```shell
curl -X DELETE -H "Content-Type: application/json" -d '{"tid": 1}' "http://localhost:5000/api/deletetweet"
```
```shell
curl "http://localhost:5000/api/tweets"
# OUTPUT: []
```

---

## 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 **J**son **W**eb **T**oken. It allows us to verify each user if they've logged in. You can read more about it [here](jwt.org) To add `JWT` to your application, you need to install `flask-jwt-extended`:
```shell
pip install flask-jwt-extended
```
*We're using the extended version because it is easier to use.*

Import JWT
```python
from flask_jwt_extended import JWTManager, create_access_token, jwt_required, get_jwt_identity
```

Now, change your `Login` route to return a json web token instead of `true`.
```python
@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"})
```

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`.
```python
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)
```
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.

```shell
curl -X POST -H "Content-Type: application/json" -d '{"email": "email@domain.com", "pwd": "password"}' "http://localhost:5000/api/login"
```
*Replace the data with whatever you've registered with*
And this should be your output:
```
{
  "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE1OTIwNDE2NDgsIm5iZiI6MTU5MjA0MTY0OCwianRpIjoiMjNiZWViMTEtOWI4Mi00MDY3LWExODMtZDkyMzAyNDM4OGU2IiwiZXhwIjoxNTkyMDQyNTQ4LCJpZGVudGl0eSI6MiwiZnJlc2giOmZhbHNlLCJ0eXBlIjoiYWNjZXNzIn0.0zxftxUINCzhlJEfy1CJZtoFbzlS0Fowm66F5JuM49E"
}
```
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.
```python
@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"})
```
And now, when you try to get tweets, you get this error:
```shell
$ curl "http://localhost:5000/api/tweets"
{
  "msg": "Missing Authorization Header"
}
```
To fix this, we add a `-H` attribute and set it to `Bearer <YourToken>`, so, for me, the new command is:
```shell
curl -H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE1OTIwNDE2NDgsIm5iZiI6MTU5MjA0MTY0OCwianRpIjoiMjNiZWViMTEtOWI4Mi00MDY3LWExODMtZDkyMzAyNDM4OGU2IiwiZXhwIjoxNTkyMDQyNTQ4LCJpZGVudGl0eSI6MiwiZnJlc2giOmZhbHNlLCJ0eXBlIjoiYWNjZXNzIn0.0zxftxUINCzhlJEfy1CJZtoFbzlS0Fowm66F5JuM49E" "http://localhost:5000/api/tweets"
```
*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:
```python
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)
```

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.

```javascript
// 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};
```
Now, we have to implement the `login` function in our `Login.jsx`
```javascript
// 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;
```

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`:
```javascript
// src/login.js
function check() {
    if (localStorage.getItem("token")) {
        return true;
    } else {
        return false;
    }
}

export {login, check};
```
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`
```javascript
// src/components/App.jsx
<Route path="/" exact component={check() ? MainPage : Home} />
```
Also, let's make the Login page redirect to the Home page and the register page redirect to our login page.
```javascript
// 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})
            }
        })
    };
```
```javascript
// 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"
                }
            });
    };
```

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`:
```json
"proxy": "http://localhost:5000",
```
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:
```javascript
// 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
    }
}
```
*NOTE: You need to restart your server to see the effect*

Now, back to our `MainPage.jsx`
```javascript
// 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;
```
If you have no tweets, you should see this.
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/ev7v0jb0lzch56t73ows.png)

Let's add a tweet:
```shell
 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
```

Now, let's refresh our page. And we see:
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/4180ow7sv3qh8r0enlqh.png)
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.

```javascript
// 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};
```

```javascript
// 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;
```

Let's create the logout component that we used in our app:
```javascript
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;
```

```javascript
// 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;
```

And finally, `app.py`
```python
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)
```

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`

```javascript
// 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
```
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 />
/...
```
And this is what our website should look like:
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/rixgcmfofqtzxwjsiqjh.png)

> *But what about that fancy pants WYSIWYG editor?*

Well, first, we need one. There are many choices out there. There's [TinyMCE](https://tiny.cloud), the one I recommend. It also has react support. But, if you don't like TinyMCE, there's [Froala](https://froala.com), used by companies like Amazon and IBM (they say). Also, there's [Editor.js](https://editorjs.io), [CKEditor 4](https://ckeditor.com/ckeditor-4/), (Quill)[https://quilljs.com/] and many more. You can just [search for a WYSIWYG editor](https://duckduckgo.com/?q=wysiwyg+editor&t=hk&ia=web&iai=r1-1&page=1&adx=slta&sexp=%7B%22v7exp%22%3A%22a%22%2C%22sltexp%22%3A%22a%22%7D) or use BBCode or Markdown, like this site.

I'm gonna use TinyMCE because it has React support.
First, head over to [tiny.cloud](https://tiny.cloud) and create an account (don't worry, TinyMCE is free for indivisuals!). Now, you should be in your [dashboard](https://www.tiny.cloud/my-account/dashboard/). Now, we need to install `@tinymce/tinymce-react` in our frontend
```shell
npm i @tinymce/tinymce-react
```
Now that TinyMCE is installed, let's use it in our website.
```javascript
// 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
```
And here's what our website should look like:
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/mbtx6j2btecr40y60cfj.png)

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](https://www.tiny.cloud/my-account/dashboard/) and copy your api key. Then, add this line as a prop to your editor:
```javascript
apiKey: 'your-api-key'
```
This should now supress the warnings. If not, check out your [Approved domains](https://www.tiny.cloud/my-account/domains/)

Now we need to add the functionality of posting. First, let's make a change to the `addtweets` route in `app.py`.
```python
@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"})
```
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.
```javascript
// 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
```
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:

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

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](https://github.com/arnu515/twitter-clone)

**[UPDATE: Part 3 is now out](https://dev.to/arnu515/build-a-twitter-clone-with-flask-and-react-part-3-4h02)**