Skip to main content

In depth: app.py

Remember to download the code for this API from the resources section of this lecture. We'll be referencing the code heavily through this page and the next few!

Code for app.py

from flask import Flask, jsonify
from flask_restful import Api
from flask_jwt_extended import JWTManager

from db import db
from blacklist import BLACKLIST
from resources.user import UserRegister, UserLogin, User, TokenRefresh, UserLogout
from resources.item import Item, ItemList
from resources.store import Store, StoreList

app = Flask(__name__)
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///data.db"
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
app.config["PROPAGATE_EXCEPTIONS"] = True
app.config["JWT_BLACKLIST_ENABLED"] = True # enable blacklist feature
app.config["JWT_BLACKLIST_TOKEN_CHECKS"] = [
"access",
"refresh",
] # allow blacklisting for access and refresh tokens
app.secret_key = "jose" # could do app.config['JWT_SECRET_KEY'] if we prefer
api = Api(app)


@app.before_first_request
def create_tables():
db.create_all()


jwt = JWTManager(app)


@jwt.user_claims_loader
def add_claims_to_jwt(
identity
): # Remember identity is what we define when creating the access token
if (
identity == 1
): # instead of hard-coding, we should read from a file or database to get a list of admins instead
return {"is_admin": True}
return {"is_admin": False}


# This method will check if a token is blacklisted, and will be called automatically when blacklist is enabled
@jwt.token_in_blacklist_loader
def check_if_token_in_blacklist(decrypted_token):
return (
decrypted_token["jti"] in BLACKLIST
) # Here we blacklist particular JWTs that have been created in the past.


# The following callbacks are used for customizing jwt response/error messages.
# The original ones may not be in a very pretty format (opinionated)
@jwt.expired_token_loader
def expired_token_callback():
return jsonify({"message": "The token has expired.", "error": "token_expired"}), 401


@jwt.invalid_token_loader
def invalid_token_callback(
error
): # we have to keep the argument here, since it's passed in by the caller internally
return (
jsonify(
{"message": "Signature verification failed.", "error": "invalid_token"}
),
401,
)


@jwt.unauthorized_loader
def missing_token_callback(error):
return (
jsonify(
{
"description": "Request does not contain an access token.",
"error": "authorization_required",
}
),
401,
)


@jwt.needs_fresh_token_loader
def token_not_fresh_callback():
return (
jsonify(
{"description": "The token is not fresh.", "error": "fresh_token_required"}
),
401,
)


@jwt.revoked_token_loader
def revoked_token_callback():
return (
jsonify(
{"description": "The token has been revoked.", "error": "token_revoked"}
),
401,
)


api.add_resource(Store, "/store/<string:name>")
api.add_resource(StoreList, "/stores")
api.add_resource(Item, "/item/<string:name>")
api.add_resource(ItemList, "/items")
api.add_resource(UserRegister, "/register")
api.add_resource(User, "/user/<int:user_id>")
api.add_resource(UserLogin, "/login")
api.add_resource(TokenRefresh, "/refresh")
api.add_resource(UserLogout, "/logout")

if __name__ == "__main__":
db.init_app(app)
app.run(port=5000, debug=True)

As you can see, it's quite long! But there are a few distinct sections. Understanding what the different parts do will go a long way!

There are:

  • Imports (up to line 10)
  • App creation and config (up to line 21)
  • Set what to do before the first request ever hits our app (lines 24-26)
  • JWT extension error configuration (lines 29-100)
  • Resource registration (lines 103-111)
  • What to do when we run this file (lines 113-115)

Imports

Imports are fairly self-explanatory–we require Flask, Flask-RESTful, Flask-JWT-Extended, our database (Flask-SQLAlchemy), our blacklist (for logouts, more on that later), and our resources.

App creation and config

We then create our app and set some configuration parameters. In this app we're setting:

  • SQLALCHEMY_DATABASE_URI: which database we want to use. For now we're using SQLite as it's very easy to work with. Later on and when we deploy we would use PostgreSQL.
  • SQLALCHEMY_TRACK_MODIFICATIONS: a configuration for Flask-SQLAlchemy which keeps track of changes to SQLAlchemy models. SQLAlchemy already does that too, so often we don't need this. For more information, see this StackOverflow answer.
  • PROPAGATE_EXCEPTIONS: an almost wholly undocumented configuration parameter of Flask, it is needed for Flask-RESTful exceptions to show up in our app as errors instead of crashing our server with a generic "Internal Server Error").
  • JWT_BLACKLIST_ENABLED: whether to enable the blacklist in Flask-JWT-Extended or not. It's needed for allowing users to log out.
  • JWT_BLACKLIST_TOKEN_CHECKS: which tokens to compare against the blacklist. There are two: access tokens and refresh tokens (more on this later!).

Before the first request

Before the first request we want to create all our tables in the database. This only runs if there is no database already created, so it's very helpful for running our application locally.

Remember to delete our data.db file (which gets created when we make our first request) every time you change any of the models, so that the database gets reconstructed.

Flask-JWT configuration

This extensive configuration is covered in the last section of the introductory course, but is not really necessary for this course. We delete it in one of the first few videos.

Essentially it adds more functionality to Flask-JWT-Extended, such as claims and error handling.

They are:

  • @jwt.user_claims_loader: what extra information to add to the JWT (in our initial code, we add a flag to tell us whether the user is an admin or not depending on their user id).
  • @jwt.token_in_blacklist_loader: how to check whether a token is blacklisted or not (in our initial code, we compare the token's unique ID to what we've stored in our blacklist set).
  • @jwt.expired_token_loader: what error message to return when a token has expired but the user still tried to use it to gain access to a resource.
  • @jwt.invalid_token_loader: what error message to return when a token is used, but is invalid.
  • @jwt.unauthorized_loader: what error message to return when a request does not contain required authorization (e.g. missing JWT).
  • @jwt.needs_fresh_token_loader: what error message to return when a fresh token is required but a non-fresh token is given to us (more on this later!).
  • @jwt.revoked_token_loader: what error message to return when a token is used but it has been blacklisted.

Resource registration

Afterwards, we register our resources.

Every resource is a class that defines some methods. Each method corresponds to a HTTP verb (e.g. get() for GET request).

Registering the resources is where we define the URLs for them:

api.add_resource(Store, "/store/<string:name>")
api.add_resource(StoreList, "/stores")
api.add_resource(Item, "/item/<string:name>")
api.add_resource(ItemList, "/items")
api.add_resource(UserRegister, "/register")
api.add_resource(User, "/user/<int:user_id>")
api.add_resource(UserLogin, "/login")
api.add_resource(TokenRefresh, "/refresh")
api.add_resource(UserLogout, "/logout")

Running the app

Finally, we get to the end of the file where we run the app.

tip

In Python, if __name__ == "__main__": is a frequently used construct that means the subsequent code block only runs if this file was executed. It does not run if the file was imported.

In here, we register our database with our app, and start running our app on port 5000, and in debug mode.

if __name__ == "__main__":
db.init_app(app)
app.run(port=5000, debug=True)