├── tests ├── __init__.py ├── unit │ ├── __init__.py │ ├── users │ │ ├── __init__.py │ │ └── test_users.py │ └── helpers_test │ │ ├── __init__.py │ │ └── test_password.py ├── integration │ ├── __init__.py │ └── users │ │ ├── __init__.py │ │ └── test_users_api.py ├── fake_data │ └── users.json └── conftest.py ├── app ├── v1 │ ├── resources │ │ ├── __init__.py │ │ ├── auth │ │ │ ├── __init__.py │ │ │ ├── serializers.py │ │ │ └── login.py │ │ └── users │ │ │ ├── __init__.py │ │ │ ├── serializers.py │ │ │ ├── models.py │ │ │ └── user.py │ └── __init__.py ├── helpers │ ├── __init__.py │ ├── password.py │ └── parsers.py └── __init__.py ├── imgs └── swagger.png ├── .travis.yml ├── run.py ├── .editorconfig ├── .dockerignore ├── tox.ini ├── Dockerfile ├── requirements.txt ├── LICENSE ├── config.py ├── .gitignore └── README.md /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/unit/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/v1/resources/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/unit/users/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/v1/resources/auth/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/integration/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/v1/resources/users/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/integration/users/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/unit/helpers_test/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/unit/users/test_users.py: -------------------------------------------------------------------------------- 1 | pass 2 | -------------------------------------------------------------------------------- /app/helpers/__init__.py: -------------------------------------------------------------------------------- 1 | from .parsers import * 2 | from .password import * 3 | -------------------------------------------------------------------------------- /imgs/swagger.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alefeans/flask-base/HEAD/imgs/swagger.png -------------------------------------------------------------------------------- /tests/fake_data/users.json: -------------------------------------------------------------------------------- 1 | [{ 2 | "username": "user", 3 | "email": "user@user.com", 4 | "password": "user" 5 | }, 6 | { 7 | "username": "fake", 8 | "email": "fake@fake.com", 9 | "password": "fakefake" 10 | }] 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | 3 | services: 4 | - mongodb 5 | python: 6 | - "3.7" 7 | install: 8 | - pip install -r requirements.txt 9 | script: 10 | - tox -q 11 | before_script: 12 | - sleep 15 13 | after_success: 14 | - codecov -------------------------------------------------------------------------------- /app/helpers/password.py: -------------------------------------------------------------------------------- 1 | from bcrypt import hashpw, checkpw, gensalt 2 | 3 | 4 | def encrypt_password(password): 5 | return hashpw(password.encode('utf-8'), gensalt()).decode('utf-8') 6 | 7 | 8 | def check_password(sent, expected): 9 | return checkpw(sent.encode('utf-8'), expected.encode('utf-8')) 10 | -------------------------------------------------------------------------------- /run.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from app import create_app 3 | 4 | app = create_app() 5 | 6 | 7 | if __name__ == "__main__": 8 | app.run(port=5000) 9 | else: 10 | gunicorn_logger = logging.getLogger('gunicorn.error') 11 | app.logger.handlers = gunicorn_logger.handlers 12 | app.logger.setLevel(gunicorn_logger.level) 13 | -------------------------------------------------------------------------------- /app/helpers/parsers.py: -------------------------------------------------------------------------------- 1 | from flask_restplus import reqparse 2 | 3 | access_token_parser = reqparse.RequestParser() 4 | access_token_parser.add_argument('Authorization', help='Bearer ', location='headers') 5 | 6 | refresh_parser = reqparse.RequestParser() 7 | refresh_parser.add_argument('Authorization', help='Bearer ', location='headers') 8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*.py] 4 | indent_size = 4 5 | indent_style = space 6 | insert_final_newline = true 7 | trim_trailing_whitespace = true 8 | end_of_line = lf 9 | charset = utf-8 10 | 11 | [*.js] 12 | indent_size = 2 13 | indent_style = space 14 | trim_trailing_whitespace = true 15 | 16 | [*.json] 17 | indent_size = 2 18 | insert_final_newline = ignore 19 | -------------------------------------------------------------------------------- /tests/unit/helpers_test/test_password.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from app.helpers import check_password, encrypt_password 3 | 4 | 5 | @pytest.mark.parametrize('sent', [ 6 | ('test'), 7 | ('changeme'), 8 | ('1234123'), 9 | ]) 10 | def test_if_check_password_and_encrypt_password_works_properly(sent): 11 | expected = encrypt_password(sent) 12 | assert check_password(sent, expected) 13 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | tests/ 2 | *.md 3 | __pycache__/ 4 | *.py[cod] 5 | *$py.class 6 | *.egg 7 | .coverage 8 | .coverage.* 9 | .cache 10 | .pytest_cache/ 11 | .env 12 | .venv 13 | env/ 14 | venv/ 15 | ENV/ 16 | env.bak/ 17 | venv.bak/ 18 | *.cfg 19 | pytest.* 20 | Dockerfile 21 | Makefile 22 | .editorconfig 23 | .dockerignore 24 | .gitignore 25 | .git 26 | imgs/ 27 | LICENSE 28 | .vscode/ 29 | .travis.yml 30 | imgs/ 31 | .history/ 32 | kubernetes/ -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | skipsdist = True 3 | envlist = py37, lint 4 | 5 | [testenv:py37] 6 | passenv = * 7 | deps = 8 | -rrequirements.txt 9 | commands = 10 | pytest {posargs:tests} -s -v -x -p no:warnings --no-cov-on-fail --cov {posargs:tests} 11 | 12 | [testenv:lint] 13 | deps = flake8 14 | commands = flake8 app tests run.py config.py 15 | 16 | [flake8] 17 | ignore = F403 18 | max-line-length = 140 19 | exclude = *.txt, *.ini, Dockerfile, *.md, LICENSE 20 | per-file-ignores = __init__.py:F401, E402 -------------------------------------------------------------------------------- /app/v1/resources/users/serializers.py: -------------------------------------------------------------------------------- 1 | from app.v1 import api 2 | from flask_restplus import fields 3 | 4 | 5 | user = api.model('User', { 6 | 'id': fields.String(readonly=True, description='User ID', attribute='_id.$oid'), 7 | 'username': fields.String(required=True, description='The Username'), 8 | 'email': fields.String(required=True, description='User Email') 9 | }) 10 | 11 | 12 | create_user = api.inherit('User Creation', user, { 13 | 'password': fields.String(required=True, description='User Password'), 14 | }) 15 | -------------------------------------------------------------------------------- /app/v1/resources/auth/serializers.py: -------------------------------------------------------------------------------- 1 | from app.v1 import api 2 | from flask_restplus import fields 3 | 4 | 5 | login = api.model('Login', { 6 | 'username': fields.String(required=True, description='Username'), 7 | 'password': fields.String(required=True, description='User Password') 8 | }) 9 | 10 | acc_token = api.model('JWT Access Token', { 11 | 'access_token': fields.String(description='Access Token') 12 | }) 13 | 14 | full_token = api.inherit('JWT Tokens', acc_token, { 15 | 'refresh_token': fields.String(description='Refresh Token'), 16 | }) 17 | -------------------------------------------------------------------------------- /app/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import Flask 2 | from flask_pymongo import PyMongo 3 | from flask_jwt_extended import JWTManager 4 | from flask_cors import CORS 5 | 6 | mongo = PyMongo() 7 | jwt = JWTManager() 8 | 9 | 10 | def create_app(): 11 | app = Flask(__name__) 12 | 13 | if app.config["ENV"] == "production": 14 | app.config.from_object("config.ProdConfig") 15 | else: 16 | app.config.from_object("config.DevConfig") 17 | 18 | mongo.init_app(app) 19 | jwt.init_app(app) 20 | 21 | from app.v1 import v1_blueprint 22 | app.register_blueprint(v1_blueprint) 23 | 24 | CORS(app) 25 | return app 26 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.7.4-alpine3.9 as base 2 | 3 | FROM base as builder 4 | 5 | RUN mkdir /install 6 | 7 | WORKDIR /install 8 | 9 | COPY requirements.txt /requirements.txt 10 | 11 | RUN apk --no-cache add \ 12 | build-base \ 13 | ca-certificates \ 14 | libffi-dev \ 15 | musl \ 16 | openssl \ 17 | openssl-dev \ 18 | zlib-dev 19 | 20 | RUN pip install --install-option="--prefix=/install" -r /requirements.txt 21 | 22 | FROM base 23 | 24 | COPY --from=builder /install /usr/local 25 | 26 | COPY . /app 27 | 28 | WORKDIR /app 29 | 30 | EXPOSE 5000 31 | 32 | CMD ["gunicorn", "-b :5000", "-w 3", "--access-logfile", "-", "--error-logfile", "-", "run:app"] 33 | -------------------------------------------------------------------------------- /app/v1/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint 2 | from flask_restplus import Api 3 | 4 | v1_blueprint = Blueprint('v1', __name__, url_prefix='/api/v1') 5 | 6 | authorizations = { 7 | 'Bearer Auth': { 8 | 'type': 'apiKey', 9 | 'in': 'header', 10 | 'name': 'Authorization' 11 | }, 12 | } 13 | 14 | api = Api(v1_blueprint, 15 | doc='/docs', 16 | title='Flask App', 17 | version='1.0', 18 | description='Flask RESTful API', 19 | security='Bearer Auth', 20 | authorizations=authorizations) 21 | 22 | from .resources.auth.login import api as auth_ns 23 | from .resources.users.user import api as user_ns 24 | 25 | api.add_namespace(auth_ns) 26 | api.add_namespace(user_ns) 27 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import json 2 | import pytest 3 | from app import create_app 4 | from flask_restplus import marshal 5 | from app.v1.resources.users.serializers import create_user 6 | 7 | 8 | @pytest.fixture(scope='session') 9 | def client(): 10 | app = create_app() 11 | app.config['TESTING'] = True 12 | client = app.test_client() 13 | yield client 14 | 15 | 16 | @pytest.fixture(scope='module') 17 | def user_list(): 18 | """ 19 | Reads the data from 'users.json' file, 20 | marshalling with the 'create_user' serializer. 21 | In this way, we can assert that our fake test 22 | will produce the same fields of our serializer. 23 | 24 | Yields: 25 | list: List of parsed users objects (dict). 26 | """ 27 | with open('tests/fake_data/users.json', 'r') as fp: 28 | data = json.loads(fp.read()) 29 | yield marshal(data, create_user) 30 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aniso8601==7.0.0 2 | atomicwrites==1.3.0 3 | attrs==19.1.0 4 | bcrypt==3.1.7 5 | certifi==2023.7.22 6 | cffi==1.15.1 7 | chardet==3.0.4 8 | charset-normalizer==2.1.1 9 | Click==7.0 10 | codecov==2.0.15 11 | coverage==4.5.4 12 | filelock==3.0.12 13 | Flask==2.3.2 14 | Flask-Cors==3.0.9 15 | Flask-JWT-Extended==3.22.0 16 | Flask-PyMongo==2.3.0 17 | flask-restplus==0.13.0 18 | gunicorn==19.9.0 19 | idna==2.8 20 | importlib-metadata==0.20 21 | itsdangerous==1.1.0 22 | Jinja2==2.11.3 23 | jsonschema==3.0.2 24 | MarkupSafe==1.1.1 25 | more-itertools==7.2.0 26 | packaging==19.1 27 | pluggy==0.12.0 28 | py==1.10.0 29 | pycparser==2.19 30 | PyJWT==2.4.0 31 | pymongo==3.9.0 32 | pyparsing==2.4.2 33 | pyrsistent==0.15.4 34 | pytest==5.1.2 35 | pytest-cov==2.7.1 36 | pytest-flask==0.15.0 37 | pytz==2019.2 38 | requests==2.31.0 39 | six==1.12.0 40 | toml==0.10.0 41 | tox==3.14.0 42 | urllib3==1.26.5 43 | virtualenv==16.7.5 44 | wcwidth==0.1.7 45 | Werkzeug==2.2.3 46 | zipp==0.6.0 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019 Álefe Silva 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import logging 4 | from logging.config import dictConfig 5 | 6 | logger = logging.getLogger(__name__) 7 | 8 | 9 | dictConfig({ 10 | "version": 1, 11 | "disable_existing_loggers": False, 12 | "formatters": { 13 | "simple": { 14 | "format": "[%(levelname)s] - [%(asctime)s] - %(name)s - %(message)s" 15 | } 16 | }, 17 | "handlers": { 18 | "console": { 19 | "class": "logging.StreamHandler", 20 | "level": "DEBUG", 21 | "formatter": "simple", 22 | } 23 | }, 24 | "loggers": { 25 | "werkzeug": { 26 | "level": "INFO", 27 | "handlers": ["console"], 28 | "propagate": False 29 | } 30 | }, 31 | "root": { 32 | "level": "DEBUG", 33 | "handlers": ["console"] 34 | } 35 | }) 36 | 37 | 38 | class BaseConfig: 39 | 40 | DEBUG = True 41 | RESTPLUS_VALIDATE = True 42 | JWT_CLAIMS_IN_REFRESH_TOKEN = True 43 | ERROR_INCLUDE_MESSAGE = False 44 | RESTPLUS_MASK_SWAGGER = False 45 | PROPAGATE_EXCEPTIONS = True 46 | 47 | try: 48 | MONGO_URI = os.environ['MONGO_URI'] 49 | JWT_SECRET_KEY = os.environ['JWT_SECRET_KEY'] 50 | except KeyError as key: 51 | logger.critical(f'{key} env var is missing !') 52 | sys.exit() 53 | 54 | 55 | class ProdConfig(BaseConfig): 56 | 57 | DEBUG = False 58 | 59 | 60 | class DevConfig(BaseConfig): 61 | pass 62 | -------------------------------------------------------------------------------- /app/v1/resources/users/models.py: -------------------------------------------------------------------------------- 1 | import json 2 | from app import mongo 3 | from flask_restplus import abort 4 | from bson.json_util import dumps 5 | from bson.objectid import ObjectId 6 | from app.helpers import encrypt_password 7 | 8 | 9 | class Users: 10 | 11 | def __init__(self): 12 | pass 13 | 14 | @staticmethod 15 | def get_all_users(): 16 | return json.loads(dumps(mongo.db.users.find().sort([('username', 1)]))) 17 | 18 | @staticmethod 19 | def get_user(id): 20 | user = mongo.db.users.find_one({'_id': ObjectId(id)}) 21 | if user: 22 | return json.loads(dumps(user)) 23 | return None 24 | 25 | @staticmethod 26 | def insert_user(user): 27 | if mongo.db.users.find_one({'username': user.get('username')}): 28 | abort(409, 'User already exists') 29 | 30 | user['password'] = encrypt_password(user.get('password', 'changeme')) 31 | if not mongo.db.users.insert_one(user).inserted_id: 32 | abort(422, 'Cannot create user') 33 | return json.loads(dumps(user)) 34 | 35 | @classmethod 36 | def update_user(cls, id, data): 37 | if not cls.get_user(id): 38 | abort(404, 'User not found') 39 | 40 | if mongo.db.users.update_one({'_id': ObjectId(id)}, {'$set': data}): 41 | return '', 204 42 | abort(422, 'No user updated') 43 | 44 | @classmethod 45 | def delete_user(cls, id): 46 | if mongo.db.users.delete_one({'_id': ObjectId(id)}).deleted_count: 47 | return '', 204 48 | abort(404, 'User not found') 49 | -------------------------------------------------------------------------------- /tests/integration/users/test_users_api.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import patch 2 | 3 | 4 | @patch('flask_jwt_extended.view_decorators.verify_jwt_in_request') 5 | def test_get_all_users_endpoint(mock_jwt_required, client): 6 | response = client.get('/api/v1/users/') 7 | assert response.status_code == 200 8 | 9 | 10 | def test_if_get_all_users_without_jwt_fails(client): 11 | response = client.get('/api/v1/users/') 12 | assert response.status_code == 401 13 | 14 | 15 | @patch('flask_jwt_extended.view_decorators.verify_jwt_in_request') 16 | def test_get_single_user_endpoint_with_fake_user(mock_jwt_required, client): 17 | response = client.get(f'/api/v1/users/54759eb3c090d83494e2d804') 18 | assert response.status_code == 404 19 | 20 | 21 | def test_if_get_single_users_without_jwt_fails(client): 22 | response = client.get(f'/api/v1/users/54759eb3c090d83494e2d804') 23 | assert response.status_code == 401 24 | 25 | 26 | @patch('flask_jwt_extended.view_decorators.verify_jwt_in_request') 27 | def test_create_and_delete_user_endpoints(mock_jwt_required, user_list, client): 28 | 29 | def create_user(user): 30 | user.pop('id') 31 | created = client.post('/api/v1/users/', json=user) 32 | assert created.status_code == 201 33 | 34 | conflict = client.post('/api/v1/users/', json=user) 35 | assert conflict.status_code == 409 36 | 37 | user.pop('password') 38 | user_id = created.json.pop('id') 39 | assert user == created.json 40 | 41 | bad = client.post('/api/v1/users/', json={'name': 'bla'}) 42 | assert bad.status_code == 400 43 | 44 | return user_id 45 | 46 | def delete_user(user_id): 47 | response = client.delete(f'/api/v1/users/{user_id}') 48 | assert response.status_code == 204 49 | response = client.delete(f'/api/v1/users/{user_id}') 50 | assert response.status_code == 404 51 | 52 | for user in user_list: 53 | user_id = create_user(user) 54 | delete_user(user_id) 55 | -------------------------------------------------------------------------------- /app/v1/resources/users/user.py: -------------------------------------------------------------------------------- 1 | from flask_restplus import Resource, Namespace 2 | from flask_jwt_extended import jwt_required 3 | from .serializers import user, create_user 4 | from .models import Users 5 | 6 | api = Namespace('users', 'Users Endpoint') 7 | 8 | 9 | @api.route('/') 10 | class UserList(Resource): 11 | 12 | @api.marshal_list_with(user) 13 | @jwt_required 14 | def get(self): 15 | """ 16 | Get all users 17 | """ 18 | return Users.get_all_users() 19 | 20 | @api.marshal_with(user, code=201) 21 | @api.expect(create_user) 22 | @api.doc(responses={ 23 | 400: 'Input payload validation failed', 24 | 401: 'Unauthorized', 25 | 409: 'User already exists', 26 | 422: 'Cannot create user', 27 | 500: 'Internal Server Error' 28 | }) 29 | @jwt_required 30 | def post(self): 31 | """ 32 | Creates a new user 33 | """ 34 | return Users.insert_user(api.payload), 201 35 | 36 | 37 | @api.route('/') 38 | class User(Resource): 39 | 40 | @api.marshal_with(user) 41 | @api.response(404, 'User not found') 42 | @api.doc(params={'id': 'User ID'}) 43 | @jwt_required 44 | def get(self, id): 45 | """ 46 | Get user by ID 47 | """ 48 | user = Users.get_user(id) 49 | if not user: 50 | api.abort(404, 'User not found') 51 | return user 52 | 53 | @api.doc(responses={ 54 | 204: 'No content', 55 | 401: 'Unauthorized', 56 | 404: 'User not found', 57 | 500: 'Internal Server Error' 58 | }, params={'id': 'User ID'}) 59 | @jwt_required 60 | def delete(self, id): 61 | """ 62 | Deletes user by ID 63 | """ 64 | return Users.delete_user(id) 65 | 66 | @api.expect(user) 67 | @api.doc(responses={ 68 | 204: 'No content', 69 | 400: 'Input payload validation failed', 70 | 401: 'Unauthorized', 71 | 422: 'No user updated', 72 | 500: 'Internal Server Error' 73 | }, params={'id': 'user ID'}) 74 | @jwt_required 75 | def put(self, id): 76 | """ 77 | Updates the user 78 | """ 79 | return Users.update_user(id, api.payload) 80 | -------------------------------------------------------------------------------- /app/v1/resources/auth/login.py: -------------------------------------------------------------------------------- 1 | from flask_restplus import Resource, Namespace 2 | from flask_jwt_extended import create_access_token, create_refresh_token, jwt_refresh_token_required, get_jwt_identity, get_jwt_claims 3 | from app import mongo, jwt 4 | from app.helpers import refresh_parser, check_password 5 | from .serializers import login, full_token, acc_token 6 | 7 | api = Namespace('auth', 'Authentication') 8 | 9 | 10 | @api.route('/login') 11 | class Login(Resource): 12 | 13 | @api.marshal_with(full_token) 14 | @api.expect(login) 15 | @api.doc(responses={ 16 | 200: 'Success', 17 | 400: 'Username or password is a required property', 18 | 401: 'Unauthorized' 19 | }, security=None) 20 | def post(self): 21 | """ 22 | Authentication endpoint 23 | """ 24 | username = api.payload.get('username') 25 | password = api.payload.get('password') 26 | 27 | user = mongo.db.users.find_one({'username': username}, {'_id': 0}) 28 | 29 | if not user: 30 | api.abort(401, 'Unauthorized') 31 | 32 | if not check_password(password, user.get('password')): 33 | api.abort(401, 'Unauthorized') 34 | 35 | access_token = create_access_token(identity=user) 36 | refresh_token = create_refresh_token(identity=user) 37 | return { 38 | 'access_token': access_token, 39 | 'refresh_token': refresh_token, 40 | 'name': user.get('name'), 41 | } 42 | 43 | 44 | @api.route('/refresh') 45 | class TokenRefresh(Resource): 46 | @jwt_refresh_token_required 47 | @api.expect(refresh_parser) 48 | @api.doc(responses={ 49 | 424: 'Invalid refresh token' 50 | }, security=None) 51 | @api.response(200, 'Success', acc_token) 52 | def post(self): 53 | """ 54 | Retrieve Access Token using Refresh Token 55 | """ 56 | claims = get_jwt_claims() 57 | claims['username'] = get_jwt_identity() 58 | access_token = create_access_token(identity=claims) 59 | return {'access_token': access_token} 60 | 61 | 62 | @jwt.user_claims_loader 63 | def add_claims_to_access_token(user): 64 | return {'privilege': user.get('privilege')} 65 | 66 | 67 | @jwt.user_identity_loader 68 | def user_identity_lookup(user): 69 | return user['username'] 70 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/python,visualstudiocode 3 | # Edit at https://www.gitignore.io/?templates=python,visualstudiocode 4 | 5 | ### Python ### 6 | # Byte-compiled / optimized / DLL files 7 | __pycache__/ 8 | *.py[cod] 9 | *$py.class 10 | 11 | # C extensions 12 | *.so 13 | 14 | # Distribution / packaging 15 | .Python 16 | build/ 17 | develop-eggs/ 18 | downloads/ 19 | eggs/ 20 | .eggs/ 21 | lib/ 22 | lib64/ 23 | parts/ 24 | sdist/ 25 | var/ 26 | wheels/ 27 | pip-wheel-metadata/ 28 | share/python-wheels/ 29 | *.egg-info/ 30 | .installed.cfg 31 | *.egg 32 | MANIFEST 33 | 34 | # PyInstaller 35 | # Usually these files are written by a python script from a template 36 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 37 | *.manifest 38 | *.spec 39 | 40 | # Installer logs 41 | pip-log.txt 42 | pip-delete-this-directory.txt 43 | 44 | # Unit test / coverage reports 45 | htmlcov/ 46 | .tox/ 47 | .nox/ 48 | .coverage 49 | .coverage.* 50 | .cache 51 | nosetests.xml 52 | coverage.xml 53 | *.cover 54 | .hypothesis/ 55 | .pytest_cache/ 56 | 57 | # Translations 58 | *.mo 59 | *.pot 60 | 61 | # Django stuff: 62 | *.log 63 | local_settings.py 64 | db.sqlite3 65 | 66 | # Flask stuff: 67 | instance/ 68 | .webassets-cache 69 | 70 | # Scrapy stuff: 71 | .scrapy 72 | 73 | # Sphinx documentation 74 | docs/_build/ 75 | 76 | # PyBuilder 77 | target/ 78 | 79 | # Jupyter Notebook 80 | .ipynb_checkpoints 81 | 82 | # IPython 83 | profile_default/ 84 | ipython_config.py 85 | 86 | # pyenv 87 | .python-version 88 | 89 | # pipenv 90 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 91 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 92 | # having no cross-platform support, pipenv may install dependencies that don’t work, or not 93 | # install all needed dependencies. 94 | #Pipfile.lock 95 | 96 | # celery beat schedule file 97 | celerybeat-schedule 98 | 99 | # SageMath parsed files 100 | *.sage.py 101 | 102 | # Environments 103 | .env 104 | .venv 105 | env/ 106 | venv/ 107 | ENV/ 108 | env.bak/ 109 | venv.bak/ 110 | 111 | # Spyder project settings 112 | .spyderproject 113 | .spyproject 114 | 115 | # Rope project settings 116 | .ropeproject 117 | 118 | # mkdocs documentation 119 | /site 120 | 121 | # mypy 122 | .mypy_cache/ 123 | .dmypy.json 124 | dmypy.json 125 | 126 | # Pyre type checker 127 | .pyre/ 128 | 129 | ### VisualStudioCode ### 130 | .vscode/* 131 | !.vscode/settings.json 132 | !.vscode/tasks.json 133 | !.vscode/launch.json 134 | !.vscode/extensions.json 135 | 136 | ### VisualStudioCode Patch ### 137 | # Ignore all local history of files 138 | .history 139 | 140 | .scannerwork/ 141 | 142 | # End of https://www.gitignore.io/api/python,visualstudiocode 143 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Flask Base 2 | [![Python](https://img.shields.io/badge/python-3.7-blue.svg)]() [![Python](https://img.shields.io/badge/python-3.6-blue.svg)]() [![MIT License](https://img.shields.io/badge/license-MIT-007EC7.svg?style=flat)](/LICENSE) 3 | 4 | My template base to build Flask RESTful APIs using [Flask RESTPlus](https://flask-restplus.readthedocs.io/en/stable/index.html), [JWT Extended](https://flask-jwt-extended.readthedocs.io/en/latest/) and [PyMongo](https://flask-pymongo.readthedocs.io/en/latest/). 5 | 6 | You can just clone this repo and start to create/customize your own RESTful API using this code as a template/example :) 7 | 8 | ## JWT, PyMongo... Do I need all of these??? 9 | 10 | __NO!__ You can remove JWT, PyMongo and Bcrypt, excluding all the references on the [requirements.txt](requirements.txt), [app](app/__init__.py), [config](config.py) and all the dowstream files that makes use of them. 11 | 12 | These _"extensions"_ and the _users_ endpoints are there just to help you if you need to implement all of the required boilerplate to work with JWT, PyMongo and so on. 13 | 14 | # Getting Started 15 | 16 | ## Installing 17 | 18 | To install the Flask Base you will need to: 19 | 20 | ``` 21 | git clone https://github.com/alefeans/flask-base.git && cd flask-base 22 | pip install -r requirements.txt 23 | ``` 24 | 25 | ## Usage 26 | 27 | ### Development 28 | 29 | ``` 30 | # only if you are using mongo and jwt 31 | export MONGO_URI="mongodb://:27017/" 32 | export JWT_SECRET_KEY="" 33 | python run.py 34 | ``` 35 | 36 | ## Docker 37 | 38 | 39 | ### Build 40 | 41 | ``` 42 | docker build -t flask-app . 43 | ``` 44 | 45 | ### Start a New Container 46 | 47 | ``` 48 | docker run -d \ 49 | --name flask-app \ 50 | -p 5000:5000 \ 51 | # if you are not using mongo and jwt, remove the two lines below keeping the 'flask-app' line 52 | -e MONGO_URI="mongodb://:27017/" \ 53 | -e JWT_SECRET_KEY="" \ 54 | flask-app 55 | ``` 56 | 57 | ## Swagger 58 | 59 | When the application starts, open your browser on `localhost:5000/api/v1/docs` to see the self-documented interactive API: 60 | 61 | ![](/imgs/swagger.png) 62 | 63 | 64 | ## Project Structure 65 | 66 | The project structure is based on the official [Scaling your project](https://flask-restplus.readthedocs.io/en/stable/scaling.html#multiple-apis-with-reusable-namespaces) doc with some adaptations (e.g `v1` folder to group versioned resources). 67 | 68 | 69 | ``` 70 | . 71 | ├── app 72 | │ ├── helpers 73 | │ │ ├── __init__.py 74 | │ │ ├── parsers.py 75 | │ │ └── password.py 76 | │ ├── __init__.py 77 | │ └── v1 78 | │ ├── __init__.py 79 | │ └── resources 80 | │ ├── auth 81 | │ │ ├── __init__.py 82 | │ │ ├── login.py 83 | │ │ └── serializers.py 84 | │ ├── __init__.py 85 | │ └── users 86 | │ ├── __init__.py 87 | │ ├── models.py 88 | │ ├── serializers.py 89 | │ └── user.py 90 | ├── config.py 91 | ├── Dockerfile 92 | ├── LICENSE 93 | ├── README.md 94 | ├── requirements.txt 95 | ├── run.py 96 | ├── tests 97 | │ ├── conftest.py 98 | │ ├── fake_data 99 | │ │ └── users.json 100 | │ ├── __init__.py 101 | │ ├── integration 102 | │ │ ├── __init__.py 103 | │ │ └── users 104 | │ │ ├── __init__.py 105 | │ │ └── test_users_api.py 106 | │ └── unit 107 | │ ├── helpers_test 108 | │ │ ├── __init__.py 109 | │ │ └── test_password.py 110 | │ ├── __init__.py 111 | │ └── users 112 | │ ├── __init__.py 113 | │ └── test_users.py 114 | └── tox.ini 115 | 116 | ``` 117 | 118 | ### Folders 119 | 120 | * `app` - All the RESTful API implementation is here. 121 | * `app/helpers` - Useful helpers for all modules. 122 | * `app/v1` - Resource groupment for all `v1` [Namespaces](https://flask-restplus.readthedocs.io/en/stable/scaling.html#multiple-namespaces). 123 | * `app/v1/resources` - All `v1` resources are implemented here. 124 | * `tests/unit` - Unit tests modules executed on the CI/CD pipeline. 125 | * `tests/integration` - Integration tests modules executed using a fake database on the CI/CD pipeline. 126 | * `tests/fake_data` - Fake data files ("fixtures"). 127 | 128 | ### Files 129 | 130 | * `app/__init__.py` - The Flask Application factory (`create_app()`) and its configuration is done here. Your [Blueprints](https://flask-restplus.readthedocs.io/en/stable/scaling.html#use-with-blueprints) are also registered here. 131 | * `app/v1/__init__.py` - The Flask RESTPlus API is created here with the versioned Blueprint (e.g `v1`). Your [Namespaces](https://flask-restplus.readthedocs.io/en/stable/scaling.html#multiple-namespaces) are registered here. 132 | * `config.py` - Config file for envs, global config vars and so on. 133 | * `Dockerfile` - Dockerfile used to build a Docker image (using [Docker Multistage Build](https://docs.docker.com/develop/develop-images/multistage-build/)) 134 | * `LICENSE` - MIT License, i.e. you are free to do whatever you want with the given code with no limits. 135 | * `tox.ini` - Config file for tests using [Tox](https://tox.readthedocs.io/en/latest/index.html). 136 | * `.dockerignore` - Lists files and directories which should be ignored by the Docker build process. 137 | * `.gitignore` - Lists files and directories which should not be added to git repository. 138 | * `requirements.txt` - All project dependencies. 139 | * `run.py` - The Application entrypoint. 140 | * `conftest.py` - Common pytest [fixtures](https://docs.pytest.org/en/latest/fixture.html). 141 | 142 | 143 | ### API Versioning 144 | 145 | If you need to create another API version (like `/api/v2`), follow these steps: 146 | 147 | First, create your `v2` API structure folder: 148 | 149 | ``` 150 | mkdir app/v2 151 | touch app/v2/__init__.py 152 | ``` 153 | 154 | Inside your `app/v2/__init__.py` create your Blueprint: 155 | 156 | ``` 157 | from flask import Blueprint 158 | from flask_restplus import Api 159 | 160 | v2_blueprint = Blueprint('v2', __name__, url_prefix='/api/v2') 161 | 162 | api = Api(v2_blueprint, 163 | doc='/docs', 164 | title='Flask App', 165 | version='2.0', 166 | description='Flask RESTful API V2') 167 | ``` 168 | 169 | Create your resources and namespaces inside `app/v2/resources` (like the `app/v1/resources`) and register them: 170 | 171 | ``` 172 | # app/v2/__init__.py 173 | 174 | from flask import Blueprint 175 | from flask_restplus import Api 176 | 177 | v2_blueprint = Blueprint('v2', __name__, url_prefix='/api/v2') 178 | 179 | api = Api(v2_blueprint, 180 | doc='/docs', 181 | title='Flask App', 182 | version='2.0', 183 | description='Flask RESTful API V2') 184 | 185 | 186 | # Fictious resource example 187 | from .resources.auth.login import api as auth_ns 188 | api.add_namespace(auth_ns) 189 | 190 | ``` 191 | 192 | And finally, register your Blueprint with the Flask Application: 193 | 194 | ``` 195 | # app/__init__.py 196 | 197 | # config code... 198 | from app.v1 import v1_blueprint 199 | app.register_blueprint(v1_blueprint) 200 | 201 | from app.v2 import v2_blueprint 202 | app.register_blueprint(v2_blueprint) 203 | 204 | ``` 205 | 206 | Now you have your new endpoints with the base path `/api/v2` :) ! 207 | 208 | OBS: Your swagger docs for this new API version will be under this base path too. Ex: `localhost:5000/api/v2/docs`. 209 | 210 | ## Tests 211 | 212 | To run the automated tests: 213 | 214 | ``` 215 | # rull all tests stages (with '-q' for a better output) 216 | tox -q 217 | 218 | # run unit tests with lint stage 219 | tox -q -- tests/unit 220 | 221 | # run integration tests with lint stage 222 | tox -q -- tests/unit 223 | 224 | # run only lint stage 225 | tox -q -e lint 226 | 227 | # skip lint stage (if u want integration, just modify the directory after '--') 228 | tox -q -e py37 -- tests/unit 229 | ``` 230 | 231 | ## License 232 | 233 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. 234 | --------------------------------------------------------------------------------