Build a twitter clone with Flask and React | PART 3
Tags: tutorial, react, webdev, python
Published: 2020-06-16 15:03:09

**IF YOU HAVEN'T READ PART ONE YET, [CLICK HERE](https://dev.to/arnu515/build-a-fullstack-twitter-clone-using-flask-and-react-1j72)**

**IF YOU HAVEN'T READ PART TWO YET, [CLICK HERE](https://dev.to/arnu515/build-a-twitter-clone-with-flask-and-react-part-2-5deb)***

## Deleting Tweets
Now, let's add the ability to delete tweets. Next to every tweet on the right, I want to add a delete button. Also, let's show the author of every tweet too:
```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" }}>
            <header className="w3-container w3-opacity w3-light-gray" style={{padding: "1rem"}}>@{props.author}</header>
            <div className="w3-container" style={{ padding: "2rem" }}>
                <h2 className="w3-xxlarge">
                    <span className="w3-opacity">{props.title}</span>
                    <button className="w3-right w3-button w3-red w3-large w3-hover-pale-red w3-round-large">Delete</button></h2>
                <div dangerouslySetInnerHTML={{__html: props.content}}/>
            </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;
```
Much better
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/jb8kntr3d5tjdryob327.png)

One user cannot delete another's post right? So, we have to check if the current logged in user is the owner.

But first, we need to add an api route to get the current user in `app.py`:
```python
@app.route("/api/getcurrentuser")
@jwt_required
def get_current_user():
    uid = get_jwt_identity()
    return jsonify(getUser(uid))
```

And now, let's allow only the author to delete their post. Add this to `TweetItem.jsx` where the delete button was:
```javascript
// ...
{props.isOwner &&
                    <button className="w3-right w3-button w3-red w3-large w3-hover-pale-red w3-round-large">Delete
                    </button>}
// ...
```

Then, let's update `MainPage.jsx`
```javascript
// src/components/MainPage.jsx
import React from "react";
import TweetItem from "./TweetItem";
import Axios from "axios";
import AddTweet from "./AddTweet";

class MainPage extends React.Component {
    state = {tweets: [], currentUser: {username: ""}}

    componentDidMount() {
        Axios.get("/api/tweets").then(res => {
            this.setState({tweets: res.data.reverse()})
        });
        setTimeout(() => {
            Axios.get("/api/getcurrentuser", {
                headers: {
                    Authorization: `Bearer ${localStorage.getItem("token")}`
                }
            }).then(res => {
                this.setState({currentUser: res.data})
            })
        }, 500)
    }

    render() {
        return (
            <React.Fragment>
                <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/>
                <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
                                    id={item.id}
                                    title={item.title}
                                    content={item.content}
                                    author={item.user.username}
                                    isOwner={this.state.currentUser.username === item.user.username}
                                    key={index}
                                />
                            );
                        })}
                </div>
            </React.Fragment>
        );
    }
}

export default MainPage;
```

Now, if I sign in with another user:
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/h1yzcxoageffmhxm2uzg.png)
I cannot see the delete button! Nice!

Now, let's add the functionality:
```javascript
// src/components/TweetItem.jsx
import React from "react";
import Axios from "axios";

function deleteTweet(tid) {
    Axios.delete("/api/deletetweet/" + tid, {headers: { Authorization: "Bearer " +localStorage.getItem("token") }}).then(res => {
        console.log(res.data)
        window.location.reload();
    })
}

function TweetItem(props) {

    return (
        <div
            className="w3-card w3-border w3-border-gray w3-round-large"
            style={{marginTop: "2rem"}}>
            <header className="w3-container w3-opacity w3-light-gray" style={{padding: "1rem"}}>@{props.author}</header>
            <div className="w3-container" style={{padding: "2rem"}}>
                <h2 className="w3-xxlarge">
                    <span className="w3-opacity">{props.title}</span>
                    {props.isOwner &&
                    <button className="w3-right w3-button w3-red w3-large w3-hover-pale-red w3-round-large" onClick={() => deleteTweet(props.id)}>Delete
                    </button>}
                </h2>
                <div dangerouslySetInnerHTML={{__html: props.content}}/>
            </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;
```

Also, I made this small change in `app.py`:
```python
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
```

---

## Some more form validation

If you noticed in that image, I'll put it here for you:
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/jb8kntr3d5tjdryob327.png)

There is a post with no title and no content! Let's add some client side form validation:
```javascript
// src/components/AddTweet.jsx
import React from "react";
import {Editor} from "@tinymce/tinymce-react/lib/cjs/main/ts";
import Axios from "axios";
import Alert from "./Alert";

class AddTweet extends React.Component {
    state = {content: "<p>I have to edit this!</p>", titleErr: "", contentErr: "", formErr: ""}

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

    submitForm = (e) => {
        e.preventDefault()
        if (this.state.content.length === 0) {
            this.setState(
                {contentErr: "Add some data to the content!"}
            )
            return;
        }
        if (document.getElementById("title").value.length === 0) {
            this.setState(
                {titleErr: "Add a title!"}
            )
            return;
        }
        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()
            } else {
                this.setState(
                        {formErr: res.data.error }
                    )
            }
        })
    }

    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}>
                    {this.state.formErr.length > 0 && <Alert message={this.state.formErr}/>}
                    <div className="w3-section">
                        <p>
                            <label htmlFor="title">Title</label>
                            <input type="text" id="title" className="w3-input w3-border w3-margin-bottom"/>
                            <small className="w3-text-gray">{this.state.titleErr}</small>
                        </p>
                        <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}
                            />
                            <small className="w3-text-gray">{this.state.contentErr}</small>
                        </p>

                        <p>
                            <button type="submit" className="w3-button w3-blue">Post</button>
                        </p>
                    </div>
                </form>
            </div>
        </div>)
    }
}

export default AddTweet
```
And now, for the server:
```python
# 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)
    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"]
        if not (email and password and username):
            return jsonify({"error": "Invalid form"})
        # 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():
    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"]
        if not (title and content):
            return jsonify({"error": "Invalid form"})
        uid = get_jwt_identity()
        addTweet(title, content, uid)
        return jsonify({"success": "true"})
    except Exception as e:
        print(e)
        return jsonify({"error": "Invalid form"})


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


@app.route("/api/getcurrentuser")
@jwt_required
def get_current_user():
    uid = get_jwt_identity()
    return jsonify(getUser(uid))


if __name__ == "__main__":
    app.run(debug=True)
```

---

## Adding User settings
Now, let's allow the user to change some settings. Let's create a new component called `UserSettings.jsx`
```javascript
// src/components/UserSettings.jsx
import React from 'react';
import Alert from "./Alert";
import Axios from "axios";

class UserSettings extends React.Component {

    state = {currentSetting: "main", err: ""} //values: main, cpwd, del

    componentDidMount() {
        if (!localStorage.getItem("token")) {
            window.location = "/login"
        }
    }

    changePassword = (e) => {
        e.preventDefault();
        Axios.post("/api/changepassword", {
            password: document.getElementById("password").value,
            npassword: document.getElementById("npassword").value
        }, {
            headers: {
                Authorization: "Bearer " + localStorage.getItem("token")
            }
        })
            .then(res => {
                if (res.data.error) {
                    this.setState(
                        {err: res.data.error}
                    )
                } else {
                    alert("Password changed! Logging you out...")
                    window.location = "/logout"
                }
            })
    }

    deleteAccount = (e) => {
        e.preventDefault();
        let x = window.confirm("Are you sure you want to delete your account? THIS CANNOT BE UNDONE. ALL OF YOUR POSTS WILL BE DELETED")
        if (x) {
            Axios.delete("/api/deleteaccount", {headers: {Authorization: "Bearer " + localStorage.getItem("token")}})
                .then(res => {
                    if (res.data.error) {
                        alert("An error occurred: " + res.data.error)
                    } else {
                        alert("Your account has been deleted. We're sad to see you go :(. Now, anyone can sign up with your username. Logging you out...")
                        window.location = "/logout"
                    }
                })
        }
    }

    render() {
        return (<div className="w3-container" style={{margin: "3rem"}}>
            <div className="w3-card w3-border w3-round-large">
                <header className="w3-container w3-xlarge w3-blue"
                        style={{padding: "0.5rem", paddingLeft: "3rem"}}>Settings
                </header>
                <div className="w3-container">
                    {this.state.err.length > 0 && <Alert message={this.state.err}/>}
                    {this.state.currentSetting === "main" && <div style={{margin: "1rem"}}>
                        <h1 className="w3-xxlarge">Settings</h1>
                        <hr className="w3-border-top w3-border-black"/>
                        <p>Choose a setting from below:</p>
                        <ul className="w3-ul w3-border w3-hoverable">
                            <li onClick={() => this.setState({currentSetting: "cpwd"})} style={{cursor: "pointer"}}
                                className="w3-hover-light-gray">Change password
                            </li>
                            <li onClick={() => this.setState({currentSetting: "del"})} style={{cursor: "pointer"}}
                                className="w3-text-red w3-hover-pale-red w3-hover-text-red">Delete account
                            </li>
                        </ul>
                    </div>}
                    {this.state.currentSetting === "cpwd" && <div style={{margin: "1rem"}}>
                        <h1 className="w3-xxlarge">Change password</h1>
                        <hr className="w3-border-top w3-border-black"/>
                        <button className="w3-button w3-blue"
                                onClick={() => this.setState({currentSetting: "main"})}>&laquo; Back
                        </button>
                        <form onSubmit={this.changePassword}>
                            <p>
                                <label htmlFor="password">Old password</label>
                                <input type="password" id="password" className="w3-input w3-border"/>
                            </p>
                            <p>
                                <label htmlFor="npassword">New password</label>
                                <input type="password" id="npassword" className="w3-input w3-border"/>
                            </p>
                            <p>
                                <button type="submit" className="w3-button w3-blue">Submit</button>
                            </p>
                        </form>
                    </div>}
                    {this.state.currentSetting == "del" && <div style={{margin: "1rem"}}>
                        <h1 className="w3-xxlarge w3-text-red">Delete account</h1>
                        <hr className="w3-border-top w3-border-black"/>
                        <button className="w3-button w3-blue"
                                onClick={() => this.setState({currentSetting: "main"})}>&laquo; Back
                        </button>
                        <p>

                        <button className="w3-button w3-red w3-large" onClick={this.deleteAccount}>DELETE ACCOUNT</button>
                        </p>
                    </div>}
                </div>
            </div>
        </div>)
    }
}

export default UserSettings;
```

Now, let's add the routes:
```python
# app.py
# ...
@app.route("/api/changepassword", methods=["POST"])
@jwt_required
def change_password():
    try:
        user = User.query.get(get_jwt_identity())
        if not (request.json["password"] and request.json["npassword"]):
            return jsonify({"error": "Invalid form"})
        if not user.pwd == request.json["password"]:
            return jsonify({"error": "Wrong password"})
        user.pwd = request.json["npassword"]
        db.session.add(user)
        db.session.commit()
        return jsonify({"success": True})
    except Exception as e:
        print(e)
        return jsonify({"error": "Invalid form"})


@app.route("/api/deleteaccount", methods=["DELETE"])
@jwt_required
def delete_account():
    try:
        user = User.query.get(get_jwt_identity())
        tweets = Tweet.query.all()
        for tweet in tweets:
            if tweet.user.username == user.username:
                delTweet(tweet.id)
        removeUser(user.id)
        return jsonify({"success": True})
    except Exception as e:
        return jsonify({"error": str(e)})
#...
```
Add this route to `App.jsx`
```javascript
<Route path="/settings" exact component={UserSettings} />
// Don't forget to import it
```

And finally, a user can change their passwords or delete their account. And if they delete their account, all tweets they've made will be deleted.

---

## Better navbar

Notice how the navbar says `Login` and `Register` even when you're logged in? Let's fix that:
```javascript
// src/components/Navbar.jsx
import React from "react";

function Navbar() {
    let x = localStorage.getItem("token")
    let a = {name: x ? "Settings" : "Login", link: x ? "/settings" : "/login"}
    let b = {name: x ? "Logout" : "Register", link: x ? "/logout" : "/register"}

    return (
        <div className="w3-bar w3-black">
            <a className="w3-bar-item w3-button" href="/">
                Quickr
            </a>
            <div style={{ float: "right" }}>
                <a className="w3-bar-item w3-button" href={a.link}>
                    {a.name}
                </a>
                <a className="w3-bar-item w3-button" href={b.link}>
                    {b.name}
                </a>
            </div>
        </div>
    );
}

export default Navbar;
```
Much better!

---

## 404 Route
If a user enters a wrong route like `http://localhost:3000/like-this-post`, then, he'll only see a navbar and will be puzzled.
So, let's fix this
```javascript
// src/components/NotFound.jsx
import React from "react";

function NotFount() {
    return (<div className="w3-container w3-center" style={{margin: "3rem"}}>
        <h1 className="w3-jumbo">404</h1>
        <p className="w3-xxlarge">The page you were searching for was not found. Double check your URL and try again</p>
        <button type="button" className="w3-button w3-blue" onClick={() => window.location = "/"}>&laquo; Back</button>
    </div>)
}
```
And then, we'll add a general route to `App.jsx`
```javascript
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";
import UserSettings from "./UserSettings";
import NotFound from "./NotFound"

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

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

    return (
        <React.Fragment>
            <Navbar />
            <Router>
                <Switch>
                    <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}/>
                    <Route path="/settings" exact component={UserSettings}/>
                    <Route component={NotFound} />
                </Switch>
            </Router>
        </React.Fragment>
    );
}

export default App;
```
---

## Security
Let's say this website grows big. A hacker notices this website and **gets access to your database**. Now, **THE PASSWORDS OF ALL  USERS** are **compromised**. And, knowing humans, they've definitely used that password on **another**, **important** website, like their **bank account**. And suddenly, BOOM, bank accounts were compromised, Google accounts were compromised, everyone's data is everywhere, all because your site did not encrypt the passwords.

Nobody wants to be the cause of a CyberWar, right? So, let's fix that. First, we need to install something to *hash* the passwords. [bcrypt](https://pypi.org/project/bcrypt/) is recommended and used by many because of its advanced features. So, let's install it in our backend. (It might already be installed, when you installed flask):
```shell
pip install bcrypt
```
If you're on `Debian` or any other system that uses `apt-get`, you need to install:
```shell
sudo apt-get install build-essential libffi-dev python-dev
```
For `RHEL` or `Fedora`:
```shell
sudo yum install gcc libffi-devel python-devel
```

*What is hashing, or encrypting*, you ask? Well, let's say you have a chest. Inside this chest, you have treasure, *your password*. What we are doing, is we're storing all of these **valuable** chests in a bunker. So, if someone breaks into it, and trust me, [they happen](https://haveibeenpwned.com/PwnedWebsites). Even **big companies** like [Adobe](https://haveibeenpwned.com/PwnedWebsites#Adobe) were hacked. Fortunately, the passwords were encrypted.

So, what *is* encrypting? Encrypting is where you put *a lock* on the chest, and only *you* have the key. So, nobody can unlock it, *except you*. There are ways over this though, your key could be *compromised*, and also, you may be using a *weak lock*, like [Adobe](https://haveibeenpwned.com/PwnedWebsites#Adobe), and people just **broke through** your lock.

Now, about *hashing*. The major difference between *encrypting* and *hasing* is that hashing is **irreversibe**. Meaning, once something is *hashed*, it cannot be *de-hashed*. This is good, and also trustworthy, since **no one except for the user** will know his/her password. We'll be using `bcrypt`, a **hashing algorithm**, but `bcrypt` does more than that.

**Salting**. Salting is where you make the hash more complicated. `bcrypt` can do that, and that's why it is preferred by many. You can read more about it [here](http://bcrypt.sourceforge.net/)

Enough nerdy stuff. Let's get back to programming *which is also nerdy, am I right? :P*

I'm gonna create a `security.py` file, which will handle encryption and decryption for us:
```python
# security.py
import bcrypt


def encpwd(pwd):
    return bcrypt.hashpw(pwd.encode(), bcrypt.gensalt()).decode()


def checkpwd(x, y):
    return bcrypt.checkpw(x.encode(), y.encode())
```
This is a very basic file, that I will expand in the future *(no spoilers)*! Also, in `bcrypt.gensalt()`, you can add a parameter in the function for the number of rounds. More rounds = more security and also, more time. On my MacBook Air, it took me 0.5sec to generate a hash with 10 rounds(default) and 85 sec with 20! So, I chose 12, which is default. But, if you have a stronger GPU, then you can go higher.

But, there is one problem with bcrypt, it cannot handle passwords above 72 characters. So, what do we do about the people who sleep on their keyboard when typing a password? We, have to **HASH IT EVEN MORE!**
```python
# security.py
import bcrypt
import base64
import hashlib


def encpwd(pwd):
    return bcrypt.hashpw(base64.b64encode(hashlib.sha256(pwd.encode()).digest()), bcrypt.gensalt()).decode()


def checkpwd(x, y):
    return bcrypt.checkpw(base64.b64encode(hashlib.sha256(x.encode()).digest()), y.encode())
```
Now, we can test it using python interactive shell:
```python
$ python
>>> import security
>>> pwd = "password" # a bad password
>>> security.encpwd(pwd)
'$2b$12$68F4aKicE.xpXhajKKtZJOk3fQEeU3izEkOlF0S9OI8Q1XZCbwMxm'
>>> # Woah nobody can guess that is "password"!
>>> pwd = "asdsandkasjndjkasndjksadjaksdkas" * 500
>>> len(pwd) # Very long > 72
16000
>>> security.encpwd(pwd)
'$2b$12$vjKs5EXYaALIUVCw396k0ufh2I21zlsEiRkskRD0YHWP8bC3Vj9ZK'
>>> # It works fine!
```

And now, you can face the world, not worrying about any hackers - wait a second, who's gonna implement this? Oh, yes, that:
```python
# app.py
# ...
@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 security.checkpwd(password, x["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 = security.encpwd(request.json["pwd"])
        username = request.json["username"]
        if not (email and password and username):
            return jsonify({"error": "Invalid form"})
        # 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"})
#...
```
And now, to test, we have to delete and recreate our database
```shell
$ rm twitter.db
$ python
>>> import app
>>> app.db.create_all()
```
And it works! Your database is now secure, but not *fully* secure. Yes, we can't *just* encrypt the password, can we? WE GOTTA ENCRYPT EM ALL! Or, just the email and username. But, we need reversible encryption for these, so hashing won't do. But, we have a module named `cryptography` which has an encryption tool called `Fernet`. As I explained before, we need a key, so let's generate one.
```python
$ python
>>> from cryptography.fernet import Fernet
>>> Fernet.generate_key().decode()
'key'
```
Of course, you'd get an actual key. Just pick one that looks secure and add it to our file.. - **WOAH WAIT**, we can't just add a key to our file, what if we wanted to post it on github, or what if someone gets access to our file, we're goners! Hence, we need to use something called `environment variables`. These are special variables that are tied to your terminal, and you can set them like this:
```shell
export VARNAME=value
```
and yes, they are generally written in CAPITAL LETTERS.
*Note: Windows users have to use `set` instead of `export`.*
And now, we can set an environment variable, `KEY`:
```shell
export KEY=mykey
```
*It doesn't need quotes. Just type it in*

Now, whenever we restart the terminal, we need to set this variable. Annoying, right? We can put it in a file called `.env` like this:
```
KEY=mykey
```
and then, using a package called `python-dotenv`, we can automatically set those variables when we run our app.
```shell
pip install python-dotenv
```
Add this to the top of `app.py`:
```python
import dotenv
dotenv.load_dotenv()
```
And thats it!

> But what about GitHub, won't the `.env` file go to GitHub?

We can add it to the `.gitignore` file:
```
backend/.env
```
and it won't be committed!
Finally, we can add the email encryption:
```python
# security.py
import bcrypt
import base64
import hashlib
import os
from cryptography.fernet import Fernet

e = Fernet(os.getenv("KEY"))


def encpwd(pwd):
    return bcrypt.hashpw(base64.b64encode(hashlib.sha256(pwd.encode()).digest()), bcrypt.gensalt()).decode()


def checkpwd(x, y):
    return bcrypt.checkpw(base64.b64encode(hashlib.sha256(x.encode()).digest()), y.encode())


def enc(txt: str) -> str:
    return e.encrypt(txt.encode()).decode()


def dec(txt: str) -> str:
    return e.decrypt(txt.encode()).decode()
```
And then, implement that in our `app.py`
```python
# app.py
# ...
@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: security.dec(x["email"]) == email and security.checkpwd(password, x["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 = security.encpwd(request.json["pwd"])
        username = request.json["username"]
        print(email, password, request.json["pwd"], username)
        if not (email and password and username):
            return jsonify({"error": "Invalid form"})
        # Check to see if user already exists
        users = getUsers()
        if len(list(filter(lambda x: security.dec(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, security.enc(email), password)
        return jsonify({"success": True})
    except Exception as e:
        print(e)
        return jsonify({"error": "Invalid form"})
# ...
```
Now, we can delete and recreate our database again, and then let's register.
```shell
$ rm twitter.db
$ python
>>> import app
>>> app.db.create_all()
```
It works! But, how can we check if it truly has encrypted our email or not? We can look at our database using an app called (DB browser for SQLite)[https://sqlitebrowser.org/dl/], which is available for windows, mac and linux. Download and open the app, click on open database at the top, 
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/2pqf9bfqgilsegws4x1j.png)
and then, choose your database file. Then, it will open the database. We can see our tables we created, namely `invalid_tokens`, `user` and `tweet`. Click on the browse data menu and choose the `user` table. And here, you can see, the email and password are a bunch of gibberish, meaning it worked! And since we're done with security, **IT'S TIME TO DEPLOY!**

---

## Prepaing for Deployment

We can now deploy our application. First, we have to prepare for deployment. 
> **Make sure you've committed everything to your git repository, just in case something goes wrong **
Now, we need to build our react application. Run this command:
```shell
npm run build
```
and this should create a brand new `build` folder in the frontend. Now, move that folder to the backend:
```shell
mv build ../backend/
```
And now, we need to serve the build in our flask application. Add these routes to `app.py`:
```python
@app.route("/<a>")
def react_routes(a):
    return app.send_static_file("index.html")


@app.route("/")
def react_index():
    return app.send_static_file("index.html")
```
And now, we need to change where we declared `app` to:
```python
app = Flask(__name__, static_folder="build", static_url_path="/")
```
And your `app.py` should look like:
```python
import dotenv
dotenv.load_dotenv()
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
import security

app = Flask(__name__, static_folder="build", static_url_path="/")
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)
    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("/<a>")
def react_routes(a):
    return app.send_static_file("index.html")


@app.route("/")
def react_index():
    return app.send_static_file("index.html")


@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: security.dec(x["email"]) == email and security.checkpwd(password, x["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 = security.encpwd(request.json["pwd"])
        username = request.json["username"]
        print(email, password, request.json["pwd"], username)
        if not (email and password and username):
            return jsonify({"error": "Invalid form"})
        # Check to see if user already exists
        users = getUsers()
        if len(list(filter(lambda x: security.dec(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, security.enc(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():
    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.message}


@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.message}


@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"]
        if not (title and content):
            return jsonify({"error": "Invalid form"})
        uid = get_jwt_identity()
        addTweet(title, content, uid)
        return jsonify({"success": "true"})
    except Exception as e:
        print(e)
        return jsonify({"error": "Invalid form"})


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


@app.route("/api/getcurrentuser")
@jwt_required
def get_current_user():
    uid = get_jwt_identity()
    return jsonify(getUser(uid))


@app.route("/api/changepassword", methods=["POST"])
@jwt_required
def change_password():
    try:
        user = User.query.get(get_jwt_identity())
        if not (request.json["password"] and request.json["npassword"]):
            return jsonify({"error": "Invalid form"})
        if not user.pwd == request.json["password"]:
            return jsonify({"error": "Wrong password"})
        user.pwd = request.json["npassword"]
        db.session.add(user)
        db.session.commit()
        return jsonify({"success": True})
    except Exception as e:
        print(e)
        return jsonify({"error": "Invalid form"})


@app.route("/api/deleteaccount", methods=["DELETE"])
@jwt_required
def delete_account():
    try:
        user = User.query.get(get_jwt_identity())
        tweets = Tweet.query.all()
        for tweet in tweets:
            if tweet.user.username == user.username:
                delTweet(tweet.id)
        removeUser(user.id)
        return jsonify({"success": True})
    except Exception as e:
        return jsonify({"error": str(e)})


if __name__ == "__main__":
    app.run(debug=True)
```

And now, you can stop your frontend server and now visit the flask server at http://localhost:5000 and you should see your React website. Now flask and react are connected together.
You can also remove `proxy` from the `package.json`, but you will have to rebuild the application:
```shell
cd frontend
rm -r build # if you have it
npm run build
rm -r ../backend/build
mv build ../backend
```
And now, our app should be working under one server, so now, we can deploy it.

---

# Deploying

I'll show you how you can deploy your application to - [Heroku](http://heroku.com). Don't want to deploy to Heroku? [Here's](https://blog.miguelgrinberg.com/post/how-to-deploy-a-react--flask-project) how you can deploy it to a Linux server.

## Deploying to Heroku
First, you need to sign up to a free account on [Heroku](https://heroku.com) and install the [Heroku CLI](https://devcenter.heroku.com/articles/heroku-cli). Once you've installed the heroku cli, type
```shell
heroku login
```
to login to heroku. Then, let's copy our backend folder to `quickr`, so here's my folder structure:
```
application
 | - backend
 | - frontend
 | - quickr
```
Now, we need to create a `git` repository in the quickr folder
```shell
cd quickr
git init
```
And now, go to the `.gitignore` file in the main folder and add `quickr/` to the `.gitignore`. Now, we need to create a `Procfile` file in the `quickr` directory. This tells Heroku how to run the app. Type this in the Procfile:
```
web: gunicorn app:app
```
What is `gunicorn`? It allows us to run applications. We need to install it though:
```shell
pip install gunicorn
```
Now, let's refresh our requirements by deleting `requirements.txt` and then typing 
```shell
pip freeze > requirements.txt
```
> Remember, we're doing this in the `quickr` folder, not `backend`
This is what it should look like:
```
aniso8601==8.0.0
astroid==2.4.1
bcrypt==3.1.7
certifi==2020.4.5.1
cffi==1.14.0
chardet==3.0.4
click==7.1.2
cryptography==2.9.2
Flask==1.1.2
Flask-Cors==3.0.8
Flask-JWT==0.3.2
Flask-JWT-Extended==3.24.1
Flask-RESTful==0.3.8
Flask-SQLAlchemy==2.4.3
get==2019.4.13
gunicorn==20.0.4
idna==2.9
isort==4.3.21
itsdangerous==1.1.0
Jinja2==2.11.2
lazy-object-proxy==1.4.3
MarkupSafe==1.1.1
mccabe==0.6.1
MouseInfo==0.1.3
Pillow==7.1.2
post==2019.4.13
public==2019.4.13
PyAutoGUI==0.9.50
pycparser==2.20
PyGetWindow==0.0.8
PyJWT==1.4.2
pylint==2.5.2
pymongo==3.10.1
PyMsgBox==1.0.7
pyperclip==1.8.0
PyRect==0.1.4
PyScreeze==0.1.26
python-dotenv==0.13.0
PyTweening==1.0.3
pytz==2020.1
query-string==2019.4.13
requests==2.23.0
rubicon-objc==0.3.1
selenium==3.141.0
six==1.14.0
SQLAlchemy==1.3.17
toml==0.10.0
urllib3==1.25.9
Werkzeug==1.0.1
wrapt==1.12.1
aniso8601==8.0.0
astroid==2.4.1
bcrypt==3.1.7
certifi==2020.4.5.1
cffi==1.14.0
chardet==3.0.4
click==7.1.2
cryptography==2.9.2
Flask==1.1.2
Flask-Cors==3.0.8
Flask-JWT==0.3.2
Flask-JWT-Extended==3.24.1
Flask-RESTful==0.3.8
Flask-SQLAlchemy==2.4.3
get==2019.4.13
idna==2.9
isort==4.3.21
itsdangerous==1.1.0
Jinja2==2.11.2
lazy-object-proxy==1.4.3
MarkupSafe==1.1.1
mccabe==0.6.1
MouseInfo==0.1.3
Pillow==7.1.2
post==2019.4.13
public==2019.4.13
PyAutoGUI==0.9.50
pycparser==2.20
PyGetWindow==0.0.8
PyJWT==1.4.2
pylint==2.5.2
pymongo==3.10.1
PyMsgBox==1.0.7
pyperclip==1.8.0
PyRect==0.1.4
PyScreeze==0.1.26
python-dotenv==0.13.0
PyTweening==1.0.3
pytz==2020.1
query-string==2019.4.13
requests==2.23.0
rubicon-objc==0.3.1
selenium==3.141.0
six==1.14.0
SQLAlchemy==1.3.17
toml==0.10.0
urllib3==1.25.9
Werkzeug==1.0.1
wrapt==1.12.1
```

Now, we need to add a `.gitignore` file in the `quickr` folder. Type this in it:
```
venv/
.env/
```
And finally, we're ready to deploy!
First, commit everything:
```shell
git add .
git commit -m "Init"
```
And then create the heroku app
```shell
heroku create appname
```
*If your app is taken, choose another name!*
Then, type `git remote -v` to check if your app was created successfully:
```shell
$ git remote -v
heroku  https://git.heroku.com/appname.git (fetch)
heroku  https://git.heroku.com/appname.git (push)
```
We're ready to deploy! Type:
```shell
git push heroku master
```
and your app should deploy!
Now, you can view your website at `appname.herokuapp.com`. If your app crashed, you can see its logs by typing
```shell
heroku logs --tail
```
Your app most likely crashed or the Register didn't work. And that's because it doesn't know about the `.env`! The `KEY` attribute will return `False`. We have to fix this! Hop over to the [Heroku Dashboard](https://dashboard.heroku.com/apps) and select your app. Now, click on settings and then "Reveal config vars". We need to add a variable called `KEY` and give it the value in the `.env`.

I also noticed a bug in the register route of `app.py`
```python
@app.route("/api/register", methods=["POST"])
def register():
    try:
        email = request.json["email"]
        email = email.lower()
        password = security.encpwd(request.json["pwd"])
        username = request.json["username"]
        print(email, password, request.json["pwd"], username)
        if not (email and password and username):
            return jsonify({"error": "Invalid form"})
        # Check to see if user already exists
        users = getUsers()
        if len(list(filter(lambda x: security.dec(x["email"]) == email, users))) == 1: # this line had the bug
            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, security.enc(email), password)
        return jsonify({"success": True})
    except Exception as e:
        print(e)
        return jsonify({"error": "Invalid form"})
```

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/fpj7qns1uvdb38v29c7m.png)
And now, when you click "Add", your app should restart and work fine.

And we're done! Whew!

> But what about the like, retweet, replies, editing tweets, admin panel and for keyboard's sake, dark mode?
Well, I'll be adding some short posts to this series in the future. Until then, stay tuned!

Code on GitHub: [here](https://github.com/arnu515/twitter-clone)
If I find any bugs, I'll be updating the code on GitHub, not here. So, if you find bugs, submit an issue. Anyway, thanks for spending >1hr of estimated time for reading my post. Means a lot to me :)

[Part 4 is out!](https://dev.to/arnu515/build-a-twitter-clone-with-flask-and-react-part-4-4fcb) This part I add a dark theme. It is very short. I am also planning to do many more parts where we add more functionality, like the Like button, Retweets, Replies, Editing, an admin panel and much more!