Skip to content

Commit 7f552a8

Browse files
deploying to Heroku
1 parent ac19a63 commit 7f552a8

File tree

11 files changed

+191
-39
lines changed

11 files changed

+191
-39
lines changed

app.py

+57-6
Original file line numberDiff line numberDiff line change
@@ -2,30 +2,81 @@
22

33
from flask import Flask
44
from flask_restful import Api
5-
from flask_jwt import JWT
5+
from flask_jwt_extended import JWTManager
66

7-
from security import authenticate, identity
8-
from resources.user import UserRegister
7+
from resources.user import UserRegister, User, UserLogin, UserLogout, TokenRefresh
98
from resources.item import Item, ItemList
109
from resources.store import Store, StoreList
10+
from blacklist import BLACKLIST
1111

1212
app = Flask(__name__)
1313

1414
app.config['DEBUG'] = True
15-
1615
app.config['SQLALCHEMY_DATABASE_URI'] = os.environ.get('DATABASE_URL', 'sqlite:///data.db')
1716
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
17+
app.config['PROPAGATE_EXCEPTIONS'] = True
18+
app.config['JWT_BLACKLIST_ENABLED'] = True
19+
app.config['JWT_BLACKLIST_TOKEN_CHECKS'] = ['access', 'refresh']
1820
app.secret_key = 'jose'
1921
api = Api(app)
2022

21-
jwt = JWT(app, authenticate, identity) # /auth
23+
jwt = JWTManager(app)
24+
25+
@jwt.user_claims_loader
26+
def add_claims_to_jwt(identity):
27+
if identity == 1:
28+
return {'is_admin': True}
29+
else:
30+
return {'is_admin': False}
31+
32+
@jwt.token_in_blacklist_loader
33+
def check_if_token_in_blacklist(decrypted_token):
34+
return decrypted_token['jti'] in BLACKLIST
35+
36+
@jwt.expired_token_loader
37+
def expired_token_callback():
38+
return {
39+
'description': 'The token has expired.',
40+
'error': 'token_expired'
41+
}, 401
42+
43+
@jwt.invalid_token_loader
44+
def invalid_token_callback(error):
45+
return {
46+
'description': 'Signature verification failed.',
47+
'error': "invalid_token"
48+
}, 401
49+
50+
@jwt.unauthorized_loader
51+
def missing_token_callback(error):
52+
return {
53+
'description': 'Request does not contain an access token.',
54+
'error': "authorization_required"
55+
}, 401
56+
57+
@jwt.needs_fresh_token_loader
58+
def token_not_fresh_callback():
59+
return {
60+
'description': 'The token is not fresh.',
61+
'error': "fresh_token_required"
62+
}, 401
63+
64+
@jwt.revoked_token_loader
65+
def revoked_token_callback():
66+
return {
67+
'description': 'The token has been revoked.',
68+
'error': "token_revoked"
69+
}, 401
2270

2371
api.add_resource(Store, '/store/<string:name>')
2472
api.add_resource(Item, '/item/<string:name>')
2573
api.add_resource(ItemList, '/items')
2674
api.add_resource(StoreList, '/stores')
27-
2875
api.add_resource(UserRegister, '/register')
76+
api.add_resource(User, '/user/<int:user_id>')
77+
api.add_resource(UserLogin, '/login')
78+
api.add_resource(UserLogout, '/logout')
79+
api.add_resource(TokenRefresh, '/refresh')
2980

3081
if __name__ == '__main__':
3182
from db import db

blacklist.py

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
BLACKLIST = set()

models/item.py

+10-1
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,21 @@ def __init__(self, name, price, store_id):
1616
self.store_id = store_id
1717

1818
def json(self):
19-
return {'name': self.name, 'price': self.price}
19+
return {
20+
'id': self.id,
21+
'name': self.name,
22+
'price': self.price,
23+
'store_id': self.store_id
24+
}
2025

2126
@classmethod
2227
def find_by_name(cls, name):
2328
return cls.query.filter_by(name=name).first()
2429

30+
@classmethod
31+
def find_all(cls):
32+
return cls.query.all()
33+
2534
def save_to_db(self):
2635
db.session.add(self)
2736
db.session.commit()

models/store.py

+9-1
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,20 @@ def __init__(self, name):
1212
self.name = name
1313

1414
def json(self):
15-
return {'name': self.name, 'items': [item.json() for item in self.items.all()]}
15+
return {
16+
'id': self.id,
17+
'name': self.name,
18+
'items': [item.json() for item in self.items.all()]
19+
}
1620

1721
@classmethod
1822
def find_by_name(cls, name):
1923
return cls.query.filter_by(name=name).first()
2024

25+
@classmethod
26+
def find_all(cls):
27+
return cls.query.all()
28+
2129
def save_to_db(self):
2230
db.session.add(self)
2331
db.session.commit()

models/user.py

+10
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,21 @@ class UserModel(db.Model):
1111
def __init__(self, username, password):
1212
self.username = username
1313
self.password = password
14+
15+
def json(self):
16+
return {
17+
'id': self.id,
18+
'username': self.username
19+
}
1420

1521
def save_to_db(self):
1622
db.session.add(self)
1723
db.session.commit()
1824

25+
def delete_from_db(self):
26+
db.session.delete(self)
27+
db.session.commit()
28+
1929
@classmethod
2030
def find_by_username(cls, username):
2131
return cls.query.filter_by(username=username).first()

requirements.txt

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
Flask
22
Flask-RESTful
3-
Flask-JWT
3+
Flask-JWT-Extended
44
Flask-SQLAlchemy
55
uwsgi
66
psycopg2

resources/item.py

+23-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
from flask_restful import Resource, reqparse
2-
from flask_jwt import jwt_required
2+
from flask_jwt_extended import (
3+
jwt_required,
4+
get_jwt_claims,
5+
jwt_optional,
6+
get_jwt_identity,
7+
fresh_jwt_required
8+
)
39
from models.item import ItemModel
410

511
class Item(Resource):
@@ -15,13 +21,14 @@ class Item(Resource):
1521
help="Every item needs a store id."
1622
)
1723

18-
@jwt_required()
24+
@jwt_required
1925
def get(self, name):
2026
item = ItemModel.find_by_name(name)
2127
if item:
2228
return item.json()
2329
return {'message': 'Item not found'}, 404
2430

31+
@fresh_jwt_required
2532
def post(self, name):
2633
if ItemModel.find_by_name(name):
2734
return {'message': "An item with name '{}' already exists.".format(name)}, 400
@@ -37,7 +44,11 @@ def post(self, name):
3744

3845
return item.json(), 201
3946

47+
@jwt_required
4048
def delete(self, name):
49+
claims = get_jwt_claims()
50+
if not claims['is_admin']:
51+
return {'message': 'Admin priveledges required.'}, 401
4152
item = ItemModel.find_by_name(name)
4253
if item:
4354
item.delete_from_db()
@@ -60,5 +71,14 @@ def put(self, name):
6071

6172

6273
class ItemList(Resource):
74+
75+
@jwt_optional
6376
def get(self):
64-
return {'items': [x.json() for x in ItemModel.query.all()]}
77+
user_id = get_jwt_identity()
78+
items = [item.json() for item in ItemModel.find_all()]
79+
if user_id:
80+
return {'items': items}, 200
81+
return {
82+
'items': [item['name'] for item in items],
83+
'message': 'More data available if you log in.'
84+
}, 200

resources/store.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -29,4 +29,4 @@ def delete(self, name):
2929

3030
class StoreList(Resource):
3131
def get(self):
32-
return {'stores': [store.json() for store in StoreModel.query.all()]}
32+
return {'stores': [store.json() for store in StoreModel.find_all()]}

resources/user.py

+78-14
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,34 @@
1-
import sqlite3
21
from flask_restful import Resource, reqparse
2+
from werkzeug.security import safe_str_cmp
3+
from flask_jwt_extended import (
4+
create_access_token,
5+
create_refresh_token,
6+
jwt_refresh_token_required,
7+
get_jwt_identity,
8+
jwt_required,
9+
get_raw_jwt
10+
)
11+
312
from models.user import UserModel
13+
from blacklist import BLACKLIST
414

5-
class UserRegister(Resource):
15+
_user_parser = reqparse.RequestParser()
16+
_user_parser.add_argument('username',
17+
type=str,
18+
required=True,
19+
help="This field cannot be blank."
20+
)
21+
_user_parser.add_argument('password',
22+
type=str,
23+
required=True,
24+
help="This field cannot be blank."
25+
)
626

7-
parser = reqparse.RequestParser()
8-
parser.add_argument('username',
9-
type=str,
10-
required=True,
11-
help="This field cannot be blank."
12-
)
13-
parser.add_argument('password',
14-
type=str,
15-
required=True,
16-
help="This field cannot be blank."
17-
)
27+
28+
class UserRegister(Resource):
1829

1930
def post(self):
20-
data = UserRegister.parser.parse_args()
31+
data = _user_parser.parse_args()
2132

2233
if UserModel.find_by_username(data['username']):
2334
return {"message": "A user with that username already exists"}, 400
@@ -26,3 +37,56 @@ def post(self):
2637
user.save_to_db()
2738

2839
return {"message": "User created successfully."}, 201
40+
41+
42+
class User(Resource):
43+
44+
@classmethod
45+
def get(cls, user_id):
46+
user = UserModel.find_by_id(user_id)
47+
if not user:
48+
return {'message': 'User not found'}, 404
49+
return user.json()
50+
51+
@classmethod
52+
def delete(cls, user_id):
53+
user = UserModel.find_by_id(user_id)
54+
if not user:
55+
return {'message': 'User not found'}, 404
56+
user.delete_from_db()
57+
return {'message': 'User deleted'}, 200
58+
59+
60+
class UserLogin(Resource):
61+
62+
@classmethod
63+
def post(cls):
64+
data = _user_parser.parse_args()
65+
user = UserModel.find_by_username(data['username'])
66+
67+
if user and safe_str_cmp(user.password, data['password']):
68+
access_token = create_access_token(identity=user.id, fresh=True)
69+
refresh_token = create_refresh_token(user.id)
70+
return {
71+
'access_token': access_token,
72+
'refresh_token': refresh_token
73+
}, 200
74+
75+
return {'message': 'Invalid credentials'}, 401
76+
77+
78+
class UserLogout(Resource):
79+
@jwt_required
80+
def post(self):
81+
jti = get_raw_jwt()['jti'] #jti is "JWT ID", a unique identifier for a JWT
82+
BLACKLIST.add(jti)
83+
return {'message': 'Successfully logged out'}, 200
84+
85+
86+
class TokenRefresh(Resource):
87+
88+
@jwt_refresh_token_required
89+
def post(self):
90+
current_user = get_jwt_identity()
91+
new_token = create_access_token(identity=current_user, fresh=False)
92+
return {'access_token': new_token}, 200

runtime.txt

+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
python-3.7.5
1+
python-3.8.2

security.py

-11
This file was deleted.

0 commit comments

Comments
 (0)