From 927ef220cda2765d21838a5a41aad032003f70b9 Mon Sep 17 00:00:00 2001 From: Luka Date: Tue, 2 Nov 2021 07:54:07 +0000 Subject: [PATCH] feat(Global): Implemented validation on requests and error handling --- .gitignore | 1 + app.py | 27 +++++++++++++++++++++----- areas/users/users.py | 39 ++++++++++++++++++------------------- areas/users/validation.py | 14 +++++++++++++ helpers/__init__.py | 2 ++ helpers/error_handler.py | 34 ++++++++++++++++++++++++++++++++ helpers/kratos_api.py | 41 ++++++++++++++++++++++++++++++--------- requirements.txt | 12 ++++++++++++ 8 files changed, 136 insertions(+), 34 deletions(-) create mode 100644 areas/users/validation.py create mode 100644 helpers/__init__.py create mode 100644 helpers/error_handler.py diff --git a/.gitignore b/.gitignore index 68fb58f..75dcbca 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ .venv .idea +.vscode __pycache__ *.pyc .DS_Store \ No newline at end of file diff --git a/app.py b/app.py index 2c7d1cf..b69248b 100644 --- a/app.py +++ b/app.py @@ -1,19 +1,36 @@ from flask import Flask, jsonify from flask_jwt_extended import JWTManager from flask_cors import CORS +from jsonschema.exceptions import ValidationError +from werkzeug.exceptions import BadRequest +# These imports are required from areas import api_v1 -# There imports are required from areas import users from areas import apps from areas import auth + +from helpers import ( + BadRequest, + KratosError, + bad_request_error, + validation_error, + kratos_error, + global_error, +) from config import * app = Flask(__name__) cors = CORS(app) -app.config['SECRET_KEY'] = SECRET_KEY +app.config["SECRET_KEY"] = SECRET_KEY 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) @@ -22,9 +39,9 @@ jwt = JWTManager(app) @jwt.unauthorized_loader @jwt.expired_token_loader def expired_token_callback(*args): - return jsonify({'errorMessage': 'Unauthorized'}), 401 + return jsonify({"errorMessage": "Unauthorized"}), 401 -@app.route('/') +@app.route("/") def index(): - return 'Open App Stack API v1.0' + return "Open App Stack API v1.0" diff --git a/areas/users/users.py b/areas/users/users.py index 7ad285c..73d7a5d 100644 --- a/areas/users/users.py +++ b/areas/users/users.py @@ -1,57 +1,56 @@ from flask import jsonify, request from flask_jwt_extended import jwt_required from flask_cors import cross_origin +from flask_expects_json import expects_json 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() @cross_origin() def get_users(): - res = KratosApi.get('/identities') + res = KratosApi.get("/identities") return jsonify(res.json()) -@api_v1.route('/users/', methods=['GET']) + +@api_v1.route("/users/", methods=["GET"]) @jwt_required() @cross_origin() def get_user(id): - res = KratosApi.get('/identities/{}'.format(id)) + res = KratosApi.get("/identities/{}".format(id)) return jsonify(res.json()) -@api_v1.route('/users', methods=['POST']) +@api_v1.route("/users", methods=["POST"]) @jwt_required() @cross_origin() +@expects_json(schema) def post_user(): data = request.get_json() - kratos_data = { - "schema_id": "default", - "traits": data - } - res = KratosApi.post('/identities', kratos_data) + kratos_data = {"schema_id": "default", "traits": data} + res = KratosApi.post("/identities", kratos_data) return jsonify(res.json()), res.status_code -@api_v1.route('/users/', methods=['PUT']) +@api_v1.route("/users/", methods=["PUT"]) @jwt_required() @cross_origin() +@expects_json(schema) def put_user(id): data = request.get_json() - kratos_data = { - "schema_id": "default", - "traits": data - } - res = KratosApi.put('/identities/{}'.format(id), kratos_data) + kratos_data = {"schema_id": "default", "traits": data} + res = KratosApi.put("/identities/{}".format(id), kratos_data) return jsonify(res.json()), res.status_code -@api_v1.route('/users/', methods=['DELETE']) +@api_v1.route("/users/", methods=["DELETE"]) @jwt_required() @cross_origin() def delete_user(id): - res = KratosApi.delete('/identities/{}'.format(id)) - if (res.status_code == 204): + res = KratosApi.delete("/identities/{}".format(id)) + if res.status_code == 204: return jsonify(), res.status_code return jsonify(res.json()), res.status_code diff --git a/areas/users/validation.py b/areas/users/validation.py new file mode 100644 index 0000000..84c3dea --- /dev/null +++ b/areas/users/validation.py @@ -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"], +} diff --git a/helpers/__init__.py b/helpers/__init__.py new file mode 100644 index 0000000..6743363 --- /dev/null +++ b/helpers/__init__.py @@ -0,0 +1,2 @@ +from .kratos_api import * +from .error_handler import * diff --git a/helpers/error_handler.py b/helpers/error_handler.py new file mode 100644 index 0000000..4c19498 --- /dev/null +++ b/helpers/error_handler.py @@ -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}) diff --git a/helpers/kratos_api.py b/helpers/kratos_api.py index 5a25b31..739f9a0 100644 --- a/helpers/kratos_api.py +++ b/helpers/kratos_api.py @@ -1,32 +1,55 @@ +from logging import error import requests 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 def get(url): try: - return requests.get('{}{}'.format(KRATOS_URL, url)) + res = requests.get("{}{}".format(KRATOS_URL, url)) + KratosApi.__handleError(res) + return res except: - return "Failed to contact Kratos" + raise KratosError() @staticmethod def post(url, data): 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: - return "Failed to contact Kratos" + raise KratosError() @staticmethod def put(url, data): 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: - return "Failed to contact Kratos" + raise KratosError() @staticmethod def delete(url): 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: - return "Failed to contact Kratos" \ No newline at end of file + raise KratosError() diff --git a/requirements.txt b/requirements.txt index dbc8957..d21805a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,5 @@ +attrs==21.2.0 +black==21.9b0 certifi==2021.10.8 cffi==1.15.0 charset-normalizer==2.0.7 @@ -5,15 +7,25 @@ click==8.0.3 cryptography==35.0.0 Flask==2.0.2 Flask-Cors==3.0.10 +flask-expects-json==1.6.0 Flask-JWT-Extended==4.3.1 gunicorn==20.1.0 idna==3.3 +install==1.3.4 itsdangerous==2.0.1 Jinja2==3.0.2 +jsonschema==4.1.2 MarkupSafe==2.0.1 +mypy-extensions==0.4.3 +pathspec==0.9.0 +platformdirs==2.4.0 pycparser==2.20 PyJWT==2.3.0 +pyrsistent==0.18.0 +regex==2021.10.23 requests==2.26.0 six==1.16.0 +tomli==1.2.2 +typing-extensions==3.10.0.2 urllib3==1.26.7 Werkzeug==2.0.2