Skip to content

Commit 92d9847

Browse files
committed
Created basic project
1 parent b85d04a commit 92d9847

21 files changed

+546
-1
lines changed

.coveragerc

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
[run]
2+
include=
3+
*project/*
4+
omit =
5+
venv/*

.gitignore

+6
Original file line numberDiff line numberDiff line change
@@ -99,3 +99,9 @@ ENV/
9999

100100
# mypy
101101
.mypy_cache/
102+
103+
.idea
104+
105+
pylintReport.txt
106+
db.sqlite3
107+
_build

.travis.yml

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
language: python
2+
sudo: false
3+
cache: false
4+
python:
5+
- '3.6'
6+
install:
7+
- pip install -r requirements-tests.txt
8+
9+
script:
10+
- coverage erase
11+
- coverage run -m unittest
12+
after_success:
13+
- coverage combine
14+
- coveralls
15+
16+
notifications:
17+
email:
18+

Dockerfile

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
FROM python:3.6.4-alpine3.7
2+
3+
RUN apk add --update curl gcc g++ libffi-dev openssl-dev python3-dev \
4+
&& rm -rf /var/cache/apk/*
5+
RUN ln -s /usr/include/locale.h /usr/include/xlocale.h
6+
7+
ENV PYTHONUNBUFFERED=1 ENVIRONMENT=pre APP_HOME=/microservice/
8+
9+
RUN mkdir $APP_HOME
10+
WORKDIR $APP_HOME
11+
ADD requirement*.txt $APP_HOME
12+
RUN pip install -r requirements-docker.txt
13+
ADD . $APP_HOME
14+
15+
EXPOSE 5000
16+
17+
CMD ["gunicorn", "--worker-class", "eventlet", "--workers", "8", "--log-level", "INFO", "--bind", "0.0.0.0:5000", "manage:app"]

README.md

+6-1
Original file line numberDiff line numberDiff line change
@@ -1 +1,6 @@
1-
# oauth
1+
# oauth
2+
OAuth with JWT Python Microservices
3+
4+
[![Build Status](https://travis-ci.org/python-microservices/oauth.svg?branch=master)](https://travis-ci.org/python-microservices/oauth)
5+
[![Coverage Status](https://coveralls.io/repos/github/python-microservices/oauth/badge.svg?branch=master)](https://coveralls.io/github/python-microservices/oauth?branch=master)
6+
[![Requirements Status](https://requires.io/github/python-microservices/oauth/requirements.svg?branch=master)](https://requires.io/github/python-microservices/oauth/requirements/?branch=master)

manage.py

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# encoding: utf-8
2+
from flask_script import Manager
3+
4+
from project import create_app
5+
6+
app, db = create_app()
7+
8+
manager = Manager(app)
9+
10+
11+
@manager.command
12+
def create_db():
13+
"""Creates the db tables."""
14+
db.create_all()
15+
16+
17+
@manager.command
18+
def drop_db():
19+
"""Drops the db tables."""
20+
db.drop_all()
21+
22+
23+
if __name__ == '__main__':
24+
manager.run()

project/__init__.py

+96
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
# encoding: utf-8
2+
3+
import os
4+
5+
from flasgger import Swagger
6+
from flask import Flask
7+
8+
from project.config import CONFIG
9+
10+
__author__ = "Alberto Vara"
11+
__email__ = "[email protected]"
12+
__version__ = "0.0.1"
13+
14+
ENVIRONMENT = os.environ.get("ENVIRONMENT", "default")
15+
16+
SWAGGER_CONFIG = {
17+
"headers": [
18+
],
19+
"specs": [
20+
{
21+
"endpoint": 'apispec_1',
22+
"route": '{application_root}/apispec_1.json',
23+
"rule_filter": lambda rule: True, # all in
24+
"model_filter": lambda tag: True, # all in
25+
}
26+
],
27+
"info": {
28+
"title": "API ",
29+
"description": "API para...",
30+
"contact": {
31+
"responsibleOrganization": "ME",
32+
"responsibleDeveloper": "Me",
33+
"email": "[email protected]",
34+
},
35+
"version": "0.0.1"
36+
},
37+
"securityDefinitions": {
38+
"APIKeyHeader": {"type": "apiKey", "name": "Authorization", "in": "header"},
39+
},
40+
"static_url_path": "{application_root}/flasgger_static",
41+
"swagger_ui": True,
42+
"uiversion": 2,
43+
"specs_route": "/apidocs/",
44+
"basePath": "{application_root}"
45+
}
46+
47+
48+
class PrefixMiddleware(object):
49+
50+
def __init__(self, app, prefix=''):
51+
self.app = app
52+
self.prefix = prefix
53+
54+
def __call__(self, environ, start_response):
55+
56+
if environ['PATH_INFO'].startswith(self.prefix):
57+
environ['PATH_INFO'] = environ['PATH_INFO'][len(self.prefix):]
58+
environ['SCRIPT_NAME'] = self.prefix
59+
return self.app(environ, start_response)
60+
else:
61+
start_response('404', [('Content-Type', 'text/plain')])
62+
return ["This url does not belong to the app.".encode()]
63+
64+
65+
def create_app():
66+
from project.models import db
67+
from project.views import views_bp as views_blueprint
68+
from project.views.oauth import jwt, bcrypt
69+
environment = os.environ.get("ENVIRONMENT", "default")
70+
71+
app = Flask(__name__)
72+
app.config.from_object(CONFIG[environment])
73+
app.wsgi_app = PrefixMiddleware(app.wsgi_app, prefix=app.config["APPLICATION_ROOT"])
74+
75+
db.init_app(app)
76+
bcrypt.init_app(app)
77+
jwt.init_app(app)
78+
79+
SWAGGER_CONFIG["specs"][0]["route"] = SWAGGER_CONFIG["specs"][0]["route"].format(
80+
application_root=app.config["APPLICATION_ROOT"]
81+
)
82+
SWAGGER_CONFIG["static_url_path"] = SWAGGER_CONFIG["static_url_path"].format(
83+
application_root=app.config["APPLICATION_ROOT"]
84+
)
85+
SWAGGER_CONFIG["specs_route"] = SWAGGER_CONFIG["specs_route"].format(
86+
application_root=app.config["APPLICATION_ROOT"]
87+
)
88+
SWAGGER_CONFIG["basePath"] = SWAGGER_CONFIG["basePath"].format(
89+
application_root=app.config["APPLICATION_ROOT"]
90+
)
91+
Swagger(app, config=SWAGGER_CONFIG)
92+
93+
app.register_blueprint(views_blueprint)
94+
with app.test_request_context():
95+
db.create_all()
96+
return app, db

project/config.py

+53
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# coding=utf-8
2+
import os
3+
4+
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
5+
6+
7+
class Config:
8+
"""Default configuration for all environments"""
9+
10+
DEBUG = False
11+
TESTING = False
12+
APP_NAME = "Oauth"
13+
APPLICATION_ROOT = "/oauth"
14+
SQLALCHEMY_TRACK_MODIFICATIONS = True
15+
SECRET_KEY = os.environ.get("SECRET_KEY") or "gjr39dkjn344_!67#"
16+
17+
18+
class TestConfig(Config):
19+
"""Configuration to run tests"""
20+
21+
DEBUG = True
22+
TESTING = True
23+
DATABASE = os.path.join(BASE_DIR, "db_test.sqlite3")
24+
SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(BASE_DIR, "db_test.sqlite3")
25+
26+
27+
class DevConfig(Config):
28+
"""Configuration to run in local environments"""
29+
30+
DEBUG = True
31+
TESTING = True
32+
SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(BASE_DIR, "db.sqlite3")
33+
34+
35+
class PreConfig(Config):
36+
"""Configuration to run with docker and kubernetes in Preproduction"""
37+
SQLALCHEMY_TRACK_MODIFICATIONS = False
38+
SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(BASE_DIR, "db.sqlite3")
39+
40+
41+
class ProdConfig(Config):
42+
"""Configuration to run with docker and kubernetes in Production"""
43+
SQLALCHEMY_TRACK_MODIFICATIONS = False
44+
SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(BASE_DIR, "prod.db")
45+
46+
47+
CONFIG = {
48+
"test": TestConfig,
49+
"dev": DevConfig,
50+
"pre": PreConfig,
51+
"prod": ProdConfig,
52+
"default": DevConfig
53+
}

project/models/__init__.py

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# encoding: utf-8
2+
from __future__ import absolute_import, print_function, unicode_literals
3+
4+
from flask_sqlalchemy import SQLAlchemy
5+
6+
db = SQLAlchemy()

project/models/models.py

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# encoding: utf-8
2+
from __future__ import absolute_import, print_function, unicode_literals
3+
4+
import datetime
5+
6+
from flask_login import UserMixin
7+
from sqlalchemy import Column, Integer, String, Boolean
8+
9+
from project.models import db
10+
11+
12+
class User(db.Model, UserMixin):
13+
__tablename__ = 'auth_user'
14+
date_joined = db.Column(db.DateTime, default=datetime.datetime.utcnow())
15+
16+
id = Column(Integer, primary_key=True, autoincrement=True)
17+
email = Column(String, default="")
18+
first_name = Column(String, default="")
19+
last_name = Column(String, default="")
20+
username = Column(String, nullable=False)
21+
password = Column(String, nullable=False)
22+
is_superuser = Column(Boolean, default=False)
23+
is_staff = Column(Boolean, default=False)
24+
is_active = Column(Boolean, default=True)
25+
26+
def __init__(self, username, password):
27+
from project.views.oauth import bcrypt
28+
self.username = username
29+
self.password = bcrypt.generate_password_hash(
30+
password, 13
31+
).decode()

project/tests/__init__.py

Whitespace-only changes.

project/tests/test_views.py

+64
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import json
2+
import os
3+
import unittest
4+
from typing import Dict, List, Union, Text
5+
6+
from project import create_app
7+
from project.models.models import User, db
8+
9+
def _format_response(response: Text = "") -> Union[List, Dict]:
10+
return json.loads(response)
11+
12+
13+
class FlaskrTestCase(unittest.TestCase):
14+
15+
def setUp(self):
16+
os.environ["ENVIRONMENT"] = "test"
17+
self.app, self.db = create_app()
18+
self.base_url = self.app.config["APPLICATION_ROOT"]
19+
self.client = self.app.test_client()
20+
21+
def tearDown(self):
22+
os.unlink(self.app.config['DATABASE'])
23+
24+
def _create_user(self, username, password):
25+
with self.app.test_request_context():
26+
user = User(
27+
username=username,
28+
password=password
29+
)
30+
# insert the user
31+
db.session.add(user)
32+
db.session.commit()
33+
return user.id
34+
35+
def test_home(self):
36+
response = self.client.get('/')
37+
self.assertEqual(response.status_code, 404)
38+
39+
def test_protected_view_error(self):
40+
response = self.client.get('{base_url}/check-token'.format(base_url=self.base_url))
41+
self.assertEqual(response.status_code, 401)
42+
self.assertEqual(_format_response(response.data)["description"], "Request does not contain an access token")
43+
self.assertEqual(_format_response(response.data)["error"], "Authorization Required")
44+
45+
def test_login_error(self):
46+
response = self.client.post('{base_url}/login'.format(base_url=self.base_url),
47+
data={"username": "", "password": ""})
48+
self.assertEqual(response.status_code, 401)
49+
self.assertEqual(_format_response(response.data)["message"], 'Bad username and/or password')
50+
51+
def test_login_ok(self):
52+
username = "test"
53+
password = "1234"
54+
self._create_user(username, password)
55+
response = self.client.post('{base_url}/login'.format(base_url=self.base_url),
56+
data={"username": username, "password": password})
57+
self.assertEqual(response.status_code, 200)
58+
result = _format_response(response.data)["access_token"]
59+
self.assertGreater(len(result), 0)
60+
61+
# check protected
62+
response = self.client.get('{base_url}/check-token'.format(base_url=self.base_url),
63+
headers={'authorization': 'JWT {}'.format(result)})
64+
self.assertEqual(response.status_code, 200)

project/views/__init__.py

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# coding=utf-8
2+
from __future__ import unicode_literals, print_function, absolute_import, division
3+
4+
from flask import Blueprint
5+
6+
views_bp = Blueprint('views', __name__, static_url_path='/static')
7+
8+
from project.views import views

project/views/oauth.py

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
from flask_bcrypt import Bcrypt
2+
from flask_jwt import JWT
3+
4+
from project.models.models import User
5+
6+
bcrypt = Bcrypt()
7+
8+
9+
def authenticate(username, password):
10+
user = User.query.filter_by(username=username).first()
11+
if user and bcrypt.check_password_hash(
12+
user.password, password
13+
):
14+
return user
15+
16+
17+
def identity(payload):
18+
user_id = payload['identity']
19+
return User.query.filter_by(id=user_id).first()
20+
21+
22+
jwt = JWT(authentication_handler=authenticate, identity_handler=identity)

0 commit comments

Comments
 (0)