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):
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')
app.register_blueprint(accounts, url_prefix='/accounts')
All blueprints should be imported in this method and then added
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)

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

View File

@ -24,7 +24,7 @@ class Account(db.Model):
def save(self):
"""
Stores current user to database
Stores this user to database
This may raise errors
"""
db.session.add(self)
@ -52,10 +52,24 @@ class Account(db.Model):
@staticmethod
def get_all():
"""
Get all stored accounts
"""
return Account.query.all()
@staticmethod
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()
def create_auth_token(self):
@ -92,15 +106,25 @@ class Role(db.Model):
self.display_name = str(name)
def save(self):
"""
Stores this role to database
This may raise errors
"""
db.session.add(self)
db.session.commit()
@staticmethod
def get_all():
"""
Get all stored roles
"""
return Role.query.all()
@staticmethod
def get(roleId):
"""
Get role with id = roleId
"""
return Role.query.filter_by(id=roleId)
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 .mqtt_client import MqttClient
devices = Blueprint('devices', __name__)
devices_bp = Blueprint('devices', __name__)
mqtt_client = None
@ -16,12 +16,12 @@ atexit.register(on_stop)
# Routes
@devices.route("/")
@devices_bp.route("/")
def hello():
return "Hello from devices!"
@devices.record
@devices_bp.record
def on_blueprint_setup(setup_state):
print('Blueprint setup')
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
aniso8601==3.0.0
bcrypt==3.1.4
cffi==1.11.5
click==6.7
Flask==0.12.2
Flask-API==1.0
Flask-Bcrypt==0.7.1
Flask-JWT==0.3.2
Flask-Migrate==2.1.1
Flask-MQTT==1.0.3
Flask-RESTful==0.3.6
Flask-Script==2.0.6
Flask-SQLAlchemy==2.3.2
gunicorn==19.8.1
@ -21,6 +22,7 @@ pycparser==2.18
PyJWT==1.4.2
python-dateutil==2.7.2
python-editor==1.0.3
pytz==2018.4
six==1.11.0
SQLAlchemy==1.2.7
typing==3.6.4