API: init api w/ user management
This commit is contained in:
parent
40e7d35cb9
commit
844457b33c
2
.gitignore
vendored
2
.gitignore
vendored
@ -1,3 +1,5 @@
|
|||||||
# Created by .ignore support plugin (hsz.mobi)
|
# Created by .ignore support plugin (hsz.mobi)
|
||||||
.idea
|
.idea
|
||||||
.idea/vcs.xml
|
.idea/vcs.xml
|
||||||
|
__pycache__/
|
||||||
|
/mpwo_api/venv/
|
||||||
|
6
docs/db/mpwo.sql
Normal file
6
docs/db/mpwo.sql
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
CREATE DATABASE IF NOT EXISTS mpwo CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||||
|
CREATE DATABASE IF NOT EXISTS mpwo_test CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||||
|
CREATE USER 'mpwo'@'localhost' IDENTIFIED BY 'mpwo';
|
||||||
|
GRANT ALL PRIVILEGES ON `mpwo` . * TO 'mpwo'@'localhost';
|
||||||
|
GRANT ALL PRIVILEGES ON `mpwo_dev` . * TO 'mpwo'@'localhost';
|
||||||
|
FLUSH PRIVILEGES;
|
34
mpwo_api/mpwo_api/__init__.py
Normal file
34
mpwo_api/mpwo_api/__init__.py
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import logging
|
||||||
|
|
||||||
|
from flask import Flask
|
||||||
|
from flask_bcrypt import Bcrypt
|
||||||
|
from flask_sqlalchemy import SQLAlchemy
|
||||||
|
|
||||||
|
|
||||||
|
db = SQLAlchemy()
|
||||||
|
bcrypt = Bcrypt()
|
||||||
|
appLog = logging.getLogger('mpwo_api')
|
||||||
|
|
||||||
|
# instantiate the app
|
||||||
|
app = Flask(__name__)
|
||||||
|
|
||||||
|
# set config
|
||||||
|
with app.app_context():
|
||||||
|
app.config.from_object('mpwo_api.config.DevelopmentConfig')
|
||||||
|
|
||||||
|
# set up extensions
|
||||||
|
db.init_app(app)
|
||||||
|
bcrypt.init_app(app)
|
||||||
|
|
||||||
|
from .users.auth import auth_blueprint # noqa
|
||||||
|
from .users.users import users_blueprint # noqa
|
||||||
|
|
||||||
|
app.register_blueprint(users_blueprint, url_prefix='/api')
|
||||||
|
app.register_blueprint(auth_blueprint, url_prefix='/api')
|
||||||
|
|
||||||
|
if app.debug:
|
||||||
|
logging.getLogger('sqlalchemy').setLevel(logging.INFO)
|
||||||
|
logging.getLogger('sqlalchemy'
|
||||||
|
).handlers = logging.getLogger('werkzeug').handlers
|
||||||
|
logging.getLogger('sqlalchemy.orm').setLevel(logging.WARNING)
|
||||||
|
appLog.setLevel(logging.DEBUG)
|
32
mpwo_api/mpwo_api/config.py
Normal file
32
mpwo_api/mpwo_api/config.py
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
class BaseConfig:
|
||||||
|
"""Base configuration"""
|
||||||
|
DEBUG = False
|
||||||
|
TESTING = False
|
||||||
|
SQLALCHEMY_TRACK_MODIFICATIONS = False
|
||||||
|
BCRYPT_LOG_ROUNDS = 13
|
||||||
|
TOKEN_EXPIRATION_DAYS = 30
|
||||||
|
TOKEN_EXPIRATION_SECONDS = 0
|
||||||
|
|
||||||
|
|
||||||
|
class DevelopmentConfig(BaseConfig):
|
||||||
|
"""Development configuration"""
|
||||||
|
DEBUG = True
|
||||||
|
SQLALCHEMY_DATABASE_URI = \
|
||||||
|
'mysql+mysqldb://mpwo:mpwo@127.0.0.1:3306/mpwo'
|
||||||
|
SECRET_KEY = 'development key'
|
||||||
|
USERNAME = 'admin'
|
||||||
|
PASSWORD = 'default'
|
||||||
|
BCRYPT_LOG_ROUNDS = 4
|
||||||
|
|
||||||
|
|
||||||
|
class TestingConfig(BaseConfig):
|
||||||
|
"""Development configuration"""
|
||||||
|
DEBUG = True
|
||||||
|
SQLALCHEMY_DATABASE_URI = \
|
||||||
|
'mysql+mysqldb://mpwo:mpwo@127.0.0.1:3306/mpwo_test'
|
||||||
|
SECRET_KEY = 'test key'
|
||||||
|
USERNAME = 'admin'
|
||||||
|
PASSWORD = 'default'
|
||||||
|
BCRYPT_LOG_ROUNDS = 4
|
||||||
|
TOKEN_EXPIRATION_DAYS = 0
|
||||||
|
TOKEN_EXPIRATION_SECONDS = 3
|
0
mpwo_api/mpwo_api/tests/__init__.py
Normal file
0
mpwo_api/mpwo_api/tests/__init__.py
Normal file
17
mpwo_api/mpwo_api/tests/base.py
Normal file
17
mpwo_api/mpwo_api/tests/base.py
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
from flask_testing import TestCase
|
||||||
|
|
||||||
|
from mpwo_api import app, db
|
||||||
|
|
||||||
|
|
||||||
|
class BaseTestCase(TestCase):
|
||||||
|
def create_app(self):
|
||||||
|
app.config.from_object('mpwo_api.config.TestingConfig')
|
||||||
|
return app
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
db.create_all()
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
db.session.remove()
|
||||||
|
db.drop_all()
|
192
mpwo_api/mpwo_api/tests/test_auth.py
Normal file
192
mpwo_api/mpwo_api/tests/test_auth.py
Normal file
@ -0,0 +1,192 @@
|
|||||||
|
import json
|
||||||
|
import time
|
||||||
|
|
||||||
|
from mpwo_api.tests.base import BaseTestCase
|
||||||
|
from mpwo_api.tests.utils import add_user
|
||||||
|
|
||||||
|
|
||||||
|
class TestAuthBlueprint(BaseTestCase):
|
||||||
|
|
||||||
|
def test_user_registration(self):
|
||||||
|
with self.client:
|
||||||
|
response = self.client.post(
|
||||||
|
'/api/auth/register',
|
||||||
|
data=json.dumps(dict(
|
||||||
|
username='justatest',
|
||||||
|
email='test@test.com',
|
||||||
|
password='123456'
|
||||||
|
)),
|
||||||
|
content_type='application/json'
|
||||||
|
)
|
||||||
|
data = json.loads(response.data.decode())
|
||||||
|
self.assertTrue(data['status'] == 'success')
|
||||||
|
self.assertTrue(data['message'] == 'Successfully registered.')
|
||||||
|
self.assertTrue(data['auth_token'])
|
||||||
|
self.assertTrue(response.content_type == 'application/json')
|
||||||
|
self.assertEqual(response.status_code, 201)
|
||||||
|
|
||||||
|
def test_user_registration_user_already_exists(self):
|
||||||
|
add_user('test', 'test@test.com', 'test')
|
||||||
|
with self.client:
|
||||||
|
response = self.client.post(
|
||||||
|
'/api/auth/register',
|
||||||
|
data=json.dumps(dict(
|
||||||
|
username='test',
|
||||||
|
email='test@test.com',
|
||||||
|
password='test'
|
||||||
|
)),
|
||||||
|
content_type='application/json'
|
||||||
|
)
|
||||||
|
data = json.loads(response.data.decode())
|
||||||
|
self.assertTrue(data['status'] == 'error')
|
||||||
|
self.assertTrue(data['message'] == 'Sorry. That user already exists.')
|
||||||
|
self.assertTrue(response.content_type == 'application/json')
|
||||||
|
self.assertEqual(response.status_code, 400)
|
||||||
|
|
||||||
|
def test_user_registration_invalid_json(self):
|
||||||
|
with self.client:
|
||||||
|
response = self.client.post(
|
||||||
|
'/api/auth/register',
|
||||||
|
data=json.dumps(dict()),
|
||||||
|
content_type='application/json'
|
||||||
|
)
|
||||||
|
data = json.loads(response.data.decode())
|
||||||
|
self.assertEqual(response.status_code, 400)
|
||||||
|
self.assertIn('Invalid payload.', data['message'])
|
||||||
|
self.assertIn('error', data['status'])
|
||||||
|
|
||||||
|
def test_user_registration_invalid_json_keys_no_username(self):
|
||||||
|
with self.client:
|
||||||
|
response = self.client.post(
|
||||||
|
'/api/auth/register',
|
||||||
|
data=json.dumps(dict(email='test@test.com', password='test')),
|
||||||
|
content_type='application/json',
|
||||||
|
)
|
||||||
|
data = json.loads(response.data.decode())
|
||||||
|
self.assertEqual(response.status_code, 400)
|
||||||
|
self.assertIn('Invalid payload.', data['message'])
|
||||||
|
self.assertIn('error', data['status'])
|
||||||
|
|
||||||
|
def test_user_registration_invalid_json_keys_no_email(self):
|
||||||
|
with self.client:
|
||||||
|
response = self.client.post(
|
||||||
|
'/api/auth/register',
|
||||||
|
data=json.dumps(dict(
|
||||||
|
username='test', password='test')),
|
||||||
|
content_type='application/json',
|
||||||
|
)
|
||||||
|
data = json.loads(response.data.decode())
|
||||||
|
self.assertEqual(response.status_code, 400)
|
||||||
|
self.assertIn('Invalid payload.', data['message'])
|
||||||
|
self.assertIn('error', data['status'])
|
||||||
|
|
||||||
|
def test_user_registration_invalid_json_keys_no_password(self):
|
||||||
|
with self.client:
|
||||||
|
response = self.client.post(
|
||||||
|
'/api/auth/register',
|
||||||
|
data=json.dumps(dict(
|
||||||
|
username='test', email='test@test.com')),
|
||||||
|
content_type='application/json',
|
||||||
|
)
|
||||||
|
data = json.loads(response.data.decode())
|
||||||
|
self.assertEqual(response.status_code, 400)
|
||||||
|
self.assertIn('Invalid payload.', data['message'])
|
||||||
|
self.assertIn('error', data['status'])
|
||||||
|
|
||||||
|
def test_registered_user_login(self):
|
||||||
|
with self.client:
|
||||||
|
add_user('test', 'test@test.com', 'test')
|
||||||
|
response = self.client.post(
|
||||||
|
'/api/auth/login',
|
||||||
|
data=json.dumps(dict(
|
||||||
|
email='test@test.com',
|
||||||
|
password='test'
|
||||||
|
)),
|
||||||
|
content_type='application/json'
|
||||||
|
)
|
||||||
|
data = json.loads(response.data.decode())
|
||||||
|
self.assertTrue(data['status'] == 'success')
|
||||||
|
self.assertTrue(data['message'] == 'Successfully logged in.')
|
||||||
|
self.assertTrue(data['auth_token'])
|
||||||
|
self.assertTrue(response.content_type == 'application/json')
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
def test_no_registered_user_login(self):
|
||||||
|
with self.client:
|
||||||
|
response = self.client.post(
|
||||||
|
'/api/auth/login',
|
||||||
|
data=json.dumps(dict(
|
||||||
|
email='test@test.com',
|
||||||
|
password='test'
|
||||||
|
)),
|
||||||
|
content_type='application/json'
|
||||||
|
)
|
||||||
|
data = json.loads(response.data.decode())
|
||||||
|
self.assertTrue(data['status'] == 'error')
|
||||||
|
self.assertTrue(data['message'] == 'User does not exist.')
|
||||||
|
self.assertTrue(response.content_type == 'application/json')
|
||||||
|
self.assertEqual(response.status_code, 404)
|
||||||
|
|
||||||
|
def test_valid_logout(self):
|
||||||
|
add_user('test', 'test@test.com', 'test')
|
||||||
|
with self.client:
|
||||||
|
# user login
|
||||||
|
resp_login = self.client.post(
|
||||||
|
'/api/auth/login',
|
||||||
|
data=json.dumps(dict(
|
||||||
|
email='test@test.com',
|
||||||
|
password='test'
|
||||||
|
)),
|
||||||
|
content_type='application/json'
|
||||||
|
)
|
||||||
|
# valid token logout
|
||||||
|
response = self.client.get(
|
||||||
|
'/api/auth/logout',
|
||||||
|
headers=dict(
|
||||||
|
Authorization='Bearer ' + json.loads(
|
||||||
|
resp_login.data.decode()
|
||||||
|
)['auth_token']
|
||||||
|
)
|
||||||
|
)
|
||||||
|
data = json.loads(response.data.decode())
|
||||||
|
self.assertTrue(data['status'] == 'success')
|
||||||
|
self.assertTrue(data['message'] == 'Successfully logged out.')
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
def test_invalid_logout_expired_token(self):
|
||||||
|
add_user('test', 'test@test.com', 'test')
|
||||||
|
with self.client:
|
||||||
|
resp_login = self.client.post(
|
||||||
|
'/api/auth/login',
|
||||||
|
data=json.dumps(dict(
|
||||||
|
email='test@test.com',
|
||||||
|
password='test'
|
||||||
|
)),
|
||||||
|
content_type='application/json'
|
||||||
|
)
|
||||||
|
# invalid token logout
|
||||||
|
time.sleep(4)
|
||||||
|
response = self.client.get(
|
||||||
|
'/api/auth/logout',
|
||||||
|
headers=dict(
|
||||||
|
Authorization='Bearer ' + json.loads(
|
||||||
|
resp_login.data.decode()
|
||||||
|
)['auth_token']
|
||||||
|
)
|
||||||
|
)
|
||||||
|
data = json.loads(response.data.decode())
|
||||||
|
self.assertTrue(data['status'] == 'error')
|
||||||
|
self.assertTrue(
|
||||||
|
data['message'] == 'Signature expired. Please log in again.')
|
||||||
|
self.assertEqual(response.status_code, 401)
|
||||||
|
|
||||||
|
def test_invalid_logout(self):
|
||||||
|
with self.client:
|
||||||
|
response = self.client.get(
|
||||||
|
'/api/auth/logout',
|
||||||
|
headers=dict(Authorization='Bearer invalid'))
|
||||||
|
data = json.loads(response.data.decode())
|
||||||
|
self.assertTrue(data['status'] == 'error')
|
||||||
|
self.assertTrue(
|
||||||
|
data['message'] == 'Invalid token. Please log in again.')
|
||||||
|
self.assertEqual(response.status_code, 401)
|
83
mpwo_api/mpwo_api/tests/test_users.py
Normal file
83
mpwo_api/mpwo_api/tests/test_users.py
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
import json
|
||||||
|
|
||||||
|
from mpwo_api.tests.base import BaseTestCase
|
||||||
|
from mpwo_api.users.models import User
|
||||||
|
from mpwo_api.tests.utils import add_user
|
||||||
|
|
||||||
|
|
||||||
|
class TestUserService(BaseTestCase):
|
||||||
|
"""Tests for the Users Service."""
|
||||||
|
|
||||||
|
def test_users(self):
|
||||||
|
""" => Ensure the /ping route behaves correctly."""
|
||||||
|
response = self.client.get('/api/ping')
|
||||||
|
data = json.loads(response.data.decode())
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertIn('pong!', data['message'])
|
||||||
|
self.assertIn('success', data['status'])
|
||||||
|
|
||||||
|
def test_single_user(self):
|
||||||
|
"""=> Get single user details"""
|
||||||
|
user = add_user('test', 'test@test.com', 'test')
|
||||||
|
|
||||||
|
with self.client:
|
||||||
|
response = self.client.get(f'/api/users/{user.id}')
|
||||||
|
data = json.loads(response.data.decode())
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertIn('success', data['status'])
|
||||||
|
|
||||||
|
self.assertTrue('created_at' in data['data'])
|
||||||
|
self.assertIn('test', data['data']['username'])
|
||||||
|
self.assertIn('test@test.com', data['data']['email'])
|
||||||
|
|
||||||
|
def test_single_user_no_id(self):
|
||||||
|
"""=> Ensure error is thrown if an id is not provided."""
|
||||||
|
with self.client:
|
||||||
|
response = self.client.get(f'/api/users/blah')
|
||||||
|
data = json.loads(response.data.decode())
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 404)
|
||||||
|
self.assertIn('fail', data['status'])
|
||||||
|
self.assertIn('User does not exist', data['message'])
|
||||||
|
|
||||||
|
def test_single_user_wrong_id(self):
|
||||||
|
"""=> Ensure error is thrown if the id does not exist."""
|
||||||
|
with self.client:
|
||||||
|
response = self.client.get(f'/api/users/99999999999')
|
||||||
|
data = json.loads(response.data.decode())
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 404)
|
||||||
|
self.assertIn('fail', data['status'])
|
||||||
|
self.assertIn('User does not exist', data['message'])
|
||||||
|
|
||||||
|
def test_users_list(self):
|
||||||
|
"""=> Ensure get single user behaves correctly."""
|
||||||
|
add_user('test', 'test@test.com', 'test')
|
||||||
|
add_user('toto', 'toto@toto.com', 'toto')
|
||||||
|
with self.client:
|
||||||
|
response = self.client.get('/api/users')
|
||||||
|
data = json.loads(response.data.decode())
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertIn('success', data['status'])
|
||||||
|
|
||||||
|
self.assertEqual(len(data['data']['users']), 2)
|
||||||
|
self.assertTrue('created_at' in data['data']['users'][0])
|
||||||
|
self.assertTrue('created_at' in data['data']['users'][1])
|
||||||
|
self.assertIn('test', data['data']['users'][0]['username'])
|
||||||
|
self.assertIn('toto', data['data']['users'][1]['username'])
|
||||||
|
self.assertIn('test@test.com', data['data']['users'][0]['email'])
|
||||||
|
self.assertIn('toto@toto.com', data['data']['users'][1]['email'])
|
||||||
|
|
||||||
|
def test_encode_auth_token(self):
|
||||||
|
"""=> Ensure correct auth token generation"""
|
||||||
|
user = add_user('test', 'test@test.com', 'test')
|
||||||
|
auth_token = user.encode_auth_token(user.id)
|
||||||
|
self.assertTrue(isinstance(auth_token, bytes))
|
||||||
|
|
||||||
|
def test_decode_auth_token(self):
|
||||||
|
user = add_user('test', 'test@test.com', 'test')
|
||||||
|
auth_token = user.encode_auth_token(user.id)
|
||||||
|
self.assertTrue(isinstance(auth_token, bytes))
|
||||||
|
self.assertTrue(User.decode_auth_token(auth_token), user.id)
|
9
mpwo_api/mpwo_api/tests/utils.py
Normal file
9
mpwo_api/mpwo_api/tests/utils.py
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
from mpwo_api.users.models import User
|
||||||
|
from mpwo_api import db
|
||||||
|
|
||||||
|
|
||||||
|
def add_user(username, email, password):
|
||||||
|
user = User(username=username, email=email, password=password)
|
||||||
|
db.session.add(user)
|
||||||
|
db.session.commit()
|
||||||
|
return user
|
0
mpwo_api/mpwo_api/users/__init__.py
Normal file
0
mpwo_api/mpwo_api/users/__init__.py
Normal file
125
mpwo_api/mpwo_api/users/auth.py
Normal file
125
mpwo_api/mpwo_api/users/auth.py
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
from flask import Blueprint, jsonify, request
|
||||||
|
from sqlalchemy import exc, or_
|
||||||
|
|
||||||
|
from mpwo_api import bcrypt, db
|
||||||
|
|
||||||
|
from .models import User
|
||||||
|
|
||||||
|
auth_blueprint = Blueprint('auth', __name__)
|
||||||
|
|
||||||
|
|
||||||
|
@auth_blueprint.route('/auth/register', methods=['POST'])
|
||||||
|
def register_user():
|
||||||
|
# get post data
|
||||||
|
post_data = request.get_json()
|
||||||
|
if not post_data:
|
||||||
|
response_object = {
|
||||||
|
'status': 'error',
|
||||||
|
'message': 'Invalid payload.'
|
||||||
|
}
|
||||||
|
return jsonify(response_object), 400
|
||||||
|
username = post_data.get('username')
|
||||||
|
email = post_data.get('email')
|
||||||
|
password = post_data.get('password')
|
||||||
|
try:
|
||||||
|
# check for existing user
|
||||||
|
user = User.query.filter(
|
||||||
|
or_(User.username == username, User.email == email)).first()
|
||||||
|
if not user:
|
||||||
|
# add new user to db
|
||||||
|
new_user = User(
|
||||||
|
username=username,
|
||||||
|
email=email,
|
||||||
|
password=password
|
||||||
|
)
|
||||||
|
db.session.add(new_user)
|
||||||
|
db.session.commit()
|
||||||
|
# generate auth token
|
||||||
|
auth_token = new_user.encode_auth_token(new_user.id)
|
||||||
|
response_object = {
|
||||||
|
'status': 'success',
|
||||||
|
'message': 'Successfully registered.',
|
||||||
|
'auth_token': auth_token.decode()
|
||||||
|
}
|
||||||
|
return jsonify(response_object), 201
|
||||||
|
else:
|
||||||
|
response_object = {
|
||||||
|
'status': 'error',
|
||||||
|
'message': 'Sorry. That user already exists.'
|
||||||
|
}
|
||||||
|
return jsonify(response_object), 400
|
||||||
|
# handler errors
|
||||||
|
except (exc.IntegrityError, exc.OperationalError, ValueError) as e:
|
||||||
|
db.session.rollback()
|
||||||
|
response_object = {
|
||||||
|
'status': 'error',
|
||||||
|
'message': 'Invalid payload.'
|
||||||
|
}
|
||||||
|
return jsonify(response_object), 400
|
||||||
|
|
||||||
|
|
||||||
|
@auth_blueprint.route('/auth/login', methods=['POST'])
|
||||||
|
def login_user():
|
||||||
|
# get post data
|
||||||
|
post_data = request.get_json()
|
||||||
|
if not post_data:
|
||||||
|
response_object = {
|
||||||
|
'status': 'error',
|
||||||
|
'message': 'Invalid payload.'
|
||||||
|
}
|
||||||
|
return jsonify(response_object), 400
|
||||||
|
email = post_data.get('email')
|
||||||
|
password = post_data.get('password')
|
||||||
|
try:
|
||||||
|
# check for existing user
|
||||||
|
user = User.query.filter(User.email == email).first()
|
||||||
|
if user and bcrypt.check_password_hash(user.password, password):
|
||||||
|
# generate auth token
|
||||||
|
auth_token = user.encode_auth_token(user.id)
|
||||||
|
response_object = {
|
||||||
|
'status': 'success',
|
||||||
|
'message': 'Successfully logged in.',
|
||||||
|
'auth_token': auth_token.decode()
|
||||||
|
}
|
||||||
|
return jsonify(response_object), 200
|
||||||
|
else:
|
||||||
|
response_object = {
|
||||||
|
'status': 'error',
|
||||||
|
'message': 'User does not exist.'
|
||||||
|
}
|
||||||
|
return jsonify(response_object), 404
|
||||||
|
# handler errors
|
||||||
|
except (exc.IntegrityError, exc.OperationalError, ValueError) as e:
|
||||||
|
db.session.rollback()
|
||||||
|
response_object = {
|
||||||
|
'status': 'error',
|
||||||
|
'message': 'Try again'
|
||||||
|
}
|
||||||
|
return jsonify(response_object), 500
|
||||||
|
|
||||||
|
|
||||||
|
@auth_blueprint.route('/auth/logout', methods=['GET'])
|
||||||
|
def logout_user():
|
||||||
|
# get auth token
|
||||||
|
auth_header = request.headers.get('Authorization')
|
||||||
|
if auth_header:
|
||||||
|
auth_token = auth_header.split(" ")[1]
|
||||||
|
resp = User.decode_auth_token(auth_token)
|
||||||
|
if not isinstance(resp, str):
|
||||||
|
response_object = {
|
||||||
|
'status': 'success',
|
||||||
|
'message': 'Successfully logged out.'
|
||||||
|
}
|
||||||
|
return jsonify(response_object), 200
|
||||||
|
else:
|
||||||
|
response_object = {
|
||||||
|
'status': 'error',
|
||||||
|
'message': resp
|
||||||
|
}
|
||||||
|
return jsonify(response_object), 401
|
||||||
|
else:
|
||||||
|
response_object = {
|
||||||
|
'status': 'error',
|
||||||
|
'message': 'Provide a valid auth token.'
|
||||||
|
}
|
||||||
|
return jsonify(response_object), 403
|
64
mpwo_api/mpwo_api/users/models.py
Normal file
64
mpwo_api/mpwo_api/users/models.py
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import datetime
|
||||||
|
|
||||||
|
import jwt
|
||||||
|
from flask import current_app
|
||||||
|
|
||||||
|
from mpwo_api import bcrypt, db
|
||||||
|
|
||||||
|
|
||||||
|
class User(db.Model):
|
||||||
|
__tablename__ = "users"
|
||||||
|
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
|
||||||
|
username = db.Column(db.String(80), unique=True, nullable=False)
|
||||||
|
email = db.Column(db.String(120), unique=True, nullable=False)
|
||||||
|
password = db.Column(db.String(255), nullable=False)
|
||||||
|
created_at = db.Column(db.DateTime, nullable=False)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return '<User %r>' % self.username
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self, username, email, password,
|
||||||
|
created_at=datetime.datetime.utcnow()):
|
||||||
|
self.username = username
|
||||||
|
self.email = email
|
||||||
|
self.password = bcrypt.generate_password_hash(
|
||||||
|
password, current_app.config.get('BCRYPT_LOG_ROUNDS')
|
||||||
|
).decode()
|
||||||
|
self.created_at = created_at
|
||||||
|
|
||||||
|
def encode_auth_token(self, user_id):
|
||||||
|
"""Generates the auth token"""
|
||||||
|
try:
|
||||||
|
payload = {
|
||||||
|
'exp': datetime.datetime.utcnow() + datetime.timedelta(
|
||||||
|
days=current_app.config.get('TOKEN_EXPIRATION_DAYS'),
|
||||||
|
seconds=current_app.config.get('TOKEN_EXPIRATION_SECONDS')
|
||||||
|
),
|
||||||
|
'iat': datetime.datetime.utcnow(),
|
||||||
|
'sub': user_id
|
||||||
|
}
|
||||||
|
return jwt.encode(
|
||||||
|
payload,
|
||||||
|
current_app.config.get('SECRET_KEY'),
|
||||||
|
algorithm='HS256'
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
return e
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def decode_auth_token(auth_token):
|
||||||
|
"""
|
||||||
|
Decodes the auth token
|
||||||
|
:param auth_token: -
|
||||||
|
:return: integer|string
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
payload = jwt.decode(
|
||||||
|
auth_token,
|
||||||
|
current_app.config.get('SECRET_KEY'))
|
||||||
|
return payload['sub']
|
||||||
|
except jwt.ExpiredSignatureError:
|
||||||
|
return 'Signature expired. Please log in again.'
|
||||||
|
except jwt.InvalidTokenError:
|
||||||
|
return 'Invalid token. Please log in again.'
|
60
mpwo_api/mpwo_api/users/users.py
Normal file
60
mpwo_api/mpwo_api/users/users.py
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
from flask import Blueprint, jsonify
|
||||||
|
|
||||||
|
from .models import User
|
||||||
|
|
||||||
|
users_blueprint = Blueprint('users', __name__)
|
||||||
|
|
||||||
|
|
||||||
|
@users_blueprint.route('/users', methods=['GET'])
|
||||||
|
def get_users():
|
||||||
|
"""Get all users"""
|
||||||
|
users = User.query.all()
|
||||||
|
users_list = []
|
||||||
|
for user in users:
|
||||||
|
user_object = {
|
||||||
|
'id': user.id,
|
||||||
|
'username': user.username,
|
||||||
|
'email': user.email,
|
||||||
|
'created_at': user.created_at
|
||||||
|
}
|
||||||
|
users_list.append(user_object)
|
||||||
|
response_object = {
|
||||||
|
'status': 'success',
|
||||||
|
'data': {
|
||||||
|
'users': users_list
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return jsonify(response_object), 200
|
||||||
|
|
||||||
|
|
||||||
|
@users_blueprint.route('/users/<user_id>', methods=['GET'])
|
||||||
|
def get_single_user(user_id):
|
||||||
|
"""Get single user details"""
|
||||||
|
response_object = {
|
||||||
|
'status': 'fail',
|
||||||
|
'message': 'User does not exist'
|
||||||
|
}
|
||||||
|
try:
|
||||||
|
user = User.query.filter_by(id=int(user_id)).first()
|
||||||
|
if not user:
|
||||||
|
return jsonify(response_object), 404
|
||||||
|
else:
|
||||||
|
response_object = {
|
||||||
|
'status': 'success',
|
||||||
|
'data': {
|
||||||
|
'username': user.username,
|
||||||
|
'email': user.email,
|
||||||
|
'created_at': user.created_at
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return jsonify(response_object), 200
|
||||||
|
except ValueError:
|
||||||
|
return jsonify(response_object), 404
|
||||||
|
|
||||||
|
|
||||||
|
@users_blueprint.route('/ping', methods=['GET'])
|
||||||
|
def ping_pong():
|
||||||
|
return jsonify({
|
||||||
|
'status': 'success',
|
||||||
|
'message': 'pong!'
|
||||||
|
})
|
24
mpwo_api/requirements.txt
Normal file
24
mpwo_api/requirements.txt
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
bcrypt==3.1.4
|
||||||
|
cffi==1.11.2
|
||||||
|
click==6.7
|
||||||
|
flake8==3.5.0
|
||||||
|
flake8-isort==2.2.2
|
||||||
|
flake8-polyfill==1.0.1
|
||||||
|
Flask==0.12.2
|
||||||
|
Flask-Bcrypt==0.7.1
|
||||||
|
Flask-SQLAlchemy==2.3.2
|
||||||
|
Flask-Testing==0.6.2
|
||||||
|
isort==4.2.15
|
||||||
|
itsdangerous==0.24
|
||||||
|
Jinja2==2.10
|
||||||
|
MarkupSafe==1.0
|
||||||
|
mccabe==0.6.1
|
||||||
|
mysqlclient==1.3.12
|
||||||
|
pycodestyle==2.3.1
|
||||||
|
pycparser==2.18
|
||||||
|
pyflakes==1.6.0
|
||||||
|
PyJWT==1.5.3
|
||||||
|
six==1.11.0
|
||||||
|
SQLAlchemy==1.1.15
|
||||||
|
testfixtures==5.3.1
|
||||||
|
Werkzeug==0.13
|
44
mpwo_api/server.py
Normal file
44
mpwo_api/server.py
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
import unittest
|
||||||
|
|
||||||
|
from mpwo_api import app, db
|
||||||
|
from mpwo_api.users.models import User
|
||||||
|
|
||||||
|
|
||||||
|
@app.cli.command()
|
||||||
|
def recreate_db():
|
||||||
|
"""Recreates a database."""
|
||||||
|
db.drop_all()
|
||||||
|
db.create_all()
|
||||||
|
db.session.commit()
|
||||||
|
print('Database (re)creation done.')
|
||||||
|
|
||||||
|
|
||||||
|
@app.cli.command()
|
||||||
|
def seed_db():
|
||||||
|
"""Seeds the database."""
|
||||||
|
db.session.add(User(
|
||||||
|
username='test',
|
||||||
|
email='test@test.com',
|
||||||
|
password='test'
|
||||||
|
))
|
||||||
|
db.session.add(User(
|
||||||
|
username='test2',
|
||||||
|
email='test2@test.com',
|
||||||
|
password='test2'
|
||||||
|
))
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
|
||||||
|
@app.cli.command()
|
||||||
|
def test():
|
||||||
|
"""Runs the tests without code coverage."""
|
||||||
|
tests = unittest.TestLoader().discover(
|
||||||
|
'mpwo_api/tests', pattern='test*.py')
|
||||||
|
result = unittest.TextTestRunner(verbosity=2).run(tests)
|
||||||
|
if result.wasSuccessful():
|
||||||
|
return 0
|
||||||
|
return 1
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
app.run()
|
Loading…
Reference in New Issue
Block a user