IF YOU HAVEN'T READ PART ONE YET, CLICK HERE
IF YOU HAVEN'T READ PART TWO YET, CLICK HERE*
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:
// 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;
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
:
@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:
// ...
{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
// 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:
I cannot see the delete button! Nice!
Now, let's add the functionality:
// 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
:
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:
There is a post with no title and no content! Let's add some client side form validation:
// 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:
# 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
// 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"})}>« 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"})}>« 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:
# 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
<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:
// 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
// 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 = "/"}>« Back</button>
</div>)
}
And then, we'll add a general route to 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";
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 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):
pip install bcrypt
If you're on Debian
or any other system that uses apt-get
, you need to install:
sudo apt-get install build-essential libffi-dev python-dev
For RHEL
or Fedora
:
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. Even big companies like 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, 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
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:
# 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!
# 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
>>> 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:
# 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
$ 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
>>> 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:
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
:
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.
pip install python-dotenv
Add this to the top of app.py
:
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:
# 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
# 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.
$ 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,
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:
npm run build
and this should create a brand new build
folder in the frontend. Now, move that folder to the backend:
mv build ../backend/
And now, we need to serve the build in our flask application. Add these routes to app.py
:
@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:
app = Flask(__name__, static_folder="build", static_url_path="/")
And your app.py
should look like:
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:
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. Don't want to deploy to Heroku? Here's how you can deploy it to a Linux server.
Deploying to Heroku
First, you need to sign up to a free account on Heroku and install the Heroku CLI. Once you've installed the heroku cli, type
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
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:
pip install gunicorn
Now, let's refresh our requirements by deleting requirements.txt
and then typing
pip freeze > requirements.txt
Remember, we're doing this in the
quickr
folder, notbackend
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:
git add .
git commit -m "Init"
And then create the heroku app
heroku create appname
If your app is taken, choose another name!
Then, type git remote -v
to check if your app was created successfully:
$ 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:
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
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 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
@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"})
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
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! 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!
Top comments (3)
PART 4 IS NOW OUT!
Here, I add a dark theme. It is a short ~2min read, so go check it out!
For those of you wondering, the app is live at quickr.herokuapp.com
This link should be added on readme repo.