Restfulify account creation and login

master
esensar 2018-05-06 21:42:21 +02:00
parent a92549b5a1
commit 8d151ba8c3
13 changed files with 219 additions and 42 deletions

View File

@ -11,11 +11,23 @@ bcrypt = Bcrypt(app)
def setup_blueprints(app): def setup_blueprints(app):
from .mod_devices import devices """
from .mod_accounts import accounts Sets up all of the blueprints for application
app.register_blueprint(devices, url_prefix='/devices') All blueprints should be imported in this method and then added
app.register_blueprint(accounts, url_prefix='/accounts')
API blueprint should expose all resources, while other
blueprints expose other domain specific functionalities
They are exposed as blueprints just for consistency, otherwise
they are just simple python packages/modules
"""
from .devices import devices_bp
from .accounts import accounts_bp
from .api import api_bp
app.register_blueprint(devices_bp)
app.register_blueprint(accounts_bp)
app.register_blueprint(api_bp, url_prefix='/api')
setup_blueprints(app) setup_blueprints(app)

View File

@ -0,0 +1,51 @@
from app import bcrypt
from flask import Blueprint
from .models import Account
accounts_bp = Blueprint('accounts', __name__)
def create_account(username, email, password):
"""
Tries to create account with given parameters. Raises error on failure
:param username: Desired username for Account
:param email: Desired email for Account
:param password: Desired password for Account
:type username: string
:type email: string
:type password: string
:returns True if account is successfully created
:rtype Boolean
:raises ValueError if account already exists
"""
if not Account.exists_with_any_of(username=username, email=email):
pw_hash = bcrypt.generate_password_hash(password).decode('utf-8')
account = Account(username, pw_hash, email)
account.save()
return True
raise ValueError("Account with given parameters already exists")
def create_token(username, password):
"""
Tries to create token for account with given parameters.
Raises error on failure
:param username: username of Account
:param password: password of Account
:type username: string
:type password: string
:returns created token
:rtype string
:raises ValueError if credentials are invalid or account does not exist
"""
if not Account.exists(username=username):
raise ValueError("Invalid credentials")
account = Account.get(username=username)
if not bcrypt.check_password_hash(account.password, password):
raise ValueError("Invalid credentials")
return account.create_auth_token()

View File

@ -1,10 +1,10 @@
from app import bcrypt from app import bcrypt, status
from flask import request, jsonify from flask import request
from .models import Account from .models import Account
def initialize_routes(accounts): def initialize_routes(accounts):
@accounts.route("/", methods=['POST']) @accounts.route("", methods=['POST'])
def create_account(): def create_account():
print(request.data) print(request.data)
user = request.data.get('user') user = request.data.get('user')
@ -17,54 +17,48 @@ def initialize_routes(accounts):
password_hash, password_hash,
user.get('email')) user.get('email'))
acct.save() acct.save()
response = jsonify({ response = {
'status': 'success', 'status': 'success',
'message': 'Success!' 'message': 'Success!'
}) }
response.status_code = 200 return response, status.HTTP_200_OK
return response
else: else:
response = jsonify({ response = {
'status': 'error', 'status': 'error',
'message': 'User already exists!' 'message': 'User already exists!'
}) }
response.status_code = 422 return response, status.HTTP_422_UNPROCESSABLE_ENTITY
return response
@accounts.route("/token", methods=['POST']) @accounts.route("/token", methods=['POST'])
def create_token(): def create_token():
print(request.data) print(request.data)
user = request.data.get('user') user = request.data.get('user')
if not user: if not user:
response = jsonify({ response = {
'status': 'error', 'status': 'error',
'message': 'Invalid request' 'message': 'Invalid request'
}) }
response.status_code = 400 return response, status.HTTP_400_BAD_REQUEST
return response
if not Account.exists(username=user.get('username')): if not Account.exists(username=user.get('username')):
response = jsonify({ response = {
'status': 'error', 'status': 'error',
'message': 'Invalid credentials' 'message': 'Invalid credentials'
}) }
response.status_code = 422 return response, status.HTTP_401_UNAUTHORIZED
return response
account = Account.get(username=user.get('username')) account = Account.get(username=user.get('username'))
if not bcrypt.check_password_hash( if not bcrypt.check_password_hash(
account.password, user.get('password')): account.password, user.get('password')):
response = jsonify({ response = {
'status': 'error', 'status': 'error',
'message': 'Invalid credentials' 'message': 'Invalid credentials'
}) }
response.status_code = 422 return response, status.HTTP_401_UNAUTHORIZED
return response
response = jsonify({ response = {
'status': 'success', 'status': 'success',
'message': 'Successfully logged in', 'message': 'Successfully logged in',
'token': account.create_auth_token() 'token': account.create_auth_token()
}) }
response.status_code = 200 return response, status.HTTP_200_OK
return response

View File

@ -24,7 +24,7 @@ class Account(db.Model):
def save(self): def save(self):
""" """
Stores current user to database Stores this user to database
This may raise errors This may raise errors
""" """
db.session.add(self) db.session.add(self)
@ -52,10 +52,24 @@ class Account(db.Model):
@staticmethod @staticmethod
def get_all(): def get_all():
"""
Get all stored accounts
"""
return Account.query.all() return Account.query.all()
@staticmethod @staticmethod
def get(**kwargs): def get(**kwargs):
"""
Get accounts with given filters
Available filters:
* username
* email
* role_id
* id
* password (useless, but not forbidden)
"""
return Account.query.filter_by(**kwargs).first() return Account.query.filter_by(**kwargs).first()
def create_auth_token(self): def create_auth_token(self):
@ -92,15 +106,25 @@ class Role(db.Model):
self.display_name = str(name) self.display_name = str(name)
def save(self): def save(self):
"""
Stores this role to database
This may raise errors
"""
db.session.add(self) db.session.add(self)
db.session.commit() db.session.commit()
@staticmethod @staticmethod
def get_all(): def get_all():
"""
Get all stored roles
"""
return Role.query.all() return Role.query.all()
@staticmethod @staticmethod
def get(roleId): def get(roleId):
"""
Get role with id = roleId
"""
return Role.query.filter_by(id=roleId) return Role.query.filter_by(id=roleId)
def __repr__(self): def __repr__(self):

View File

@ -0,0 +1,11 @@
from flask import Blueprint
from flask_restful import Api
from .resources.account import AccountResource
from .resources.token import TokenResource
api_bp = Blueprint('api', __name__)
api = Api(api_bp)
# Add resources
api.add_resource(AccountResource, '/v1/accounts')
api.add_resource(TokenResource, '/v1/token')

View File

View File

@ -0,0 +1,43 @@
from flask_restful import Resource, reqparse, abort
import app.accounts as accounts
def user(user_dict):
"""
Type definition of user object required as a parameter
Required keys:
* username - string
* password - string
* email - string
:returns user dictionary with required keys
:rtype dict
:raises ValueError if parameter is not dict or is missing required keys
"""
if not isinstance(user_dict, dict):
raise ValueError("User should contain username, password and email")
if ('username' not in user_dict or
'password' not in user_dict or
'email' not in user_dict):
raise ValueError("User should contain username, password and email")
return user_dict
class AccountResource(Resource):
parser = reqparse.RequestParser(bundle_errors=True)
parser.add_argument('user', location='json', type=user,
help='User is not valid. Error: {error_msg}',
required=True)
def post(self):
try:
args = AccountResource.parser.parse_args()['user']
success = accounts.create_account(
args['username'],
args['email'],
args['password'])
if success:
return '', 201
except ValueError:
abort(422, message='Account already exists', status='error')

View File

@ -0,0 +1,46 @@
from flask_restful import Resource, reqparse, abort, fields, marshal_with
import app.accounts as accounts
def user(user_dict):
"""
Type definition of user object required as a parameter
Required keys:
* username - string
* password - string
:returns user dictionary with required keys
:rtype dict
:raises ValueError if parameter is not dict or is missing required keys
"""
if not isinstance(user_dict, dict):
raise ValueError("User should contain username, password and email")
if ('username' not in user_dict or
'password' not in user_dict):
raise ValueError("User should contain username, password and email")
return user_dict
class TokenResource(Resource):
parser = reqparse.RequestParser(bundle_errors=True)
parser.add_argument('user', location='json', type=user,
help='User is not valid. Error: {error_msg}',
required=True)
res_fields = {
'status': fields.String(default='Success'),
'token': fields.String
}
@marshal_with(res_fields)
def post(self):
try:
args = TokenResource.parser.parse_args()['user']
token = accounts.create_token(
args['username'],
args['password'])
if token:
return {'token': token}, 200
except ValueError:
abort(401, message='Invalid credentials', status='error')

View File

@ -2,7 +2,7 @@ import atexit
from flask import Blueprint from flask import Blueprint
from .mqtt_client import MqttClient from .mqtt_client import MqttClient
devices = Blueprint('devices', __name__) devices_bp = Blueprint('devices', __name__)
mqtt_client = None mqtt_client = None
@ -16,12 +16,12 @@ atexit.register(on_stop)
# Routes # Routes
@devices.route("/") @devices_bp.route("/")
def hello(): def hello():
return "Hello from devices!" return "Hello from devices!"
@devices.record @devices_bp.record
def on_blueprint_setup(setup_state): def on_blueprint_setup(setup_state):
print('Blueprint setup') print('Blueprint setup')
mqtt_client = MqttClient() mqtt_client = MqttClient()

View File

@ -1,6 +0,0 @@
from flask import Blueprint
from .controllers import initialize_routes
accounts = Blueprint('accounts', __name__)
initialize_routes(accounts)

View File

@ -1,13 +1,14 @@
alembic==0.9.9 alembic==0.9.9
aniso8601==3.0.0
bcrypt==3.1.4 bcrypt==3.1.4
cffi==1.11.5 cffi==1.11.5
click==6.7 click==6.7
Flask==0.12.2 Flask==0.12.2
Flask-API==1.0 Flask-API==1.0
Flask-Bcrypt==0.7.1 Flask-Bcrypt==0.7.1
Flask-JWT==0.3.2
Flask-Migrate==2.1.1 Flask-Migrate==2.1.1
Flask-MQTT==1.0.3 Flask-MQTT==1.0.3
Flask-RESTful==0.3.6
Flask-Script==2.0.6 Flask-Script==2.0.6
Flask-SQLAlchemy==2.3.2 Flask-SQLAlchemy==2.3.2
gunicorn==19.8.1 gunicorn==19.8.1
@ -21,6 +22,7 @@ pycparser==2.18
PyJWT==1.4.2 PyJWT==1.4.2
python-dateutil==2.7.2 python-dateutil==2.7.2
python-editor==1.0.3 python-editor==1.0.3
pytz==2018.4
six==1.11.0 six==1.11.0
SQLAlchemy==1.2.7 SQLAlchemy==1.2.7
typing==3.6.4 typing==3.6.4