diff --git a/code/backend/nest_backend/__main__.py b/code/backend/nest_backend/__main__.py index fb8dd56..57839b5 100644 --- a/code/backend/nest_backend/__main__.py +++ b/code/backend/nest_backend/__main__.py @@ -7,20 +7,33 @@ import werkzeug.middleware.proxy_fix from .routes import * from .database import Base, tables import psycopg2 - +from .gestione import * +from flask_jwt_extended import * app = Flask(__name__) if os.getenv('COOKIE_SECRET'): app.secret_key = os.getenv('COOKIE_SECRET') else: app.secret_key = "testing" +if os.getenv("JWT_SECRET_KEY"): + app.config["JWT_SECRET_KEY"] = os.getenv("JWT_SECRET_KEY") +else: + app.config["JWT_SECRET_KEY"] = "testing" + reverse_proxy_app = werkzeug.middleware.proxy_fix.ProxyFix(app=app, x_for=1, x_proto=0, x_host=1, x_port=0, x_prefix=0) app.config['SQLALCHEMY_DATABASE_URI'] = 'postgresql://postgres:password@localhost:5432/PdSDev' Base.app = app Base.init_app(app) +jwt = JWTManager(app) # Routes setup + app.add_url_rule("/doa", view_func=page_doa, methods=["GET", "POST"]) +app.add_url_rule("/api/login", view_func=page_login, methods=["POST"]) +app.add_url_rule("/api/user/create", view_func=page_user_create, methods=["POST"]) if __name__ == "__main__": Base.create_all() + if not User.query.filter_by(isAdmin=True).all(): + Base.session.add(User(email="admin@admin.com", password=gen_password("password"), username="admin", isAdmin=True)) + Base.session.commit() app.run(debug=True) diff --git a/code/backend/nest_backend/gestione.py b/code/backend/nest_backend/gestione.py index 9e7573a..72a5242 100644 --- a/code/backend/nest_backend/gestione.py +++ b/code/backend/nest_backend/gestione.py @@ -5,3 +5,59 @@ A utilities Python Module. Gestione adds many fancy thingamajigs to the flask application, such as a login system and such. """ +from .database import * +import bcrypt +import functools +from flask_jwt_extended import get_jwt_identity + + +def authenticate(username, password): + """ + Authentication method. It checks if the combination of username+password is a valid match. If not, it returns None. + :param username: the user's email + :param password: the user's password + :return: if the credentials are correct, it returns the user. Else, it returns None. + """ + user = User.query.filter_by(email=username).first() + try: + if bcrypt.checkpw(bytes(password, encoding="utf-8"), user.password): + return user + except AttributeError: + # Se non esiste l'Utente + return None + + +def identity(payload): + """ + Authentication verification method. It checks if the user is in fact registered on the server. + It is required by Flask-JWT, and shouldnt be used alone. + :param payload: the reqest payload. + :return: an User or None. It depends whether the user is actually registered on the platform. + """ + user_id = payload['identity'] + user = User.query.filter_by(id=user_id).first() + if user: + return user.id + return None + + +def gen_password(password): + """ + It generates an hashed password. + :param password: the password that needs to be hashed. + :return: the password's hash. + """ + return bcrypt.hashpw(bytes(password, "utf-8"), bcrypt.gensalt()) + + +def find_user(email): + return User.query.filter_by(email=email).first() + + +def admin_or_403(f): + @functools.wraps(f) + def func(*args, **kwargs): + current_user = get_jwt_identity() + return f(*args, **kwargs) + + return func diff --git a/code/backend/nest_backend/routes/__init__.py b/code/backend/nest_backend/routes/__init__.py index 1a1609c..6752c31 100644 --- a/code/backend/nest_backend/routes/__init__.py +++ b/code/backend/nest_backend/routes/__init__.py @@ -2,4 +2,5 @@ This module imports all the routes that return something to the frontend. """ -from .doa import page_doa \ No newline at end of file +from .doa import page_doa +from .users import * \ No newline at end of file diff --git a/code/backend/nest_backend/routes/doa.py b/code/backend/nest_backend/routes/doa.py index 29c4e6d..a8fc545 100644 --- a/code/backend/nest_backend/routes/doa.py +++ b/code/backend/nest_backend/routes/doa.py @@ -7,7 +7,10 @@ from ..database import * def page_doa(): - utente = User() + """ + Dead or Alive page. If a client sees this, the server is probably fine. + :return: A friendly and calming message, that makes you happy that the server is not on fire. + """ if request.method == "GET": - return "Get" - return "If you see this, the server is fine." + return "If you see this, the server is fine." + return "Hello there." diff --git a/code/backend/nest_backend/routes/users/__init__.py b/code/backend/nest_backend/routes/users/__init__.py new file mode 100644 index 0000000..f83b7ce --- /dev/null +++ b/code/backend/nest_backend/routes/users/__init__.py @@ -0,0 +1,2 @@ +from .user_create import page_user_create +from .login import page_login \ No newline at end of file diff --git a/code/backend/nest_backend/routes/users/login.py b/code/backend/nest_backend/routes/users/login.py new file mode 100644 index 0000000..3948521 --- /dev/null +++ b/code/backend/nest_backend/routes/users/login.py @@ -0,0 +1,21 @@ +from flask import render_template, abort, jsonify, request +from ...database import * +from ...gestione import * +from flask_jwt_extended import create_access_token + + +def page_login(): + """ + The API call that allows to log-in. It requires: + :form email: The user's email + :form password: The users's password + :return: Json-formatted data. If the login is successful, it will contain the access_token. + + The access_token must be included in the Authorization header, using the format Bearer . + """ + email = request.json.get("email", None) + password = request.json.get("password", None) + if authenticate(email, password): + access_token = create_access_token(identity=email) + return jsonify({"result": "success", "access_token": access_token}), 201 + return jsonify({"result": "failure", "msg": "Bad username or password."}), 401 diff --git a/code/backend/nest_backend/routes/users/user_create.py b/code/backend/nest_backend/routes/users/user_create.py new file mode 100644 index 0000000..bf98b3f --- /dev/null +++ b/code/backend/nest_backend/routes/users/user_create.py @@ -0,0 +1,22 @@ +from flask import render_template, abort, jsonify, request +from ...database import * +from flask_jwt_extended import jwt_required +from ...gestione import * + +@jwt_required() +def page_user_create(): + """ + The API call that allows to create new users. It requires: + :form email: The user's email + :form password: The users's password + :form username: The users's username + :return: Json-formatted data. If something goes wrong, it returns a + {'result':'failure', 'content':'something blew up'}, else it returns {'result':'success', 'content':{newUser.to_json()}. + """ + user = find_user(get_jwt_identity()) + if not user.isAdmin: + abort(403) + nUser = User(email=request.json.get("email"), password=gen_password(request.json.get("password")), username=request.json.get("username")) + Base.session.add(nUser) + Base.session.commit() + return jsonify({"result":"success", "content":"something"}) diff --git a/code/backend/poetry.lock b/code/backend/poetry.lock index 5ebbdf2..a81a551 100644 --- a/code/backend/poetry.lock +++ b/code/backend/poetry.lock @@ -1,3 +1,30 @@ +[[package]] +name = "bcrypt" +version = "3.2.0" +description = "Modern password hashing for your software and your servers" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +cffi = ">=1.1" +six = ">=1.4.1" + +[package.extras] +tests = ["pytest (>=3.2.1,!=3.3.0)"] +typecheck = ["mypy"] + +[[package]] +name = "cffi" +version = "1.14.5" +description = "Foreign Function Interface for Python calling C code." +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +pycparser = "*" + [[package]] name = "click" version = "7.1.2" @@ -25,6 +52,22 @@ dev = ["pytest", "coverage", "tox", "sphinx", "pallets-sphinx-themes", "sphinxco docs = ["sphinx", "pallets-sphinx-themes", "sphinxcontrib-log-cabinet", "sphinx-issues"] dotenv = ["python-dotenv"] +[[package]] +name = "flask-jwt-extended" +version = "4.1.0" +description = "Extended JWT integration with Flask" +category = "main" +optional = false +python-versions = ">=3.6,<4" + +[package.dependencies] +Flask = ">=1.0,<2.0" +PyJWT = ">=2.0,<3.0" +Werkzeug = ">=0.14" + +[package.extras] +asymmetric_crypto = ["cryptography (>=3.0,<4.0)"] + [[package]] name = "flask-sqlalchemy" version = "2.5.1" @@ -86,6 +129,36 @@ category = "main" optional = false python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*" +[[package]] +name = "pycparser" +version = "2.20" +description = "C parser in Python" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "pyjwt" +version = "2.0.1" +description = "JSON Web Token implementation in Python" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.extras] +crypto = ["cryptography (>=3.3.1,<4.0.0)"] +dev = ["sphinx", "sphinx-rtd-theme", "zope.interface", "cryptography (>=3.3.1,<4.0.0)", "pytest (>=6.0.0,<7.0.0)", "coverage[toml] (==5.0.4)", "mypy", "pre-commit"] +docs = ["sphinx", "sphinx-rtd-theme", "zope.interface"] +tests = ["pytest (>=6.0.0,<7.0.0)", "coverage[toml] (==5.0.4)"] + +[[package]] +name = "six" +version = "1.15.0" +description = "Python 2 and 3 compatibility utilities" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" + [[package]] name = "sqlalchemy" version = "1.4.10" @@ -132,9 +205,57 @@ watchdog = ["watchdog"] [metadata] lock-version = "1.1" python-versions = "^3.8.5" -content-hash = "71b957004ae80674013de8327f592dd17b9ffb4e5c6a865e69326555f9c2a1ce" +content-hash = "6bc937df83a15e8774d6ab63f19f5b6b3780730d935678c81259f53ee7cd8ebc" [metadata.files] +bcrypt = [ + {file = "bcrypt-3.2.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:c95d4cbebffafcdd28bd28bb4e25b31c50f6da605c81ffd9ad8a3d1b2ab7b1b6"}, + {file = "bcrypt-3.2.0-cp36-abi3-manylinux1_x86_64.whl", hash = "sha256:63d4e3ff96188e5898779b6057878fecf3f11cfe6ec3b313ea09955d587ec7a7"}, + {file = "bcrypt-3.2.0-cp36-abi3-manylinux2010_x86_64.whl", hash = "sha256:cd1ea2ff3038509ea95f687256c46b79f5fc382ad0aa3664d200047546d511d1"}, + {file = "bcrypt-3.2.0-cp36-abi3-manylinux2014_aarch64.whl", hash = "sha256:cdcdcb3972027f83fe24a48b1e90ea4b584d35f1cc279d76de6fc4b13376239d"}, + {file = "bcrypt-3.2.0-cp36-abi3-win32.whl", hash = "sha256:a67fb841b35c28a59cebed05fbd3e80eea26e6d75851f0574a9273c80f3e9b55"}, + {file = "bcrypt-3.2.0-cp36-abi3-win_amd64.whl", hash = "sha256:81fec756feff5b6818ea7ab031205e1d323d8943d237303baca2c5f9c7846f34"}, + {file = "bcrypt-3.2.0.tar.gz", hash = "sha256:5b93c1726e50a93a033c36e5ca7fdcd29a5c7395af50a6892f5d9e7c6cfbfb29"}, +] +cffi = [ + {file = "cffi-1.14.5-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:bb89f306e5da99f4d922728ddcd6f7fcebb3241fc40edebcb7284d7514741991"}, + {file = "cffi-1.14.5-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:34eff4b97f3d982fb93e2831e6750127d1355a923ebaeeb565407b3d2f8d41a1"}, + {file = "cffi-1.14.5-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:99cd03ae7988a93dd00bcd9d0b75e1f6c426063d6f03d2f90b89e29b25b82dfa"}, + {file = "cffi-1.14.5-cp27-cp27m-win32.whl", hash = "sha256:65fa59693c62cf06e45ddbb822165394a288edce9e276647f0046e1ec26920f3"}, + {file = "cffi-1.14.5-cp27-cp27m-win_amd64.whl", hash = "sha256:51182f8927c5af975fece87b1b369f722c570fe169f9880764b1ee3bca8347b5"}, + {file = "cffi-1.14.5-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:43e0b9d9e2c9e5d152946b9c5fe062c151614b262fda2e7b201204de0b99e482"}, + {file = "cffi-1.14.5-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:cbde590d4faaa07c72bf979734738f328d239913ba3e043b1e98fe9a39f8b2b6"}, + {file = "cffi-1.14.5-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:5de7970188bb46b7bf9858eb6890aad302577a5f6f75091fd7cdd3ef13ef3045"}, + {file = "cffi-1.14.5-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:a465da611f6fa124963b91bf432d960a555563efe4ed1cc403ba5077b15370aa"}, + {file = "cffi-1.14.5-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:d42b11d692e11b6634f7613ad8df5d6d5f8875f5d48939520d351007b3c13406"}, + {file = "cffi-1.14.5-cp35-cp35m-win32.whl", hash = "sha256:72d8d3ef52c208ee1c7b2e341f7d71c6fd3157138abf1a95166e6165dd5d4369"}, + {file = "cffi-1.14.5-cp35-cp35m-win_amd64.whl", hash = "sha256:29314480e958fd8aab22e4a58b355b629c59bf5f2ac2492b61e3dc06d8c7a315"}, + {file = "cffi-1.14.5-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:3d3dd4c9e559eb172ecf00a2a7517e97d1e96de2a5e610bd9b68cea3925b4892"}, + {file = "cffi-1.14.5-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:48e1c69bbacfc3d932221851b39d49e81567a4d4aac3b21258d9c24578280058"}, + {file = "cffi-1.14.5-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:69e395c24fc60aad6bb4fa7e583698ea6cc684648e1ffb7fe85e3c1ca131a7d5"}, + {file = "cffi-1.14.5-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:9e93e79c2551ff263400e1e4be085a1210e12073a31c2011dbbda14bda0c6132"}, + {file = "cffi-1.14.5-cp36-cp36m-win32.whl", hash = "sha256:58e3f59d583d413809d60779492342801d6e82fefb89c86a38e040c16883be53"}, + {file = "cffi-1.14.5-cp36-cp36m-win_amd64.whl", hash = "sha256:005a36f41773e148deac64b08f233873a4d0c18b053d37da83f6af4d9087b813"}, + {file = "cffi-1.14.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:2894f2df484ff56d717bead0a5c2abb6b9d2bf26d6960c4604d5c48bbc30ee73"}, + {file = "cffi-1.14.5-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:0857f0ae312d855239a55c81ef453ee8fd24136eaba8e87a2eceba644c0d4c06"}, + {file = "cffi-1.14.5-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:cd2868886d547469123fadc46eac7ea5253ea7fcb139f12e1dfc2bbd406427d1"}, + {file = "cffi-1.14.5-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:35f27e6eb43380fa080dccf676dece30bef72e4a67617ffda586641cd4508d49"}, + {file = "cffi-1.14.5-cp37-cp37m-win32.whl", hash = "sha256:9ff227395193126d82e60319a673a037d5de84633f11279e336f9c0f189ecc62"}, + {file = "cffi-1.14.5-cp37-cp37m-win_amd64.whl", hash = "sha256:9cf8022fb8d07a97c178b02327b284521c7708d7c71a9c9c355c178ac4bbd3d4"}, + {file = "cffi-1.14.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8b198cec6c72df5289c05b05b8b0969819783f9418e0409865dac47288d2a053"}, + {file = "cffi-1.14.5-cp38-cp38-manylinux1_i686.whl", hash = "sha256:ad17025d226ee5beec591b52800c11680fca3df50b8b29fe51d882576e039ee0"}, + {file = "cffi-1.14.5-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:6c97d7350133666fbb5cf4abdc1178c812cb205dc6f41d174a7b0f18fb93337e"}, + {file = "cffi-1.14.5-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:8ae6299f6c68de06f136f1f9e69458eae58f1dacf10af5c17353eae03aa0d827"}, + {file = "cffi-1.14.5-cp38-cp38-win32.whl", hash = "sha256:b85eb46a81787c50650f2392b9b4ef23e1f126313b9e0e9013b35c15e4288e2e"}, + {file = "cffi-1.14.5-cp38-cp38-win_amd64.whl", hash = "sha256:1f436816fc868b098b0d63b8920de7d208c90a67212546d02f84fe78a9c26396"}, + {file = "cffi-1.14.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1071534bbbf8cbb31b498d5d9db0f274f2f7a865adca4ae429e147ba40f73dea"}, + {file = "cffi-1.14.5-cp39-cp39-manylinux1_i686.whl", hash = "sha256:9de2e279153a443c656f2defd67769e6d1e4163952b3c622dcea5b08a6405322"}, + {file = "cffi-1.14.5-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:6e4714cc64f474e4d6e37cfff31a814b509a35cb17de4fb1999907575684479c"}, + {file = "cffi-1.14.5-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:158d0d15119b4b7ff6b926536763dc0714313aa59e320ddf787502c70c4d4bee"}, + {file = "cffi-1.14.5-cp39-cp39-win32.whl", hash = "sha256:afb29c1ba2e5a3736f1c301d9d0abe3ec8b86957d04ddfa9d7a6a42b9367e396"}, + {file = "cffi-1.14.5-cp39-cp39-win_amd64.whl", hash = "sha256:f2d45f97ab6bb54753eab54fffe75aaf3de4ff2341c9daee1987ee1837636f1d"}, + {file = "cffi-1.14.5.tar.gz", hash = "sha256:fd78e5fee591709f32ef6edb9a015b4aa1a5022598e36227500c8f4e02328d9c"}, +] click = [ {file = "click-7.1.2-py2.py3-none-any.whl", hash = "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"}, {file = "click-7.1.2.tar.gz", hash = "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a"}, @@ -143,6 +264,10 @@ flask = [ {file = "Flask-1.1.2-py2.py3-none-any.whl", hash = "sha256:8a4fdd8936eba2512e9c85df320a37e694c93945b33ef33c89946a340a238557"}, {file = "Flask-1.1.2.tar.gz", hash = "sha256:4efa1ae2d7c9865af48986de8aeb8504bf32c7f3d6fdc9353d34b21f4b127060"}, ] +flask-jwt-extended = [ + {file = "Flask-JWT-Extended-4.1.0.tar.gz", hash = "sha256:77ca23f23e80480ea42b9c1d9b0fca214e08db7192583e782c2421416b4a4655"}, + {file = "Flask_JWT_Extended-4.1.0-py2.py3-none-any.whl", hash = "sha256:f952f4ebd449182431c6755acfb7cbb52b8034df7a9f9ef95eb51ccfc1e235b0"}, +] flask-sqlalchemy = [ {file = "Flask-SQLAlchemy-2.5.1.tar.gz", hash = "sha256:2bda44b43e7cacb15d4e05ff3cc1f8bc97936cc464623424102bfc2c35e95912"}, {file = "Flask_SQLAlchemy-2.5.1-py2.py3-none-any.whl", hash = "sha256:f12c3d4cc5cc7fdcc148b9527ea05671718c3ea45d50c7e732cceb33f574b390"}, @@ -252,6 +377,18 @@ psycopg2 = [ {file = "psycopg2-2.8.6-cp39-cp39-win_amd64.whl", hash = "sha256:d5062ae50b222da28253059880a871dc87e099c25cb68acf613d9d227413d6f7"}, {file = "psycopg2-2.8.6.tar.gz", hash = "sha256:fb23f6c71107c37fd667cb4ea363ddeb936b348bbd6449278eb92c189699f543"}, ] +pycparser = [ + {file = "pycparser-2.20-py2.py3-none-any.whl", hash = "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705"}, + {file = "pycparser-2.20.tar.gz", hash = "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0"}, +] +pyjwt = [ + {file = "PyJWT-2.0.1-py3-none-any.whl", hash = "sha256:b70b15f89dc69b993d8a8d32c299032d5355c82f9b5b7e851d1a6d706dffe847"}, + {file = "PyJWT-2.0.1.tar.gz", hash = "sha256:a5c70a06e1f33d81ef25eecd50d50bd30e34de1ca8b2b9fa3fe0daaabcf69bf7"}, +] +six = [ + {file = "six-1.15.0-py2.py3-none-any.whl", hash = "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"}, + {file = "six-1.15.0.tar.gz", hash = "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259"}, +] sqlalchemy = [ {file = "SQLAlchemy-1.4.10-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:6c24884bb8d0065cf6f61b643e8f32947ef8386a5bcdad41b921ed81994ea8f1"}, {file = "SQLAlchemy-1.4.10-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:266fbf4a0d3f4ed614fff60485e3ba83d3eef4a736102b9b7e461402dc930234"}, diff --git a/code/backend/pyproject.toml b/code/backend/pyproject.toml index 5f87c99..f36e6c2 100644 --- a/code/backend/pyproject.toml +++ b/code/backend/pyproject.toml @@ -9,6 +9,8 @@ python = "^3.8.5" psycopg2 = "^2.8.6" Flask = "^1.1.2" Flask-SQLAlchemy = "^2.5.1" +bcrypt = "^3.2.0" +Flask-JWT-Extended = "^4.1.0" [tool.poetry.dev-dependencies]