IF YOU HAVEN'T READ PART ONE YET, CLICK HERE
This is a 3 parter series. This is the second part.
Let's continue where we left off.
PART 2 - ADDING FUNCTIONALITY
Creating the home page
Let's create the page that we see AFTER we've logged in. I'm gonna create a new component called MainPage.jsx
.
// src/components/MainPage.jsx
import React from "react";
class MainPage extends React.Component {
render() {
return (
<React.Fragment>
<div
className="w3-container w3-jumbo"
style={{ margin: "3rem", paddingLeft: "1rem" }}>
Tweets
</div>
</React.Fragment>
);
}
}
export default MainPage;
For displaying a Tweet, let's create a separate TweetItem.jsx
component. This component will be a stateless functional component.
// src/components/TweetItem.jsx
import React from "react";
function TweetItem(props) {
return (
<div
className="w3-card w3-border w3-border-gray w3-round-large"
style={{ marginTop: "2rem" }}>
<div className="w3-container" style={{ padding: "2rem" }}>
<h2 className="w3-opacity w3-xxlarge">{props.title}</h2>
<div dangerouslySetInnerHTML={{ __html: props.content }}></div>
</div>
<footer className="w3-container w3-center w3-large">
<button className="w3-button" style={{ marginRight: "2rem" }}>
Like
</button>
<button className="w3-button" style={{ marginRight: "2rem" }}>
Retweet
</button>
<button className="w3-button">Reply</button>
</footer>
</div>
);
}
export default TweetItem;
The dangerouslySetInnerHTML
attribute added to the <div>
element allows us to render the HTML from a string. And like its name suggests, it is dangerous, because any hacker can add <script>
tags and execute malicious code. We are setting this attribute because we are going to use a WYSIWYG editor to allow the user to post their tweets with formatting. The WYSIWYG editor we're gonna use has precautions to prevent XSS Attacks.
Now, let's make a few dummy tweets to see how this goes. Update your MainPage.jsx
to look like this:
import React from "react";
import TweetItem from "./TweetItem";
class MainPage extends React.Component {
render() {
let tweets = [
{
title: "Hello, world!",
content: "<h3>Just gonna type html here!</h3>",
},
{ title: "Tweet", content: "<code>Code!</code>" },
{
title: "Nice!",
content:
"<a href='https://www.youtube.com/watch?v=dQw4w9WgXcQ'>Here's a link! I need to use single quotes for the href.</a>",
},
{
title: "Hello, world!",
content:
"<div>Typing <strong>using</strong> <em>more</em> <u>than</u> <sup>one</sup> <sub>html</sub> <del>tag</del>!</div>",
},
];
return (
<React.Fragment>
<div
className="w3-container w3-jumbo"
style={{ margin: "3rem", paddingLeft: "1rem" }}>
Tweets
</div>
<div className="w3-container">
{tweets.map((item, index) => {
return (
<TweetItem
title={item.title}
content={item.content}
key={index}
/>
);
})}
</div>
</React.Fragment>
);
}
}
export default MainPage;
As you can see, I'm iterating through every tweet in an array. I can use html tags to style the content. This is what your website should look like:
Adding a tweets model
Awesome! But, static data won't do! We need to get data from the database, but, we don't have any way to add tweets to our database! So, let's create a Tweet
model like we created the Users
model. Add this to app.py
:
class Tweet(db.Model):
id = db.Column(db.Integer, primary_key=True)
uid = db.Column(db.Integer, db.ForeignKey("user.id"))
user = db.relationship('User', foreign_keys=uid)
title = db.Column(db.String(256))
content = db.Column(db.String(2048))
So, if you see up there, I have added a new table (or model) called Tweet
, and also, let's rename the class Users
to User
, I forgot that in the last part :P. Now, let's add some CRUD functions.
def getTweets():
tweets = Tweet.query.all()
return [{"id": i.id, "title": i.title, "content": i.content, "user": getUser(i.uid)} for i in tweets]
def getUserTweets(uid):
tweets = Tweet.query.all()
return [{"id": item.id, "userid": item.user_id, "title": item.title, "content": item.content} for item in filter(lambda i: i.user_id == uid, tweets)]
def addTweet(title, content, uid):
if (title and content and uid):
try:
user = list(filter(lambda i: i.id == uid, User.query.all()))[0]
twt = Tweet(title=title, content=content, user=user)
db.session.add(twt)
db.session.commit()
return True
except Exception as e:
print(e)
return False
else:
return False
def delTweet(tid):
try:
tweet = Tweet.query.get(tid)
db.session.delete(tweet)
db.session.commit()
return True
except Exception as e:
print(e)
return False
I also made a few changes to the User
class.
class User(db.Model):
id = db.Column(db.Integer, primary_key = True) # primary_key makes it so that this value is unique and can be used to identify this record.
username = db.Column(db.String(24))
email = db.Column(db.String(64))
pwd = db.Column(db.String(64))
# Constructor
def __init__(self, username, email, pwd):
self.username = username
self.email = email
self.pwd = pwd
def getUsers():
users = User.query.all()
return [{"id": i.id, "username": i.username, "email": i.email, "password": i.pwd} for i in users]
def getUser(uid):
users = User.query.all()
user = list(filter(lambda x: x.id == uid, users))[0]
return {"id": user.id, "username": user.username, "email": user.email, "password": user.pwd}
def addUser(username, email, pwd):
try:
user = User(username, email, pwd)
db.session.add(user)
db.session.commit()
return True
except Exception as e:
print(e)
return False
def removeUser(uid):
try:
user = User.query.get(uid)
db.session.delete(user)
db.session.commit()
return True
except Exception as e:
print(e)
return False
Now, we can add some temporary routes and test if everything works. But first, since we made some changes to our Model, we need to reset the database. Find the file twitter.db
and delete it. Now, type:
python -i app.py
and press ^C
to terminate it. You should be in the python console now. Type:
import app
app.db.create_all()
And this should create twitter.db
.
Now, let's add a route for adding a tweet and getting all tweets.
@app.route("/api/tweets")
def get_tweets():
return jsonify(getTweets())
@app.route("/api/addtweet", methods=["POST"])
def add_tweet():
try:
title = request.json["title"]
content = request.json["content"]
uid = request.json["uid"]
addTweet(title, content, uid)
return jsonify({"success": "true"})
except Exception as e:
print(e)
return jsonify({"error": "Invalid form"})
Finally, let's test it. Make sure you already have a registered user. Type this command:
curl -X POST -H "Content-Type: application/json" -d '{"title": "a", "content": "e", "uid": 1}' "http://localhost:5000/api/addtweet"
If all is good, you should get {"success": true}
as an output.
Now, let's list the tweets:
curl "http://localhost:5000/api/tweets"
If your output looks similar to this, you're good!
[
{
"content": "e",
"id": 1,
"title": "a",
"user": {
"email": "sasdasd@asdasd.sads",
"id": 1,
"password": "as",
"username": "df"
}
}
]
Let's also add a delete route, so that we can delete tweets.
@app.route("/api/deletetweet", methods=["DELETE"])
def delete_tweet():
try:
tid = request.json["tid"]
delTweet(tid)
return jsonify({"success": "true"})
except:
return jsonify({"error": "Invalid form"})
Of course, we have to test it!
curl -X DELETE -H "Content-Type: application/json" -d '{"tid": 1}' "http://localhost:5000/api/deletetweet"
curl "http://localhost:5000/api/tweets"
# OUTPUT: []
Securing our API with JWT
Let's say you decide to make your API public. Or someone finds out your API routes. He can then perform many post requests and possibly impersonate users and add tweets on their behalf. Nobody want's that right? So, let's add some authentication to our API using JWT.
JWT stands for Json Web Token. It allows us to verify each user if they've logged in. You can read more about it here To add JWT
to your application, you need to install flask-jwt-extended
:
pip install flask-jwt-extended
We're using the extended version because it is easier to use.
Import JWT
from flask_jwt_extended import JWTManager, create_access_token, jwt_required, get_jwt_identity
Now, change your Login
route to return a json web token instead of true
.
@app.route("/api/login", methods=["POST"])
def login():
try:
email = request.json["email"]
password = request.json["pwd"]
if (email and password):
user = list(filter(lambda x: x["email"] == email and x["password"] == password, getUsers()))
# Check if user exists
if len(user) == 1:
token = create_access_token(identity=user[0]["id"])
return jsonify({"token": token})
else:
return jsonify({"error": "Invalid credentials"})
else:
return jsonify({"error": "Invalid form"})
except Exception as e:
print(e)
return jsonify({"error": "Invalid form"})
Before we run this code and test it out, we need to initialise JWT for our app like we did for CORS
. Type this under where you declared app
.
app = Flask(__name__)
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///twitter.db"
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
app.config["JWT_SECRET_KEY"] = "myawesomesecretisnevergonnagiveyouup"
CORS(app)
JWTManager(app)
When you publish your website, you may want to make your secret more secure and/or put it in an environment variable. We'll cover that in the third part. Also, I added the SQLALCHEMY_TRACK_MODIFICATIONS
value in the config to remove the annoying error we get in the console when we start our app. Now, if you try and login, you should get a token.
curl -X POST -H "Content-Type: application/json" -d '{"email": "email@domain.com", "pwd": "password"}' "http://localhost:5000/api/login"
Replace the data with whatever you've registered with
And this should be your output:
{
"token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE1OTIwNDE2NDgsIm5iZiI6MTU5MjA0MTY0OCwianRpIjoiMjNiZWViMTEtOWI4Mi00MDY3LWExODMtZDkyMzAyNDM4OGU2IiwiZXhwIjoxNTkyMDQyNTQ4LCJpZGVudGl0eSI6MiwiZnJlc2giOmZhbHNlLCJ0eXBlIjoiYWNjZXNzIn0.0zxftxUINCzhlJEfy1CJZtoFbzlS0Fowm66F5JuM49E"
}
If so, nice! Now, let's make some of our api routes protected. Protected routes are routes that require you to have an Authorization
header (Yes, with a z
, no matter where you live) to your request in order for it to go through. Let's add the decorator @jwt_required
in our tweet
routes.
@app.route("/api/tweets")
@jwt_required
def get_tweets():
return jsonify(getTweets())
@app.route("/api/addtweet", methods=["POST"])
@jwt_required
def add_tweet():
try:
title = request.json["title"]
content = request.json["content"]
uid = request.json["uid"]
addTweet(title, content, uid)
return jsonify({"success": "true"})
except Exception as e:
print(e)
return jsonify({"error": "Invalid form"})
@app.route("/api/deletetweet", methods=["DELETE"])
@jwt_required
def delete_tweet():
try:
tid = request.json["tid"]
delTweet(tid)
return jsonify({"success": "true"})
except:
return jsonify({"error": "Invalid form"})
And now, when you try to get tweets, you get this error:
$ curl "http://localhost:5000/api/tweets"
{
"msg": "Missing Authorization Header"
}
To fix this, we add a -H
attribute and set it to Bearer <YourToken>
, so, for me, the new command is:
curl -H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE1OTIwNDE2NDgsIm5iZiI6MTU5MjA0MTY0OCwianRpIjoiMjNiZWViMTEtOWI4Mi00MDY3LWExODMtZDkyMzAyNDM4OGU2IiwiZXhwIjoxNTkyMDQyNTQ4LCJpZGVudGl0eSI6MiwiZnJlc2giOmZhbHNlLCJ0eXBlIjoiYWNjZXNzIn0.0zxftxUINCzhlJEfy1CJZtoFbzlS0Fowm66F5JuM49E" "http://localhost:5000/api/tweets"
If you're using Insomnia or Postman, you need to add a Header with a name of Authorization
and value of Bearer <JWT>
to your request
And you should get a valid response. Awesome! I feel like we don't need to protect the GET
route, so I won't. Anyway, here's what your code should look like:
from flask import Flask, request, jsonify
from flask_sqlalchemy import SQLAlchemy
from flask_cors import CORS
import re
from flask_jwt_extended import JWTManager, create_access_token, jwt_required, get_jwt_identity
app = Flask(__name__)
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///twitter.db"
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
app.config["JWT_SECRET_KEY"] = "myawesomesecretisnevergonnagiveyouup"
CORS(app)
JWTManager(app)
# DB
db = SQLAlchemy(app)
class User(db.Model):
id = db.Column(db.Integer, primary_key = True) # primary_key makes it so that this value is unique and can be used to identify this record.
username = db.Column(db.String(24))
email = db.Column(db.String(64))
pwd = db.Column(db.String(64))
# Constructor
def __init__(self, username, email, pwd):
self.username = username
self.email = email
self.pwd = pwd
def getUsers():
users = User.query.all()
return [{"id": i.id, "username": i.username, "email": i.email, "password": i.pwd} for i in users]
def getUser(uid):
users = User.query.all()
user = list(filter(lambda x: x.id == uid, users))[0]
return {"id": user.id, "username": user.username, "email": user.email, "password": user.pwd}
def addUser(username, email, pwd):
try:
user = User(username, email, pwd)
db.session.add(user)
db.session.commit()
return True
except Exception as e:
print(e)
return False
def removeUser(uid):
try:
user = User.query.get(uid)
db.session.delete(user)
db.session.commit()
return True
except Exception as e:
print(e)
return False
class Tweet(db.Model):
id = db.Column(db.Integer, primary_key=True)
uid = db.Column(db.Integer, db.ForeignKey("user.id"))
user = db.relationship('User', foreign_keys=uid)
title = db.Column(db.String(256))
content = db.Column(db.String(2048))
def getTweets():
tweets = Tweet.query.all()
return [{"id": i.id, "title": i.title, "content": i.content, "user": getUser(i.uid)} for i in tweets]
def getUserTweets(uid):
tweets = Tweet.query.all()
return [{"id": item.id, "userid": item.user_id, "title": item.title, "content": item.content} for item in filter(lambda i: i.user_id == uid, tweets)]
def addTweet(title, content, uid):
try:
user = list(filter(lambda i: i.id == uid, User.query.all()))[0]
twt = Tweet(title=title, content=content, user=user)
db.session.add(twt)
db.session.commit()
return True
except Exception as e:
print(e)
return False
def delTweet(tid):
try:
tweet = Tweet.query.get(tid)
db.session.delete(tweet)
db.session.commit()
return True
except Exception as e:
print(e)
return False
# ROUTES
@app.route("/api/login", methods=["POST"])
def login():
try:
email = request.json["email"]
password = request.json["pwd"]
if (email and password):
user = list(filter(lambda x: x["email"] == email and x["password"] == password, getUsers()))
# Check if user exists
if len(user) == 1:
token = create_access_token(identity=user[0]["id"])
return jsonify({"token": token})
else:
return jsonify({"error": "Invalid credentials"})
else:
return jsonify({"error": "Invalid form"})
except Exception as e:
print(e)
return jsonify({"error": "Invalid form"})
@app.route("/api/register", methods=["POST"])
def register():
try:
email = request.json["email"]
email = email.lower()
password = request.json["pwd"]
username = request.json["username"]
# Check to see if user already exists
users = getUsers()
if(len(list(filter(lambda x: x["email"] == email, users))) == 1):
return jsonify({"error": "Invalid form"})
# Email validation check
if not re.match(r"[\w\._]{5,}@\w{3,}.\w{2,4}", email):
return jsonify({"error": "Invalid email"})
addUser(username, email, password)
return jsonify({"success": True})
except Exception as e:
print(e)
return jsonify({"error": "Invalid form"})
@app.route("/api/tweets")
def get_tweets():
return jsonify(getTweets())
@app.route("/api/addtweet", methods=["POST"])
@jwt_required
def add_tweet():
try:
title = request.json["title"]
content = request.json["content"]
uid = request.json["uid"]
addTweet(title, content, uid)
return jsonify({"success": "true"})
except Exception as e:
print(e)
return jsonify({"error": "Invalid form"})
@app.route("/api/deletetweet", methods=["DELETE"])
@jwt_required
def delete_tweet():
try:
tid = request.json["tid"]
delTweet(tid)
return jsonify({"success": "true"})
except:
return jsonify({"error": "Invalid form"})
if __name__ == "__main__":
app.run(debug=True)
Now we're ready to connect it to the frontend!
Connecting frontend to backend
First, we have to make it so that the user can only see the main page if they log in, so, change the default page from MainPage
to Home
. Let's create a login.js
file that will allow us to handle login events. What this file will do is that it will help us add JWT to the localstorage, check if JWT has expired, and log a person out.
// src/login.js
import Axios from "axios";
async function login(email, pwd) {
const res =await Axios.post("http://localhost:5000/api/login", {email, pwd});
const {data} = await res;
if (data.error) {
return data.error
} else {
localStorage.setItem("token", data.token);
return true
}
}
export {login};
Now, we have to implement the login
function in our Login.jsx
// src/components/Login.jsx
import React, { Component } from "react";
import axios from "axios";
import Alert from "./Alert";
import {login} from "../login";
class Login extends Component {
state = { err: "" };
login = (e) => {
e.preventDefault();
login(document.getElementById("email").value,
document.getElementById("password").value).then(r => {
if (r === true) {
this.setState({login: true})
} else {
this.setState({err: r})
}
})
};
render() {
return (
<div className="w3-card-4" style={{ margin: "2rem" }}>
<div className="w3-container w3-blue w3-center w3-xlarge">
LOGIN
</div>
<div className="w3-container">
{this.state.err.length > 0 && (
<Alert
message={`Check your form and try again! (${this.state.err})`}
/>
)}
<form onSubmit={this.login}>
<p>
<label htmlFor="email">Email</label>
<input
type="email"
className="w3-input w3-border"
id="email"
/>
</p>
<p>
<label htmlFor="password">Password</label>
<input
type="password"
className="w3-input w3-border"
id="password"
/>
</p>
<p>
<button type="submit" className="w3-button w3-blue">
Login
</button>
{this.state.login && "You're logged in!"}
</p>
</form>
</div>
</div>
);
}
}
export default Login;
Now, if we login, we can see the message You're logged in!
. But, to check if JWT was added to our browser's local storage, let's open the console and type localStorage
. If you see a token, success! But, there's still one thing missing - If the user is logged in, we need to show the tweets. If not, we need to show the home page.
Let's add a check
function to our login.js
:
// src/login.js
function check() {
if (localStorage.getItem("token")) {
return true;
} else {
return false;
}
}
export {login, check};
This is a very basic check. In the next part, we will add tokens that will expire and also upgrade our check to see if the token is valid or not.
We can now add this check
functionality to our App.jsx
// src/components/App.jsx
<Route path="/" exact component={check() ? MainPage : Home} />
Also, let's make the Login page redirect to the Home page and the register page redirect to our login page.
// src/components/Login.jsx
login = (e) => {
e.preventDefault();
login(document.getElementById("email").value,
document.getElementById("password").value).then(r => {
if (r === true) {
window.location = "/"
} else {
this.setState({err: r})
}
})
};
// src/components/Register.jsx
register = (e) => {
e.preventDefault();
axios
.post("http://localhost:5000/api/register", {
email: document.getElementById("email").value,
username: document.getElementById("username").value,
pwd: document.getElementById("password").value,
})
.then((res) => {
if (res.data.error) {
this.setState({ err: res.data.error });
} else {
window.location = "/login"
}
});
};
Nice! Now, let's work on the tweets
Fetching tweets from our database
Since our MainPage.jsx
is a class-component
, we can add a function called componentDidMount()
to our class. This function fires when the module renders. Let's make it fetch data from the database. Also, just before I forget, let's add this line anywhere above scripts
to our package.json
:
"proxy": "http://localhost:5000",
So now, instead of writing http://localhost:5000
everytime in our API calls, we can only specify the path. This will be useful later when we deploy. So, find any Axios
calls in the frontend and remove http://localhost:5000
from them. Eg:
// src/login.js
async function login(email, pwd) {
const res =await Axios.post("/api/login", {email, pwd});
const {data} = await res;
if (data.error) {
return data.error
} else {
localStorage.setItem("token", data.token);
return true
}
}
NOTE: You need to restart your server to see the effect
Now, back to our MainPage.jsx
// src/components/MainPage.jsx
import React from "react";
import TweetItem from "./TweetItem";
import Axios from "axios";
class MainPage extends React.Component {
state = {tweets: []}
componentDidMount() {
Axios.get("/api/tweets").then(res => {
this.setState({tweets: res.data})
});
}
render() {
return (
<React.Fragment>
<div
className="w3-container w3-jumbo"
style={{ margin: "3rem", paddingLeft: "1rem" }}>
Tweets
</div>
<div className="w3-container">
{this.state.tweets.length === 0 ? <p className="w3-xlarge w3-opacity" style={{marginLeft: "2rem"}}>No tweets! Create one</p> : this.state.tweets.map((item, index) => {
return (
<TweetItem
title={item.title}
content={item.content}
key={index}
/>
);
})}
</div>
</React.Fragment>
);
}
}
export default MainPage;
If you have no tweets, you should see this.
Let's add a tweet:
curl -X POST -H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE1OTIxMTc4NTAsIm5iZiI6MTU5MjExNzg1MCwianRpIjoiYmEzMzA1ZWItNjFlNS00ZWQ5LTg2MTgtN2JiMDRkNTAyZTBiIiwiZXhwIjoxNTkyMTE4NzUwLCJpZGVudGl0eSI6MiwiZnJlc2giOmZhbHNlLCJ0eXBlIjoiYWNjZXNzIn0.emhpKPeHYMS3Vk4hOZ_Y0R1herf7vygp9jpRUQnCIao" -H "Content-Type: application/json" -d '{"title": "abcd", "content": "<p>xyz</p>", "uid": 1}' http://localhost:5000/api/addtweet
Now, let's refresh our page. And we see:
Awesome!
Improving the login system
Flask-JWT
by default expires all login tokens in 15 minutes. We need to check for the expiration of these tokens and refresh them if they expire. Let's also add a logout functionality.
// src/login.js
import Axios from "axios";
async function login(email, pwd) {
const res = await Axios.post("/api/login", {email, pwd});
const {data} = await res;
if (data.error) {
return data.error
} else {
localStorage.setItem("token", data.token);
localStorage.setItem("refreshToken", data.refreshToken);
return true
}
}
async function check() {
const token = localStorage.getItem("token")
try {
const res = await Axios.post("/api/checkiftokenexpire", {}, {
headers: {
Authorization: "Bearer " + token
}
})
const {data} = await res;
return data.success
} catch {
console.log("p")
const refresh_token = localStorage.getItem("refreshToken")
if (!refresh_token) {
localStorage.removeItem("token")
return false;
}
Axios.post("/api/refreshtoken", {}, {
headers: {
Authorization: `Bearer ${refresh_token}`
}
}).then(res => {
localStorage.setItem("token", res.data.token)
})
return true;
}
}
function logout() {
if (localStorage.getItem("token")) {
const token = localStorage.getItem("token")
Axios.post("/api/logout/access", {}, {
headers: {
Authorization: `Bearer ${token}`
}
}).then(res => {
if (res.data.error) {
console.error(res.data.error)
} else {
localStorage.removeItem("token")
}
})
}
if (localStorage.getItem("refreshToken")) {
const refreshToken = localStorage.getItem("refreshToken")
Axios.post("/api/logout/refresh", {}, {
headers: {
Authorization: `Bearer ${refreshToken}`
}
}).then(res => {
if (res.data.error) {
console.error(res.data.error)
} else {
localStorage.removeItem("refreshToken")
}
})
}
localStorage.clear();
setTimeout(() => window.location = "/", 500)
}
export {login, check, logout};
// src/components/App.jsx
import React from "react";
import Home from "./Home";
import Navbar from "./Navbar";
import Login from "./Login";
import Register from "./Register";
import { BrowserRouter as Router, Route, Switch } from "react-router-dom";
import MainPage from "./MainPage";
import {check} from "../login";
import Logout from "./Logout";
function App() {
let [login, setLogin] = React.useState(false);
check().then(r => setLogin(r))
return (
<React.Fragment>
<Navbar />
<Router>
<Route path="/" exact>
{login ? <MainPage/> : <Home/>}
</Route>
<Route path="/login" exact component={Login} />
<Route path="/register" exact component={Register} />
<Route path="/logout" exact component={Logout} />
</Router>
</React.Fragment>
);
}
export default App;
Let's create the logout component that we used in our app:
import React from "react";
import {logout} from "../login";
class Logout extends React.Component {
componentDidMount() {
logout()
}
render() {
return (
<div className="w3-container w3-xlarge">
<p>Please wait, logging you out...</p>
</div>
)
}
}
export default Logout;
// src/components/Login.jsx
import React, {Component} from "react";
import axios from "axios";
import Alert from "./Alert";
import {login, check} from "../login";
class Login extends Component {
state = {err: ""};
componentDidMount() {
check().then(r => {if (r) {
window.location = "/"
}})
}
login = (e) => {
e.preventDefault();
login(document.getElementById("email").value,
document.getElementById("password").value).then(r => {
if (r === true) {
window.location = "/"
} else {
this.setState({err: r})
}
})
};
render() {
return (
<div className="w3-card-4" style={{margin: "2rem"}}>
<div className="w3-container w3-blue w3-center w3-xlarge">
LOGIN
</div>
<div className="w3-container">
{this.state.err.length > 0 && (
<Alert
message={`Check your form and try again! (${this.state.err})`}
/>
)}
<form onSubmit={this.login}>
<p>
<label htmlFor="email">Email</label>
<input
type="email"
className="w3-input w3-border"
id="email"
/>
</p>
<p>
<label htmlFor="password">Password</label>
<input
type="password"
className="w3-input w3-border"
id="password"
/>
</p>
<p>
<button type="submit" className="w3-button w3-blue">
Login
</button>
</p>
</form>
</div>
</div>
);
}
}
export default Login;
And finally, app.py
from flask import Flask, request, jsonify
from flask_sqlalchemy import SQLAlchemy
from flask_cors import CORS
import re
from flask_jwt_extended import JWTManager, create_access_token, jwt_required, get_jwt_identity, \
jwt_refresh_token_required, create_refresh_token, get_raw_jwt
app = Flask(__name__)
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///twitter.db"
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
db = SQLAlchemy(app)
app.config["JWT_SECRET_KEY"] = "myawesomesecretisnevergonnagiveyouup"
app.config["JWT_BLACKLIST_ENABLED"] = True
app.config["JWT_BLACKLIST_TOKEN_CHECKS"] = ["access", "refresh"]
jwt = JWTManager(app)
CORS(app)
# DB
class User(db.Model):
id = db.Column(db.Integer,
primary_key=True) # primary_key makes it so that this value is unique and can be used to identify this record.
username = db.Column(db.String(24))
email = db.Column(db.String(64))
pwd = db.Column(db.String(64))
# Constructor
def __init__(self, username, email, pwd):
self.username = username
self.email = email
self.pwd = pwd
def getUsers():
users = User.query.all()
return [{"id": i.id, "username": i.username, "email": i.email, "password": i.pwd} for i in users]
def getUser(uid):
users = User.query.all()
user = list(filter(lambda x: x.id == uid, users))[0]
return {"id": user.id, "username": user.username, "email": user.email, "password": user.pwd}
def addUser(username, email, pwd):
try:
user = User(username, email, pwd)
db.session.add(user)
db.session.commit()
return True
except Exception as e:
print(e)
return False
def removeUser(uid):
try:
user = User.query.get(uid)
db.session.delete(user)
db.session.commit()
return True
except Exception as e:
print(e)
return False
class Tweet(db.Model):
id = db.Column(db.Integer, primary_key=True)
uid = db.Column(db.Integer, db.ForeignKey("user.id"))
user = db.relationship('User', foreign_keys=uid)
title = db.Column(db.String(256))
content = db.Column(db.String(2048))
def getTweets():
tweets = Tweet.query.all()
return [{"id": i.id, "title": i.title, "content": i.content, "user": getUser(i.uid)} for i in tweets]
def getUserTweets(uid):
tweets = Tweet.query.all()
return [{"id": item.id, "userid": item.user_id, "title": item.title, "content": item.content} for item in
filter(lambda i: i.user_id == uid, tweets)]
def addTweet(title, content, uid):
try:
user = list(filter(lambda i: i.id == uid, User.query.all()))[0]
twt = Tweet(title=title, content=content, user=user)
db.session.add(twt)
db.session.commit()
return True
except Exception as e:
print(e)
return False
def delTweet(tid):
try:
tweet = Tweet.query.get(tid)
db.session.delete(tweet)
db.session.commit()
return True
except Exception as e:
print(e)
return False
class InvalidToken(db.Model):
__tablename__ = "invalid_tokens"
id = db.Column(db.Integer, primary_key=True)
jti = db.Column(db.String)
def save(self):
db.session.add(self)
db.session.commit()
@classmethod
def is_invalid(cls, jti):
q = cls.query.filter_by(jti=jti).first()
return bool(q)
@jwt.token_in_blacklist_loader
def check_if_blacklisted_token(decrypted):
jti = decrypted["jti"]
return InvalidToken.is_invalid(jti)
# ROUTES
@app.route("/api/login", methods=["POST"])
def login():
try:
email = request.json["email"]
password = request.json["pwd"]
if email and password:
user = list(filter(lambda x: x["email"] == email and x["password"] == password, getUsers()))
# Check if user exists
if len(user) == 1:
token = create_access_token(identity=user[0]["id"])
refresh_token = create_refresh_token(identity=user[0]["id"])
return jsonify({"token": token, "refreshToken": refresh_token})
else:
return jsonify({"error": "Invalid credentials"})
else:
return jsonify({"error": "Invalid form"})
except Exception as e:
print(e)
return jsonify({"error": "Invalid form"})
@app.route("/api/register", methods=["POST"])
def register():
try:
email = request.json["email"]
email = email.lower()
password = request.json["pwd"]
username = request.json["username"]
# Check to see if user already exists
users = getUsers()
if (len(list(filter(lambda x: x["email"] == email, users))) == 1):
return jsonify({"error": "Invalid form"})
# Email validation check
if not re.match(r"[\w\._]{5,}@\w{3,}.\w{2,4}", email):
return jsonify({"error": "Invalid email"})
addUser(username, email, password)
return jsonify({"success": True})
except Exception as e:
print(e)
return jsonify({"error": "Invalid form"})
@app.route("/api/checkiftokenexpire", methods=["POST"])
@jwt_required
def check_if_token_expire():
print(get_jwt_identity())
return jsonify({"success": True})
@app.route("/api/refreshtoken", methods=["POST"])
@jwt_refresh_token_required
def refresh():
identity = get_jwt_identity()
token = create_access_token(identity=identity)
return jsonify({"token": token})
@app.route("/api/logout/access", methods=["POST"])
@jwt_required
def access_logout():
jti = get_raw_jwt()["jti"]
try:
invalid_token = InvalidToken(jti=jti)
invalid_token.save()
return jsonify({"success": True})
except Exception as e:
print(e)
return {"error": e}
@app.route("/api/logout/refresh", methods=["POST"])
@jwt_required
def refresh_logout():
jti = get_raw_jwt()["jti"]
try:
invalid_token = InvalidToken(jti=jti)
invalid_token.save()
return jsonify({"success": True})
except Exception as e:
print(e)
return {"error": e}
@app.route("/api/tweets")
def get_tweets():
return jsonify(getTweets())
@app.route("/api/addtweet", methods=["POST"])
@jwt_required
def add_tweet():
try:
title = request.json["title"]
content = request.json["content"]
uid = request.json["uid"]
addTweet(title, content, uid)
return jsonify({"success": "true"})
except Exception as e:
print(e)
return jsonify({"error": "Invalid form"})
@app.route("/api/deletetweet", methods=["DELETE"])
@jwt_required
def delete_tweet():
try:
tid = request.json["tid"]
delTweet(tid)
return jsonify({"success": "true"})
except:
return jsonify({"error": "Invalid form"})
if __name__ == "__main__":
app.run(debug=True)
Whew! That should finish up the login work.
Allow users to create tweets
Now, let's allow users to create tweets. First, we need a form where users can enter their tweets. I choose to design a modal that will appear on the click of a button. You can choose to do the same or create a new page to make a tweet. For the modal, let's create a new component called AddTweet.jsx
// src/components/AddTweet.jsx
import React from "react";
function AddTweet() {
return (<div className="w3-modal w3-animate-opacity" id="addTweet">
<div className="w3-modal-content w3-card">
<header className="w3-container w3-blue">
<span className="w3-button w3-display-topright w3-hover-none w3-hover-text-white" onClick={() => {
document.getElementById("addTweet").style.display = "none"
}}>X</span>
<h2>Add tweet</h2>
</header>
<form className="w3-container">
<div className="w3-section">
<label htmlFor="title">Title</label>
<input type="text" id="title" className="w3-input w3-border w3-margin-bottom"/>
<textarea cols="30" rows="10"/>
</div>
</form>
</div>
</div>)
}
export default AddTweet
And let's add a button to MainPage.jsx
to open this model
// src/components/MainPage.jsx
import AddTweet from "./AddTweet";
// ...
<div
className="w3-container w3-jumbo"
style={{ margin: "3rem", paddingLeft: "1rem" }}>
<h1>Tweets</h1>
<button className="w3-button w3-blue w3-large" onClick={() => {
document.getElementById("addTweet").style.display = "block"
}}>Add tweet</button>
</div>
<AddTweet />
/...
And this is what our website should look like:
But what about that fancy pants WYSIWYG editor?
Well, first, we need one. There are many choices out there. There's TinyMCE, the one I recommend. It also has react support. But, if you don't like TinyMCE, there's Froala, used by companies like Amazon and IBM (they say). Also, there's Editor.js, CKEditor 4, (Quill)[https://quilljs.com/] and many more. You can just search for a WYSIWYG editor or use BBCode or Markdown, like this site.
I'm gonna use TinyMCE because it has React support.
First, head over to tiny.cloud and create an account (don't worry, TinyMCE is free for indivisuals!). Now, you should be in your dashboard. Now, we need to install @tinymce/tinymce-react
in our frontend
npm i @tinymce/tinymce-react
Now that TinyMCE is installed, let's use it in our website.
// src/components/AddTweet.jssx
import React from "react";
import {Editor} from "@tinymce/tinymce-react/lib/cjs/main/ts";
function AddTweet() {
let [content, setContent] = React.useState("");
return (<div className="w3-modal w3-animate-opacity" id="addTweet">
<div className="w3-modal-content w3-card">
<header className="w3-container w3-blue">
<span className="w3-button w3-display-topright w3-hover-none w3-hover-text-white" onClick={() => {
document.getElementById("addTweet").style.display = "none"
}}>X</span>
<h2>Add tweet</h2>
</header>
<form className="w3-container">
<div className="w3-section">
<p>
<label htmlFor="title">Title</label>
<input type="text" id="title" className="w3-input w3-border w3-margin-bottom"/>
</p>
<Editor
initialValue="<p>This is the initial content of the editor</p>"
init={{
height: 300,
menubar: false,
statusbar: false,
toolbar_mode: "sliding",
plugins: [
'advlist autolink lists link image imagetools media emoticons preview anchor',
'searchreplace visualblocks code fullscreen',
'insertdatetime media table paste code help wordcount'
],
toolbar:
'undo redo | formatselect | bold italic underline strikethrough | image anchor media | \
alignleft aligncenter alignright alignjustify | \
outdent indent | bulllist numlist | fullscreen preview | emoticons help',
contextmenu: "bold italic underline indent outdent help"
}}
/>
<p>
<button type="submit" className="w3-button w3-blue">Post</button>
</p>
</div>
</form>
</div>
</div>)
}
export default AddTweet
And here's what our website should look like:
Ahh, much better. But what about that little warning up there? To fix that, we need to add our apikey
to our Editor. Open your TinyMCE Dashboard and copy your api key. Then, add this line as a prop to your editor:
apiKey: 'your-api-key'
This should now supress the warnings. If not, check out your Approved domains
Now we need to add the functionality of posting. First, let's make a change to the addtweets
route in app.py
.
@app.route("/api/addtweet", methods=["POST"])
@jwt_required
def add_tweet():
try:
title = request.json["title"]
content = request.json["content"]
uid = get_jwt_identity() # The line that changed
addTweet(title, content, uid)
return jsonify({"success": "true"})
except Exception as e:
print(e)
return jsonify({"error": "Invalid form"})
Instead of giving the uid
in the post request, we can get it from the JWT
.
Now, let's get the content from the TinyMCE editor and post it to our database. (Also, I decided to convert AddTweet
to a class component.
// src/components/AddTweet.jsx
import React from "react";
import {Editor} from "@tinymce/tinymce-react/lib/cjs/main/ts";
import Axios from "axios";
class AddTweet extends React.Component {
state = {content: ""}
handleEditorChange = (content, editor) => {
console.log(content)
this.setState({content})
}
submitForm = (e) => {
e.preventDefault()
Axios.post("/api/addtweet", {
title: document.getElementById("title").value,
content: this.state.content
}, {
headers: {
Authorization: "Bearer " + localStorage.getItem("token")
}
}).then(res => {
if (res.data.success) {
window.location.reload()
}
})
}
render() {
return (<div className="w3-modal w3-animate-opacity" id="addTweet">
<div className="w3-modal-content w3-card">
<header className="w3-container w3-blue">
<span className="w3-button w3-display-topright w3-hover-none w3-hover-text-white" onClick={() => {
document.getElementById("addTweet").style.display = "none"
}}>X</span>
<h2>Add tweet</h2>
</header>
<form className="w3-container" onSubmit={this.submitForm}>
<div className="w3-section">
<p>
<label htmlFor="title">Title</label>
<input type="text" id="title" className="w3-input w3-border w3-margin-bottom"/>
</p>
<Editor
initialValue="<p>This is the initial content of the editor</p>"
init={{
height: 300,
menubar: false,
statusbar: false,
toolbar_mode: "sliding",
plugins: [
'advlist autolink lists link image imagetools media emoticons preview anchor',
'searchreplace visualblocks code fullscreen',
'insertdatetime media table paste code help wordcount'
],
toolbar:
'undo redo | formatselect | bold italic underline strikethrough | image anchor media | \
alignleft aligncenter alignright alignjustify | \
outdent indent | bulllist numlist | fullscreen preview | emoticons help',
contextmenu: "bold italic underline indent outdent help"
}}
onEditorChange={this.handleEditorChange}
/>
<p>
<button type="submit" className="w3-button w3-blue">Post</button>
</p>
</div>
</form>
</div>
</div>)
}
}
export default AddTweet
And now, when we post the tweet, Hurrah! The tweet appears. But, there is a problem. New tweets appear at the bottom. The solution is very simple! We can simply reverse the array in MainPage.jsx
. Just change componentDidMount
to this:
componentDidMount() {
Axios.get("/api/tweets").then(res => {
this.setState({tweets: res.data.reverse()})
});
}
And we're done boys!
But wait! What about deleting and updating tweets and users?
No time for that in this part. I'll cover that in the next one.
Anyway this has been Part 2. Cya! And of course, the code is available on Github
Top comments (3)
Part 3 is out: dev.to/arnu515/build-a-twitter-clo...
Thanks man you're a saver
You're welcome!