Restfulify account creation and login
parent
a92549b5a1
commit
8d151ba8c3
|
@ -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)
|
||||||
|
|
|
@ -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()
|
|
@ -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
|
|
|
@ -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):
|
|
@ -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')
|
|
@ -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')
|
|
@ -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')
|
|
@ -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()
|
|
@ -1,6 +0,0 @@
|
||||||
from flask import Blueprint
|
|
||||||
from .controllers import initialize_routes
|
|
||||||
|
|
||||||
accounts = Blueprint('accounts', __name__)
|
|
||||||
|
|
||||||
initialize_routes(accounts)
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue