Commit d953c489 authored by Patrick van der Leer's avatar Patrick van der Leer
Browse files

Initial setup of JWT tokens

parent e9e510db
......@@ -34,11 +34,13 @@ def init(config_object=ProdConfig):
def configure_blueprints(app):
""" Configure blueprints. """
create_api(app)
from eBookHub.api import auth_app
app.register_blueprint(auth_app)
def create_api(app, host='localhost', port=5000, api_prefix=''):
from safrs import SAFRSAPI
from .models import Author, Book, BookEdition, BookSerie, Genre, Publisher
from .models import Author, Book, BookEdition, BookSerie, Genre, Publisher, User
api = SAFRSAPI(app, host='{}:{}'.format(host, port), prefix=api_prefix)
api.expose_object(Author)
......@@ -47,12 +49,21 @@ def create_api(app, host='localhost', port=5000, api_prefix=''):
api.expose_object(BookSerie)
api.expose_object(Genre)
api.expose_object(Publisher)
api.expose_object(User)
api._swagger_object['securityDefinitions'] = {
"Bearer": {
"type": "apiKey",
"name": "Authorization",
"in": "header"
}}
def configure_extensions(app):
"""Configure Flask extensions."""
ext.bcrypt.init_app(app)
ext.cache.init_app(app)
ext.jwt.init_app(app)
ext.db.init_app(app)
import eBookHub.models.core
ext.migrate.init_app(app, db)
......
from .auth import auth, auth_app
__all__ = (
auth,
auth_app
)
from flask import Blueprint, jsonify, request
from flask_httpauth import HTTPBasicAuth
from flask_jwt_extended import jwt_refresh_token_required, get_jwt_identity, unset_jwt_cookies, set_access_cookies, \
set_refresh_cookies
auth = HTTPBasicAuth()
auth_app = Blueprint('auth', __name__, url_prefix="/auth")
@auth.verify_password
def verify_password(email, password):
from eBookHub.models.user import User
user = User.login(email, password)
return user is None
@auth_app.route('/login', methods=['POST'])
def login():
if not request.is_json:
return jsonify({"msg": "Missing JSON in request"}), 400
email = request.json.get('email', None)
password = request.json.get('password', None)
if not email:
return jsonify({"msg": "Missing email parameter"}), 400
if not password:
return jsonify({"msg": "Missing password parameter"}), 400
from eBookHub.models.user import User
user = User.login(email, password)
resp = jsonify({'login': True})
set_access_cookies(resp, user.create_access_token())
set_refresh_cookies(resp, user.create_refresh_token())
return resp, 200
# Same thing as login here, except we are only setting a new cookie
# for the access token.
@auth_app.route('/token/refresh', methods=['POST'])
@jwt_refresh_token_required
def refresh():
from eBookHub.models import User
current_user = User.query.filter_by(email=get_jwt_identity()).first()
access_token = current_user.create_access_token(identity=current_user)
# Set the JWT access cookie in the response
resp = jsonify({'refresh': True})
set_access_cookies(resp, access_token)
return resp, 200
@auth_app.route('/token/remove', methods=['POST'])
def logout():
resp = jsonify({'logout': True})
unset_jwt_cookies(resp)
return resp, 200
......@@ -47,6 +47,11 @@ class Config(metaclass=MetaFlaskEnv):
'FLASK_SECRET_KEY',
'CjP6YrXaDq8nchVyCF4DEuzw8A9Gutmy5vnTWyXdHWts4TnqeE3' * mp.cpu_count()
)
JWT_SECRET_KEY = os.environ.get(
'FLASK_JWT_SECRET_KEY',
'CjP9Gutmy5vnTWyXdH6YrXaDq8nchVyCF4DEuzw' * mp.cpu_count()
)
JWT_REFRESH_COOKIE_PATH = '/auth/token/refresh'
APP_DIR = os.path.abspath(os.path.dirname(__file__)) # This directory
......
......@@ -2,11 +2,17 @@ from inspect import isfunction, ismethod
from datetime import datetime as dt
from safrs import SAFRSBase
from safrs.api_methods import search, re_search
from sqlalchemy.ext.declarative import DeclarativeMeta
from sqlalchemy.orm.collections import InstrumentedList
from .extensions import db
# Add search and startswith methods so we can perform lookups from the frontend
SAFRSBase.search = search
SAFRSBase.re_search = re_search
class PrettyPrint:
_exclude_from_meta_dump: list = []
......
from flask_bcrypt import Bcrypt
from flask_caching import Cache
from flask_jwt_extended import JWTManager
from flask_login import LoginManager
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
......@@ -9,3 +10,4 @@ login_manager = LoginManager()
db = SQLAlchemy()
migrate = Migrate()
cache = Cache()
jwt = JWTManager()
from .core import Author, Book, BookEdition, BookSerie, Genre, Publisher
from .user import User
__all__ = [
Author,
......@@ -6,5 +7,7 @@ __all__ = [
BookEdition,
BookSerie,
Genre,
Publisher
Publisher,
User
]
......@@ -26,7 +26,8 @@ class Genre(Model):
books = db.relationship(
"Book",
secondary=book_genre_table,
back_populates="genres"
back_populates="genres",
lazy='dynamic'
)
......@@ -34,7 +35,7 @@ class Publisher(Model):
"""Publisher"""
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(255))
books = db.relationship("BookEdition", back_populates="publisher")
books = db.relationship("BookEdition", back_populates="publisher", lazy='dynamic')
class Author(Model):
......@@ -46,7 +47,8 @@ class Author(Model):
books = db.relationship(
"Book",
secondary=author_book_table,
back_populates="authors"
back_populates="authors",
lazy='dynamic'
)
......@@ -60,12 +62,14 @@ class Book(Model):
authors = db.relationship(
"Author",
secondary=author_book_table,
back_populates="books"
back_populates="books",
lazy='dynamic'
)
genres = db.relationship(
"Genre",
secondary=book_genre_table,
back_populates="books"
back_populates="books",
lazy='dynamic'
)
serie_id = db.Column(db.Integer, db.ForeignKey('book_serie.id'))
serie = db.relationship("BookSerie", back_populates="books")
......@@ -105,4 +109,4 @@ class BookSerie(Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(255))
language = db.Column(db.String(3), comment="ISO-639-2")
books = db.relationship("Book", back_populates="serie")
books = db.relationship("Book", back_populates="serie", lazy='dynamic')
from safrs import jsonapi_rpc
from eBookHub.api import auth
from eBookHub.database import Model, db
from passlib.apps import custom_app_context as pwd_context
from flask_jwt_extended import jwt_required, create_access_token, create_refresh_token
class User(Model):
custom_decorators = [jwt_required, auth.login_required]
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(255))
email = db.Column(db.String(80, collation='NOCASE'), unique=True, nullable=False)
password_hash = db.Column(db.String(128))
language = db.Column(db.String(5), nullable=True, default="en")
timezone = db.Column(db.String(64), nullable=True, default="Europe/Amsterdam")
@staticmethod
def login(email_or_token, password=None):
"""
args:
email_or_token: email or token
password: password
:param email_or_token:
:param password:
:return:
"""
user = User.query.filter_by(email=email_or_token).first()
if not user or not user.verify_password(password):
return None
return user
def create_access_token(self):
return create_access_token(self.email)
def create_refresh_token(self):
return create_refresh_token(self.email)
@jsonapi_rpc(http_methods=['POST'])
def hash_password(self, password):
self.password_hash = pwd_context.encrypt(password)
@jsonapi_rpc(http_methods=['POST'])
def verify_password(self, password):
return pwd_context.verify(password, self.password_hash)
......@@ -66,6 +66,7 @@ genre_data = [
{"fiction": 1, "name": "Young adult"},
]
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
......
"""Auth
Revision ID: A00000000002
Revises: A00000000001
Create Date: 2019-03-12 16:15:22.112280
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'A00000000002'
down_revision = 'A00000000001'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
user_table = op.create_table(
'user',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('name', sa.String(length=255), nullable=True),
sa.Column('email', sa.String(length=80, collation='NOCASE'), nullable=False),
sa.Column('password_hash', sa.String(length=128), nullable=True),
sa.Column('language', sa.String(length=5), nullable=True),
sa.Column('timezone', sa.String(length=64), nullable=True),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('email')
)
op.bulk_insert(
user_table,
[
{
'name': 'Admin',
'email': 'admin@example.org',
"password_hash": "C7AD44CBAD762A5DA0A452F9E854FDC1E0E7A52A38015F23F"
"3EAB1D80B931DD472634DFAC71CD34EBC35D16AB7FB8A90C81"
"F975113D6C7538DC69DD8DE9077EC"
}
]
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('user')
# ### end Alembic commands ###
# -*- coding: utf-8 -*-
from eBookHub import init
from eBookHub.extensions import db
from eBookHub.extensions import db as _db
from eBookHub.config import TestConfig
from eBookHub.database import SoftDeleteMixin
class MyTestCase(object):
app = None
db = None
def create_app(self):
self.app = app = init(TestConfig)
......@@ -16,12 +17,13 @@ class MyTestCase(object):
def setUp(self):
with self.app.app_context():
db.create_all()
self.db = _db
_db.create_all()
def tearDown(self):
with self.app.app_context():
db.session.remove()
db.drop_all()
self.db.session.remove()
self.db.drop_all()
class ModelTestCase(MyTestCase):
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment