feat(Global): Implemented validation on requests and error handling

This commit is contained in:
Luka 2021-11-02 07:54:07 +00:00
parent 38d94cd041
commit 927ef220cd
8 changed files with 136 additions and 34 deletions

1
.gitignore vendored
View file

@ -1,5 +1,6 @@
.venv .venv
.idea .idea
.vscode
__pycache__ __pycache__
*.pyc *.pyc
.DS_Store .DS_Store

27
app.py
View file

@ -1,19 +1,36 @@
from flask import Flask, jsonify from flask import Flask, jsonify
from flask_jwt_extended import JWTManager from flask_jwt_extended import JWTManager
from flask_cors import CORS from flask_cors import CORS
from jsonschema.exceptions import ValidationError
from werkzeug.exceptions import BadRequest
# These imports are required
from areas import api_v1 from areas import api_v1
# There imports are required
from areas import users from areas import users
from areas import apps from areas import apps
from areas import auth from areas import auth
from helpers import (
BadRequest,
KratosError,
bad_request_error,
validation_error,
kratos_error,
global_error,
)
from config import * from config import *
app = Flask(__name__) app = Flask(__name__)
cors = CORS(app) cors = CORS(app)
app.config['SECRET_KEY'] = SECRET_KEY app.config["SECRET_KEY"] = SECRET_KEY
app.register_blueprint(api_v1) app.register_blueprint(api_v1)
# Error handlers
app.register_error_handler(Exception, global_error)
app.register_error_handler(BadRequest, bad_request_error)
app.register_error_handler(ValidationError, validation_error)
app.register_error_handler(KratosError, kratos_error)
jwt = JWTManager(app) jwt = JWTManager(app)
@ -22,9 +39,9 @@ jwt = JWTManager(app)
@jwt.unauthorized_loader @jwt.unauthorized_loader
@jwt.expired_token_loader @jwt.expired_token_loader
def expired_token_callback(*args): def expired_token_callback(*args):
return jsonify({'errorMessage': 'Unauthorized'}), 401 return jsonify({"errorMessage": "Unauthorized"}), 401
@app.route('/') @app.route("/")
def index(): def index():
return 'Open App Stack API v1.0' return "Open App Stack API v1.0"

View file

@ -1,57 +1,56 @@
from flask import jsonify, request from flask import jsonify, request
from flask_jwt_extended import jwt_required from flask_jwt_extended import jwt_required
from flask_cors import cross_origin from flask_cors import cross_origin
from flask_expects_json import expects_json
from areas import api_v1 from areas import api_v1
from helpers.kratos_api import KratosApi from helpers import KratosApi
from .validation import schema
@api_v1.route('/users', methods=['GET']) @api_v1.route("/users", methods=["GET"])
@jwt_required() @jwt_required()
@cross_origin() @cross_origin()
def get_users(): def get_users():
res = KratosApi.get('/identities') res = KratosApi.get("/identities")
return jsonify(res.json()) return jsonify(res.json())
@api_v1.route('/users/<string:id>', methods=['GET'])
@api_v1.route("/users/<string:id>", methods=["GET"])
@jwt_required() @jwt_required()
@cross_origin() @cross_origin()
def get_user(id): def get_user(id):
res = KratosApi.get('/identities/{}'.format(id)) res = KratosApi.get("/identities/{}".format(id))
return jsonify(res.json()) return jsonify(res.json())
@api_v1.route('/users', methods=['POST']) @api_v1.route("/users", methods=["POST"])
@jwt_required() @jwt_required()
@cross_origin() @cross_origin()
@expects_json(schema)
def post_user(): def post_user():
data = request.get_json() data = request.get_json()
kratos_data = { kratos_data = {"schema_id": "default", "traits": data}
"schema_id": "default", res = KratosApi.post("/identities", kratos_data)
"traits": data
}
res = KratosApi.post('/identities', kratos_data)
return jsonify(res.json()), res.status_code return jsonify(res.json()), res.status_code
@api_v1.route('/users/<string:id>', methods=['PUT']) @api_v1.route("/users/<string:id>", methods=["PUT"])
@jwt_required() @jwt_required()
@cross_origin() @cross_origin()
@expects_json(schema)
def put_user(id): def put_user(id):
data = request.get_json() data = request.get_json()
kratos_data = { kratos_data = {"schema_id": "default", "traits": data}
"schema_id": "default", res = KratosApi.put("/identities/{}".format(id), kratos_data)
"traits": data
}
res = KratosApi.put('/identities/{}'.format(id), kratos_data)
return jsonify(res.json()), res.status_code return jsonify(res.json()), res.status_code
@api_v1.route('/users/<string:id>', methods=['DELETE']) @api_v1.route("/users/<string:id>", methods=["DELETE"])
@jwt_required() @jwt_required()
@cross_origin() @cross_origin()
def delete_user(id): def delete_user(id):
res = KratosApi.delete('/identities/{}'.format(id)) res = KratosApi.delete("/identities/{}".format(id))
if (res.status_code == 204): if res.status_code == 204:
return jsonify(), res.status_code return jsonify(), res.status_code
return jsonify(res.json()), res.status_code return jsonify(res.json()), res.status_code

14
areas/users/validation.py Normal file
View file

@ -0,0 +1,14 @@
import re
schema = {
"type": "object",
"properties": {
"email": {
"type": "string",
"description": "Email of the user",
"pattern": r"(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|\"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*\")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\.){3}(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9])|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])",
"minLength": 1,
}
},
"required": ["email"],
}

2
helpers/__init__.py Normal file
View file

@ -0,0 +1,2 @@
from .kratos_api import *
from .error_handler import *

34
helpers/error_handler.py Normal file
View file

@ -0,0 +1,34 @@
from flask import jsonify
from jsonschema import ValidationError
class KratosError(Exception):
pass
class BadRequest(Exception):
pass
def bad_request_error(e):
message = e.args[0] if e.args else "Bad request to the server."
return jsonify({"errorMessage": message})
def validation_error(e):
original_error = e.description
return (
jsonify({"errorMessage": "{} is not valid.".format(original_error.path[0])}),
400,
)
def kratos_error(e):
message = e.args[0] if e.args else "Failed to contant Kratos."
status_code = e.args[1] if e.args else 500
return jsonify({"errorMessage": message}), status_code
def global_error(e):
message = e.args[0] if e.args else "Something went wrong."
return jsonify({"errorMessage": message})

View file

@ -1,32 +1,55 @@
from logging import error
import requests import requests
from config import * from config import *
from .error_handler import KratosError
class KratosApi:
@staticmethod
def __handleError(res):
if res.status_code >= 400:
message = res.json()["error"]["message"]
raise KratosError(message, res.status_code)
class KratosApi():
@staticmethod @staticmethod
def get(url): def get(url):
try: try:
return requests.get('{}{}'.format(KRATOS_URL, url)) res = requests.get("{}{}".format(KRATOS_URL, url))
KratosApi.__handleError(res)
return res
except: except:
return "Failed to contact Kratos" raise KratosError()
@staticmethod @staticmethod
def post(url, data): def post(url, data):
try: try:
return requests.post('{}{}'.format(KRATOS_URL, url), json=data) res = requests.post("{}{}".format(KRATOS_URL, url), json=data)
KratosApi.__handleError(res)
return res
except KratosError as err:
raise err
except: except:
return "Failed to contact Kratos" raise KratosError()
@staticmethod @staticmethod
def put(url, data): def put(url, data):
try: try:
return requests.put('{}{}'.format(KRATOS_URL, url), json=data) res = requests.put("{}{}".format(KRATOS_URL, url), json=data)
KratosApi.__handleError(res)
return res
except KratosError as err:
raise err
except: except:
return "Failed to contact Kratos" raise KratosError()
@staticmethod @staticmethod
def delete(url): def delete(url):
try: try:
return requests.delete('{}{}'.format(KRATOS_URL, url)) res = requests.delete("{}{}".format(KRATOS_URL, url))
KratosApi.__handleError(res)
return res
except KratosError as err:
raise err
except: except:
return "Failed to contact Kratos" raise KratosError()

View file

@ -1,3 +1,5 @@
attrs==21.2.0
black==21.9b0
certifi==2021.10.8 certifi==2021.10.8
cffi==1.15.0 cffi==1.15.0
charset-normalizer==2.0.7 charset-normalizer==2.0.7
@ -5,15 +7,25 @@ click==8.0.3
cryptography==35.0.0 cryptography==35.0.0
Flask==2.0.2 Flask==2.0.2
Flask-Cors==3.0.10 Flask-Cors==3.0.10
flask-expects-json==1.6.0
Flask-JWT-Extended==4.3.1 Flask-JWT-Extended==4.3.1
gunicorn==20.1.0 gunicorn==20.1.0
idna==3.3 idna==3.3
install==1.3.4
itsdangerous==2.0.1 itsdangerous==2.0.1
Jinja2==3.0.2 Jinja2==3.0.2
jsonschema==4.1.2
MarkupSafe==2.0.1 MarkupSafe==2.0.1
mypy-extensions==0.4.3
pathspec==0.9.0
platformdirs==2.4.0
pycparser==2.20 pycparser==2.20
PyJWT==2.3.0 PyJWT==2.3.0
pyrsistent==0.18.0
regex==2021.10.23
requests==2.26.0 requests==2.26.0
six==1.16.0 six==1.16.0
tomli==1.2.2
typing-extensions==3.10.0.2
urllib3==1.26.7 urllib3==1.26.7
Werkzeug==2.0.2 Werkzeug==2.0.2