Skip to content

Commit 5d60e6a

Browse files
authored
Merge pull request #53 from tecladocode/jose/cou-97-write-flask-jwt-extended-section
2 parents 90180f9 + 89c1fc5 commit 5d60e6a

File tree

426 files changed

+17928
-11
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

426 files changed

+17928
-11
lines changed

docs/docs-upcoming/07_flask_admin/01_project_overview/README.md

Lines changed: 0 additions & 1 deletion
This file was deleted.

docs/docs-upcoming/07_flask_admin/README.md

Lines changed: 0 additions & 1 deletion
This file was deleted.

docs/docs-upcoming/07_flask_admin/_category_.json

Lines changed: 0 additions & 4 deletions
This file was deleted.

docs/docs-upcoming/08_flask_jwt_extended/01_project_overview/README.md

Lines changed: 0 additions & 1 deletion
This file was deleted.

docs/docs-upcoming/08_flask_jwt_extended/README.md

Lines changed: 0 additions & 1 deletion
This file was deleted.

docs/docs/06_sql_storage_sqlalchemy/01_project_overview_sqlalchemy/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ description: Let's look at what we'll do in this section. There are no changes t
66
# Project Overview (and why use SQLAlchemy)
77

88
- [x] Set metadata above
9-
- [ ] Start writing!
9+
- [x] Start writing!
1010

1111
In this section we'll make absolutely no changes to the API! However, we will completely change the way we store data.
1212

docs/docs/07_sqlalchemy_many_to_many/02_one_to_many_review/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@ class StoreModel(db.Model):
5959
</Tabs>
6060
</div>
6161

62+
Remember to import the `TagModel` in `models/__init__.py` so that it is then imported by `app.py`. Otherwise SQLAlchemy won't know about it, and it won't be able to create the tables.
63+
6264
## The marshmallow schemas
6365

6466
These are the new schemas we'll add. Note that none of the tag schemas have any notion of "items". We'll add those to the schemas when we construct the many-to-many relationship.

docs/docs/07_sqlalchemy_many_to_many/03_many_to_many_relationships/README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,15 @@ class ItemsTags(db.Model):
6363
tag_id = db.Column(db.Integer, db.ForeignKey("tags.id"))
6464
```
6565

66+
Let's also add this to our `models/__init__.py` file:
67+
68+
```python title="models/__init__.py"
69+
from models.item import ItemModel
70+
from models.tag import TagModel
71+
from models.store import StoreModel
72+
from models.item_tags import ItemsTags
73+
```
74+
6675
### Using the secondary table in the main models
6776

6877

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
---
2+
title: Changes in this section
3+
description: Overview of the API endpoints we'll use for user registration and authentication.
4+
---
5+
6+
# Changes in this section
7+
8+
In this section we will add the following endpoints:
9+
10+
| Method | Endpoint | Description |
11+
| -------------- | ----------------- | ----------------------------------------------------- |
12+
| `POST` | `/register` | Create user accounts given an `email` and `password`. |
13+
| `POST` | `/login` | Get a JWT given an `email` and `password`. |
14+
| 🔒 <br/> `POST` | `/logout` | Revoke a JWT. |
15+
| 🔒 <br/> `POST` | `/refresh` | Get a fresh JWT given a refresh JWT. |
16+
| `GET` | `/user/{user_id}` | (dev-only) Get info about a user given their ID. |
17+
| `DELETE` | `/user/{user_id}` | (dev-only) Delete a user given their ID. |
18+
19+
We will also protect some existing endpoints by requiring a JWT from clients. You can see which endpoints will be protected in [The API we'll build in this course](/docs/course_intro/what_is_rest_api/#the-api-well-build-in-this-course)
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
---
2+
title: What is a JWT?
3+
description: Understand what a JWT is, what data it contains, and how it may be used.
4+
---
5+
6+
# What is a JWT?
7+
8+
A JWT is a signed JSON object with a specific structure. Our Flask app will sign the JWTs with the secret key, proving that _it generated them_.
9+
10+
The Flask app generates a JWT when a user logs in (with their username and password). In the JWT, we'll store the user ID. The client then stores the JWT and sends it to us on every request.
11+
12+
Because we can prove our app generated the JWT (through its signature), and we will receive the JWT with the user ID in every request, we can _treat requests that include a JWT as "logged in"_.
13+
14+
For example, if we want certain endpoints to only be accessible to logged-in users, all we do is require a JWT in them. Since the client can only get a JWT after logging in, we know that including a JWT is proof that the client logged in successfully at some point in the past.
15+
16+
And since the JWT includes the user ID inside it, when we receive a JWT we know _who logged in_ to get the JWT.
17+
18+
There's a lot more information about JWTs here: [https://jwt.io/introduction](https://jwt.io/introduction). This includes information such as:
19+
20+
- What is stored inside a JWT?
21+
- Are JWTs secure?
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
---
2+
title: Flask-JWT-Extended setup
3+
description: Install and set up the Flask-JWT-Extended extension with our REST API.
4+
---
5+
6+
# Flask-JWT-Extended setup
7+
8+
- [x] Set metadata above
9+
- [x] Start writing!
10+
- [x] Create `start` folder
11+
- [x] Create `end` folder
12+
- [ ] Create per-file diff between `end` and `start` (use "Compare Folders")
13+
14+
First, let's update our requirements:
15+
16+
```diff title="requirements.txt"
17+
+ flask-jwt-extended
18+
```
19+
20+
Then we must do two things:
21+
22+
- Add the extension to our `app.py`.
23+
- Set a secret key that the extension will use to _sign_ the JWTs.
24+
25+
```python title="app.py"
26+
from flask import Flask
27+
from flask_smorest import Api
28+
# highlight-start
29+
from flask_jwt_extended import JWTManager
30+
# highlight-end
31+
32+
from db import db
33+
34+
from resources.item import blp as ItemBlueprint
35+
from resources.store import blp as StoreBlueprint
36+
from resources.tag import blp as TagBlueprint
37+
38+
39+
def create_app(db_url=None):
40+
app = Flask(__name__)
41+
app.config["API_TITLE"] = "Stores REST API"
42+
app.config["API_VERSION"] = "v1"
43+
app.config["OPENAPI_VERSION"] = "3.0.3"
44+
app.config["OPENAPI_URL_PREFIX"] = "/"
45+
app.config["OPENAPI_SWAGGER_UI_PATH"] = "/swagger-ui"
46+
app.config[
47+
"OPENAPI_SWAGGER_UI_URL"
48+
] = "https://cdn.jsdelivr.net/npm/swagger-ui-dist/"
49+
app.config["SQLALCHEMY_DATABASE_URI"] = db_url or "sqlite:///data.db"
50+
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
51+
app.config["PROPAGATE_EXCEPTIONS"] = True
52+
db.init_app(app)
53+
api = Api(app)
54+
55+
# highlight-start
56+
app.config["JWT_SECRET_KEY"] = "jose"
57+
jwt = JWTManager(app)
58+
# highlight-end
59+
60+
@app.before_first_request
61+
def create_tables():
62+
import models # noqa: F401
63+
64+
db.create_all()
65+
66+
api.register_blueprint(ItemBlueprint)
67+
api.register_blueprint(StoreBlueprint)
68+
api.register_blueprint(TagBlueprint)
69+
70+
return app
71+
```
72+
73+
:::caution
74+
The secret key set here, `"jose"`, is **not very safe**.
75+
76+
Instead you should generate a long and random secret key using something like `secrets.SystemRandom().getrandbits(128)`.
77+
:::
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
.venv
2+
*.pyc
3+
__pycache__
4+
data.db
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[flake8]
2+
per-file-ignores = __init__.py:F401
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
FLASK_APP=app
2+
FLASK_ENV=development
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
FROM python:3.10
2+
WORKDIR /app
3+
COPY ./requirements.txt requirements.txt
4+
RUN pip install --no-cache-dir --upgrade -r requirements.txt
5+
COPY . .
6+
CMD ["gunicorn", "--bind", "0.0.0.0:80", "app:create_app()"]
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
from flask import Flask
2+
from flask_smorest import Api
3+
from flask_jwt_extended import JWTManager
4+
5+
from db import db
6+
7+
from resources.item import blp as ItemBlueprint
8+
from resources.store import blp as StoreBlueprint
9+
from resources.tag import blp as TagBlueprint
10+
11+
12+
def create_app(db_url=None):
13+
app = Flask(__name__)
14+
app.config["API_TITLE"] = "Stores REST API"
15+
app.config["API_VERSION"] = "v1"
16+
app.config["OPENAPI_VERSION"] = "3.0.3"
17+
app.config["OPENAPI_URL_PREFIX"] = "/"
18+
app.config["OPENAPI_SWAGGER_UI_PATH"] = "/swagger-ui"
19+
app.config[
20+
"OPENAPI_SWAGGER_UI_URL"
21+
] = "https://cdn.jsdelivr.net/npm/swagger-ui-dist/"
22+
app.config["SQLALCHEMY_DATABASE_URI"] = db_url or "sqlite:///data.db"
23+
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
24+
app.config["PROPAGATE_EXCEPTIONS"] = True
25+
db.init_app(app)
26+
api = Api(app)
27+
28+
app.config["JWT_SECRET_KEY"] = "jose"
29+
jwt = JWTManager(app)
30+
31+
@app.before_first_request
32+
def create_tables():
33+
import models # noqa: F401
34+
35+
db.create_all()
36+
37+
api.register_blueprint(ItemBlueprint)
38+
api.register_blueprint(StoreBlueprint)
39+
api.register_blueprint(TagBlueprint)
40+
41+
return app
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import pytest
2+
from app import create_app
3+
4+
5+
@pytest.fixture()
6+
def app():
7+
app = create_app("sqlite://")
8+
app.config.update(
9+
{
10+
"TESTING": True,
11+
}
12+
)
13+
14+
yield app
15+
16+
17+
@pytest.fixture()
18+
def client(app):
19+
return app.test_client()
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from flask_sqlalchemy import SQLAlchemy
2+
3+
db = SQLAlchemy()
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
from models.item import ItemModel
2+
from models.tag import TagModel
3+
from models.store import StoreModel
4+
from models.item_tags import ItemsTags
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
from db import db
2+
3+
4+
class ItemModel(db.Model):
5+
__tablename__ = "items"
6+
7+
id = db.Column(db.Integer, primary_key=True)
8+
name = db.Column(db.String(80), unique=True, nullable=False)
9+
price = db.Column(db.Float(precision=2), unique=False, nullable=False)
10+
11+
store_id = db.Column(
12+
db.Integer, db.ForeignKey("stores.id"), unique=False, nullable=False
13+
)
14+
store = db.relationship("StoreModel", back_populates="items")
15+
16+
tags = db.relationship("TagModel", back_populates="items", secondary="items_tags")
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
from db import db
2+
3+
4+
class ItemsTags(db.Model):
5+
__tablename__ = "items_tags"
6+
7+
id = db.Column(db.Integer, primary_key=True)
8+
item_id = db.Column(db.Integer, db.ForeignKey("items.id"))
9+
tag_id = db.Column(db.Integer, db.ForeignKey("tags.id"))
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
from db import db
2+
3+
4+
class StoreModel(db.Model):
5+
__tablename__ = "stores"
6+
7+
id = db.Column(db.Integer, primary_key=True)
8+
name = db.Column(db.String(80), unique=True, nullable=False)
9+
10+
tags = db.relationship("TagModel", back_populates="store", lazy="dynamic")
11+
items = db.relationship("ItemModel", back_populates="store", lazy="dynamic")
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
from db import db
2+
3+
4+
class TagModel(db.Model):
5+
__tablename__ = "tags"
6+
7+
id = db.Column(db.Integer, primary_key=True)
8+
name = db.Column(db.String(80), unique=True, nullable=False)
9+
store_id = db.Column(db.String(), db.ForeignKey("stores.id"), nullable=False)
10+
11+
store = db.relationship("StoreModel", back_populates="tags")
12+
items = db.relationship("ItemModel", back_populates="tags", secondary="items_tags")
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
pytest
2+
black
3+
flake8
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
Flask-JWT-Extended
2+
Flask-Smorest
3+
Flask-SQLAlchemy
4+
passlib
5+
marshmallow
6+
python-dotenv
7+
gunicorn

docs/docs/08_flask_jwt_extended/03_flask_jwt_extended_setup/end/resources/__init__.py

Whitespace-only changes.
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import pytest
2+
3+
4+
@pytest.fixture()
5+
def created_store_id(client):
6+
response = client.post(
7+
"/stores",
8+
json={"name": "Test Store"},
9+
)
10+
11+
return response.json["id"]
12+
13+
14+
@pytest.fixture()
15+
def created_item_id(client, created_store_id):
16+
response = client.post(
17+
"/items",
18+
json={"name": "Test Item", "price": 10.5, "store_id": created_store_id},
19+
)
20+
21+
return response.json["id"]
22+
23+
24+
@pytest.fixture()
25+
def created_tag_id(client, created_store_id):
26+
response = client.post(
27+
f"/stores/{created_store_id}/tags",
28+
json={"name": "Test Tag"},
29+
)
30+
31+
return response.json["id"]

0 commit comments

Comments
 (0)