From 5f00cb6844744de961eb87c7f25d61a03997e759 Mon Sep 17 00:00:00 2001 From: Omosola Odetunde Date: Wed, 10 Sep 2025 18:16:51 +0200 Subject: [PATCH 01/16] refactor: cleaning up check_password bcrypt/argon2 implementation --- server/user/models.py | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/server/user/models.py b/server/user/models.py index 8de25367..a611200a 100644 --- a/server/user/models.py +++ b/server/user/models.py @@ -13,25 +13,32 @@ class User(Base): def set_password(self, password): self.password = argon2.generate_password_hash(password) - def check_password(self, passwd): + def check_password(self, password): """ Check if the passwordhash is in Argon2 or Bcrypt(old) format Resets the password hash to argon2 format if stored in bcrypt Returns value for login route """ + + # check password (if bcrypt hash) + try: + if bcrypt.check_password_hash(self.password, password): + self.set_password(password) + # pwd in bcrypt form and correct + return True + except ValueError as error: + print(error) + + # check password (if argon2 hash) try: - if bcrypt.check_password_hash(self.password, passwd): - bpass = True + if argon2.check_password_hash(self.password, password): + self.set_password(password) + # pwd in argon2 form and correct + return True except ValueError as error: print(error) - bpass = False - if argon2.check_password_hash(self.password, passwd): - return True - elif not argon2.check_password_hash(self.password, passwd) and not bpass: - return False - elif not argon2.check_password_hash(self.password, passwd) and bpass: - self.set_password(passwd) - return True + + return False def update_bio(self, new_bio): self.bio = new_bio From 43721502f71d84280136bcb6e45daa2c5d623aa6 Mon Sep 17 00:00:00 2001 From: Omosola Odetunde Date: Mon, 15 Sep 2025 12:56:10 +0200 Subject: [PATCH 02/16] refactor: standardizing signin response message --- server/src/client/app/src/pages/auth/SignIn.js | 6 +++--- server/user/views.py | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/server/src/client/app/src/pages/auth/SignIn.js b/server/src/client/app/src/pages/auth/SignIn.js index 7f5f17ea..f59a5799 100755 --- a/server/src/client/app/src/pages/auth/SignIn.js +++ b/server/src/client/app/src/pages/auth/SignIn.js @@ -46,11 +46,11 @@ function SignIn() { }) .then(response => { console.log(response.data); - if (response.data.msg === "NotConfirmed") { + if (response.data.msg === "UserNotConfirmed") { setConfirm(true); - } else if (response.data.msg === "Wrong username or password") { + } else if (response.data.msg === "WrongUsernameOrPassword") { setNotExist(true); - } else if (response.data.msg === "wrong password") { + } else if (response.data.msg === "WrongPassword") { setWrongPass(true); } else { localStorage.setItem("token", response.data.access_token); diff --git a/server/user/views.py b/server/user/views.py index 05b21b8d..ddc527fc 100644 --- a/server/user/views.py +++ b/server/user/views.py @@ -55,16 +55,16 @@ def login(): user = session.query(User).filter_by(username=jobj["email"]).first() print(jobj) if user is None: - print("user does not exist") - return jsonify({"msg": "Wrong username or password"}), 200 + print("User does not exist") + return jsonify({"msg": "WrongUsernameOrPassword"}), 200 elif not user.check_password(jobj["password"]): print("Wrong password") - return jsonify({"msg": "wrong password"}), 200 + return jsonify({"msg": "WrongPassword"}), 200 elif user.active == 0: print("User not confirmed") - return jsonify({"msg": "NotConfirmed"}), 200 + return jsonify({"msg": "UserNotConfirmed"}), 200 else: user_g = session.query(UserGroups).filter_by(user_id=user.id).first() From 595447c649178f510cc0a4e65c4649805b929232 Mon Sep 17 00:00:00 2001 From: Omosola Odetunde Date: Mon, 15 Sep 2025 13:09:18 +0200 Subject: [PATCH 03/16] refactor: minor SignUp cleanup --- .../src/client/app/src/pages/auth/SignIn.js | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/server/src/client/app/src/pages/auth/SignIn.js b/server/src/client/app/src/pages/auth/SignIn.js index f59a5799..68724925 100755 --- a/server/src/client/app/src/pages/auth/SignIn.js +++ b/server/src/client/app/src/pages/auth/SignIn.js @@ -75,12 +75,15 @@ function SignIn() { )} + {/** Header **/} Welcome back! Sign in to continue + + {/** Start Error Banner **/} {errorlog && ( )} - {wrongpass && ( + {/* User with this email & password could not be found */} + {(wrongpass || notexist) && ( )} + {/* User's account not yet confirmed */} {confirmflag && ( (resend activation token) )} - {notexist && ( - - - Wrong username or password - - )} + {/** End Error Banner **/} + + {/** Start Entry Fields **/}
Email Address @@ -156,6 +155,7 @@ function SignIn() { Forgot password + {/** End Entry Fields **/}
From 4600cd5930ed53510889afc7d498da558476a287 Mon Sep 17 00:00:00 2001 From: Omosola Odetunde Date: Wed, 17 Sep 2025 19:54:05 +0200 Subject: [PATCH 04/16] refactor/fix: expanding login tests and fixing login() --- server/user/views.py | 2 +- tests/test_user_routes.py | 152 ++++++++++++++++++++++++++++++-------- 2 files changed, 123 insertions(+), 31 deletions(-) diff --git a/server/user/views.py b/server/user/views.py index ddc527fc..b7daf4da 100644 --- a/server/user/views.py +++ b/server/user/views.py @@ -52,7 +52,7 @@ def login(): jobj = request.get_json() with Session() as session: # need to inspect jobj. Is `email` actually username or is there also a `username`? - user = session.query(User).filter_by(username=jobj["email"]).first() + user = session.query(User).filter_by(email=jobj["email"]).first() print(jobj) if user is None: print("User does not exist") diff --git a/tests/test_user_routes.py b/tests/test_user_routes.py index e7fccf9b..7f265c5e 100644 --- a/tests/test_user_routes.py +++ b/tests/test_user_routes.py @@ -6,34 +6,112 @@ from server.user.models import User -@pytest.fixture(scope="module") -def test_user(): - user1 = User(email="abc@abc.com", username="abc") - user1.set_password("abcabc") - with Session() as session: - session.add(user1) +@pytest.fixture +def db_session(test_client, valid_user, unconfirmed_user): + # setup + session = Session() + session.add(valid_user) + session.add(unconfirmed_user) + session.commit() + + try: + # test + yield session + finally: + # cleanup + session.delete(valid_user) + session.delete(unconfirmed_user) session.commit() - yield -def test_confirm_user(test_client, init_database): - with Session() as session: - user = session.query(User).filter_by(email="ff@ff.com").first() - url = "?token=" + str(user.activation_code) +@pytest.fixture(scope="function") +def valid_user(): + user = User( + email="abc@abc.com", + username="abc", + ip_address="1.2.3.4", + created_on="0000", + company="0000", + country="0000", + bio="No Bio", + active=1 + ) + user.set_password("abcabc") + + return user + + +@pytest.fixture(scope="function") +def unconfirmed_user(): + user = User( + email="ff@ff.com", + username="ff", + ip_address="1.2.3.4", + created_on="0000", + company="0000", + country="0000", + bio="No Bio", + active=0 + ) + user.set_password("ff") + + return user + + +def login(test_client, email, password): response = test_client.post( - "/confirmation", json={"url": url, "password": "ff"}, follow_redirects=True + "/login", json={"email": email, "password": password}, follow_redirects=True ) - assert response.status_code == 200 + return response -def test_login(test_client, init_database): +def test_confirm_user(test_client, init_database, unconfirmed_user): + url = "?token=" + str(unconfirmed_user.activation_code) response = test_client.post( - "/login", json={"email": "ff@ff.com", "password": "ff"}, follow_redirects=True + "/confirmation", json={"url": url, "password": "ff"}, follow_redirects=True ) assert response.status_code == 200 -def test_profile(test_client, init_database): +def test_login(test_client, init_database, db_session, valid_user): + response = login(test_client, valid_user.email, "abcabc") + + user = db_session.query(User).filter_by(email=valid_user.email).first() + + assert response.json["access_token"] + assert response.status_code == 200 + + +def test_login_wrong_password(test_client, init_database, db_session, valid_user): + response = login(test_client, valid_user.email, "wrongpassword") + + user = db_session.query(User).filter_by(email=valid_user.email).first() + + assert response.json["msg"] == "WrongPassword" + assert response.status_code == 200 + + +def test_login_user_not_existent(test_client, init_database, db_session, valid_user): + response = login(test_client, "fake@user.com", "wrongpassword") + + user = db_session.query(User).filter_by(email=valid_user.email).first() + + assert response.json["msg"] == "WrongUsernameOrPassword" + assert response.status_code == 200 + + +def test_login_user_not_confirmed(test_client, init_database, db_session, unconfirmed_user): + response = login(test_client, unconfirmed_user.email, "ff") + + user = db_session.query(User).filter_by(email=unconfirmed_user.email).first() + + assert response.json["msg"] == "UserNotConfirmed" + assert response.status_code == 200 + + +def test_profile(test_client, init_database, valid_user): + login(test_client, valid_user.email, "abcabc") + access_token = str(os.environ.get("TEST_ACCESS_TOKEN")) headers = {"Authorization": "Bearer {}".format(access_token)} response = test_client.get("/profile", headers=headers) @@ -43,7 +121,9 @@ def test_profile(test_client, init_database): assert session.query(User).filter_by(email="ff@ff.com").first() == user -def test_profile_changes(test_client, init_database): +def test_profile_changes(test_client, init_database, valid_user): + login(test_client, valid_user.email, "abcabc") + access_token = str(os.environ.get("TEST_ACCESS_TOKEN")) headers = {"Authorization": "Bearer {}".format(access_token)} response = test_client.post( @@ -54,48 +134,60 @@ def test_profile_changes(test_client, init_database): "first_name": "ssd", "last_name": "sds", "image": "", - "email": "ff@ff.com", + "email": valid_user.email, }, ) + assert response.status_code == 200 -def test_api_key_get(test_client, init_database): +def test_api_key_get(test_client, init_database, valid_user): + login(test_client, valid_user.email, "abcabc") + access_token = str(os.environ.get("TEST_ACCESS_TOKEN")) headers = {"Authorization": "Bearer {}".format(access_token)} response = test_client.get("/api-key", headers=headers) + assert response.status_code == 200 -def test_api_key_post(test_client, init_database): +def test_api_key_post(test_client, init_database, valid_user): + login(test_client, valid_user.email, "abcabc") + access_token = str(os.environ.get("TEST_ACCESS_TOKEN")) headers = {"Authorization": "Bearer {}".format(access_token)} response = test_client.post("/api-key", headers=headers) + assert response.status_code == 200 -def test_logout(test_client, init_database): +def test_logout(test_client, init_database, valid_user): + login(test_client, valid_user.email, "abcabc") + access_token = str(os.environ.get("TEST_ACCESS_TOKEN")) headers = {"Authorization": "Bearer {}".format(access_token)} response = test_client.get("/logout", headers=headers) + assert response.status_code == 200 -def test_forgot_token(test_client, init_database): - with Session() as session: - user = session.query(User).filter_by(email="ff@ff.com").first() - url = "?token=" + str(user.forgotten_password_code) +def test_forgot_token(test_client, init_database, valid_user): + login(test_client, valid_user.email, "abcabc") + + url = "?token=" + str(valid_user.forgotten_password_code) response = test_client.post( "/forgot-token", json={"url": url}, follow_redirects=True ) + assert response.status_code == 200 -def test_reset_password(test_client, init_database): - with Session() as session: - user = session.query(User).filter_by(email="ff@ff.com").first() - url = "?token=" + str(user.forgotten_password_code) +def test_reset_password(test_client, init_databas, valid_user): + login(test_client, valid_user.email, "abcabc") + + url = "?token=" + str(valid_user.forgotten_password_code) response = test_client.post( - "/forgot-token", json={"url": url, "password": "ff"}, follow_redirects=True + "/forgot-token", json={"url": url, "password": "abcabc"}, follow_redirects=True ) + assert response.status_code == 200 From 38a82a77828107cb2055bd127ae4c53e3bdbabd3 Mon Sep 17 00:00:00 2001 From: Omosola Odetunde Date: Wed, 17 Sep 2025 22:14:01 +0200 Subject: [PATCH 05/16] tests/fix: refactoring all existing user_routes tests. all passing. --- tests/test_user_routes.py | 112 +++++++++++++++++++++++++++----------- 1 file changed, 79 insertions(+), 33 deletions(-) diff --git a/tests/test_user_routes.py b/tests/test_user_routes.py index 7f265c5e..49ffdc7a 100644 --- a/tests/test_user_routes.py +++ b/tests/test_user_routes.py @@ -9,6 +9,7 @@ @pytest.fixture def db_session(test_client, valid_user, unconfirmed_user): # setup + print("setup") session = Session() session.add(valid_user) session.add(unconfirmed_user) @@ -16,9 +17,11 @@ def db_session(test_client, valid_user, unconfirmed_user): try: # test + print("test") yield session finally: # cleanup + print("cleanup") session.delete(valid_user) session.delete(unconfirmed_user) session.commit() @@ -34,6 +37,8 @@ def valid_user(): company="0000", country="0000", bio="No Bio", + session_hash="0000", + forgotten_password_code="0000", active=1 ) user.set_password("abcabc") @@ -51,6 +56,9 @@ def unconfirmed_user(): company="0000", country="0000", bio="No Bio", + session_hash="0000", + activation_code="0000", + forgotten_password_code="0000", active=0 ) user.set_password("ff") @@ -65,19 +73,18 @@ def login(test_client, email, password): return response -def test_confirm_user(test_client, init_database, unconfirmed_user): +def test_confirm_user(test_client, init_database, db_session, unconfirmed_user): url = "?token=" + str(unconfirmed_user.activation_code) response = test_client.post( "/confirmation", json={"url": url, "password": "ff"}, follow_redirects=True ) + assert response.status_code == 200 def test_login(test_client, init_database, db_session, valid_user): response = login(test_client, valid_user.email, "abcabc") - user = db_session.query(User).filter_by(email=valid_user.email).first() - assert response.json["access_token"] assert response.status_code == 200 @@ -85,17 +92,13 @@ def test_login(test_client, init_database, db_session, valid_user): def test_login_wrong_password(test_client, init_database, db_session, valid_user): response = login(test_client, valid_user.email, "wrongpassword") - user = db_session.query(User).filter_by(email=valid_user.email).first() - assert response.json["msg"] == "WrongPassword" assert response.status_code == 200 -def test_login_user_not_existent(test_client, init_database, db_session, valid_user): +def test_login_user_not_existent(test_client, init_database, db_session): response = login(test_client, "fake@user.com", "wrongpassword") - user = db_session.query(User).filter_by(email=valid_user.email).first() - assert response.json["msg"] == "WrongUsernameOrPassword" assert response.status_code == 200 @@ -103,65 +106,90 @@ def test_login_user_not_existent(test_client, init_database, db_session, valid_u def test_login_user_not_confirmed(test_client, init_database, db_session, unconfirmed_user): response = login(test_client, unconfirmed_user.email, "ff") - user = db_session.query(User).filter_by(email=unconfirmed_user.email).first() - assert response.json["msg"] == "UserNotConfirmed" assert response.status_code == 200 -def test_profile(test_client, init_database, valid_user): +def test_get_profile(test_client, init_database, db_session, valid_user): login(test_client, valid_user.email, "abcabc") access_token = str(os.environ.get("TEST_ACCESS_TOKEN")) headers = {"Authorization": "Bearer {}".format(access_token)} response = test_client.get("/profile", headers=headers) - with Session() as session: - user = session.query(User).filter_by(email=response.json["email"]).first() - assert response.status_code == 200 - assert session.query(User).filter_by(email="ff@ff.com").first() == user + + retrieved_profile = response.json + + profile = { + "username": valid_user.username, + "bio": valid_user.bio, + "first_name": valid_user.first_name, + "last_name": valid_user.last_name, + "email": valid_user.email, + "image": valid_user.image, + "id": valid_user.id + } + + assert response.status_code == 200 + assert retrieved_profile == profile -def test_profile_changes(test_client, init_database, valid_user): +def test_profile_changes(test_client, init_database, db_session, valid_user): login(test_client, valid_user.email, "abcabc") + print(db_session, "HELLO2") access_token = str(os.environ.get("TEST_ACCESS_TOKEN")) headers = {"Authorization": "Bearer {}".format(access_token)} - response = test_client.post( - "/profile", - headers=headers, - json={ - "bio": "ss bio", - "first_name": "ssd", - "last_name": "sds", - "image": "", - "email": valid_user.email, - }, - ) + changes = { + "bio": "newbio", + "first_name": "newfirstname", + "last_name": "newlastname", + "image": "newimage", + "email": valid_user.email, + } + + response = test_client.post("/profile", headers=headers, json=changes) + db_session.refresh(valid_user) + + assert valid_user.bio == changes["bio"] + assert valid_user.first_name == changes["first_name"] + assert valid_user.last_name == changes["last_name"] + # note/todo: currently not testing image. image update functionality + # not implemented yet in views.py::profile + assert valid_user.email == changes["email"] + assert response.json["msg"] == "User information changed" assert response.status_code == 200 -def test_api_key_get(test_client, init_database, valid_user): +def test_api_key_get(test_client, init_database, db_session, valid_user): login(test_client, valid_user.email, "abcabc") access_token = str(os.environ.get("TEST_ACCESS_TOKEN")) headers = {"Authorization": "Bearer {}".format(access_token)} response = test_client.get("/api-key", headers=headers) + assert response.json["apikey"] == valid_user.session_hash assert response.status_code == 200 -def test_api_key_post(test_client, init_database, valid_user): +def test_api_key_post(test_client, init_database, db_session, valid_user): login(test_client, valid_user.email, "abcabc") + old_session_hash = valid_user.session_hash + access_token = str(os.environ.get("TEST_ACCESS_TOKEN")) headers = {"Authorization": "Bearer {}".format(access_token)} response = test_client.post("/api-key", headers=headers) + db_session.refresh(valid_user) + + # confirm session hash has been updated + assert valid_user.session_hash != old_session_hash + assert response.json["msg"] == "API Key updated" assert response.status_code == 200 -def test_logout(test_client, init_database, valid_user): +def test_logout(test_client, init_database, db_session, valid_user): login(test_client, valid_user.email, "abcabc") access_token = str(os.environ.get("TEST_ACCESS_TOKEN")) @@ -171,7 +199,7 @@ def test_logout(test_client, init_database, valid_user): assert response.status_code == 200 -def test_forgot_token(test_client, init_database, valid_user): +def test_forgot_token(test_client, init_database, db_session, valid_user): login(test_client, valid_user.email, "abcabc") url = "?token=" + str(valid_user.forgotten_password_code) @@ -179,15 +207,33 @@ def test_forgot_token(test_client, init_database, valid_user): "/forgot-token", json={"url": url}, follow_redirects=True ) + assert response.json["msg"] == "Token confirmed" assert response.status_code == 200 -def test_reset_password(test_client, init_databas, valid_user): +def test_forgot_token_invalid_token(test_client, init_database, db_session, valid_user): login(test_client, valid_user.email, "abcabc") + url = "?token=faketoken" + response = test_client.post( + "/forgot-token", json={"url": url}, follow_redirects=True + ) + + assert response.json["msg"] == "Error" + assert response.status_code == 401 + + +def test_reset_password(test_client, init_database, db_session, valid_user): + login(test_client, valid_user.email, "abcabc") + + new_password = "newpassword" + url = "?token=" + str(valid_user.forgotten_password_code) response = test_client.post( - "/forgot-token", json={"url": url, "password": "abcabc"}, follow_redirects=True + "/resetpassword", json={"url": url, "password": new_password}, follow_redirects=True ) + db_session.refresh(valid_user) + + assert valid_user.check_password(new_password) assert response.status_code == 200 From 36e2da53ba02a642bcd965ef34dddd00c36d99d6 Mon Sep 17 00:00:00 2001 From: Omosola Odetunde Date: Wed, 17 Sep 2025 22:15:27 +0200 Subject: [PATCH 06/16] refactor: standardizing session calls and filter_by calls --- server/user/views.py | 209 +++++++++++++++++++------------------- tests/test_user_routes.py | 4 - 2 files changed, 104 insertions(+), 109 deletions(-) diff --git a/server/user/views.py b/server/user/views.py index b7daf4da..e3301738 100644 --- a/server/user/views.py +++ b/server/user/views.py @@ -40,7 +40,7 @@ def check_if_token_in_blocklist(jwt_header, decrypted_token): @user_blueprint.route("/login", methods=["POST"]) -def login(): +def login(session=Session()): """ Login @@ -50,111 +50,108 @@ def login(): """ jobj = request.get_json() - with Session() as session: - # need to inspect jobj. Is `email` actually username or is there also a `username`? - user = session.query(User).filter_by(email=jobj["email"]).first() - print(jobj) - if user is None: - print("User does not exist") - return jsonify({"msg": "WrongUsernameOrPassword"}), 200 - - elif not user.check_password(jobj["password"]): - print("Wrong password") - return jsonify({"msg": "WrongPassword"}), 200 - - elif user.active == 0: - print("User not confirmed") - return jsonify({"msg": "UserNotConfirmed"}), 200 - - else: - user_g = session.query(UserGroups).filter_by(user_id=user.id).first() - if user_g is None: - user_ = UserGroups(user_id=user.id) - user_.set_group() - session.add(user_) - session.commit() - access_token = create_access_token(identity=user.username) - testing = strtobool(os.environ.get("TESTING", "True")) - print(testing) - if testing: - print("executed") - os.environ["TEST_ACCESS_TOKEN"] = access_token - # exporting access token to environment for testing - return jsonify(access_token=access_token), 200 + # need to inspect jobj. Is `email` actually username or is there also a `username`? + user = session.query(User).filter_by(email=jobj["email"]).first() + print(jobj) + if user is None: + print("User does not exist") + return jsonify({"msg": "WrongUsernameOrPassword"}), 200 + + elif not user.check_password(jobj["password"]): + print("Wrong password") + return jsonify({"msg": "WrongPassword"}), 200 + + elif user.active == 0: + print("User not confirmed") + return jsonify({"msg": "UserNotConfirmed"}), 200 + + else: + user_g = session.query(UserGroups).filter_by(user_id=user.id).first() + if user_g is None: + user_ = UserGroups(user_id=user.id) + user_.set_group() + session.add(user_) + session.commit() + access_token = create_access_token(identity=user.username) + testing = strtobool(os.environ.get("TESTING", "True")) + print(testing) + if testing: + print("executed") + os.environ["TEST_ACCESS_TOKEN"] = access_token + # exporting access token to environment for testing + return jsonify(access_token=access_token), 200 @user_blueprint.route("/profile", methods=["GET", "POST"]) @jwt_required() -def profile(): +def profile(session=Session()): """ Function to edit and retrieve user profile information """ current_user = get_jwt_identity() - with Session() as session: - user = session.query(User).filter_by(username=current_user).first() - if request.method == "GET": - return ( - jsonify( - { - "username": user.username, - "bio": user.bio, - "first_name": user.first_name, - "last_name": user.last_name, - "email": user.email, - "image": user.image, - "id": user.id, - } - ), - 200, - ) - elif request.method == "POST": - data = request.get_json() - # print(data['image']) - user.update_bio(data["bio"]) - user.update_first_name(data["first_name"]) - user.update_last_name(data["last_name"]) - if data["email"] != user.email: - print("email changed") - token = secrets.token_hex() - user.update_activation_code(token) - confirmation_email(data["email"], token) - user.update_email(data["email"]) + user = session.query(User).filter_by(username=current_user).first() + if request.method == "GET": + return ( + jsonify( + { + "username": user.username, + "bio": user.bio, + "first_name": user.first_name, + "last_name": user.last_name, + "email": user.email, + "image": user.image, + "id": user.id, + } + ), + 200, + ) + elif request.method == "POST": + data = request.get_json() + # print(data['image']) + user.update_bio(data["bio"]) + user.update_first_name(data["first_name"]) + user.update_last_name(data["last_name"]) + if data["email"] != user.email: + print("email changed") + token = secrets.token_hex() + user.update_activation_code(token) + confirmation_email(data["email"], token) + user.update_email(data["email"]) - session.merge(user) - session.commit() - return jsonify({"msg": "User information changed"}), 200 - else: - return jsonify({"msg": "profile OK"}), 200 + session.merge(user) + session.commit() + return jsonify({"msg": "User information changed"}), 200 + else: + return jsonify({"msg": "profile OK"}), 200 @jwt_required() @user_blueprint.route("/verifytoken", methods=["GET"]) -def verifytoken(): +def verifytoken(session=Session()): return "token-valid" # TODO Change Address before production @user_blueprint.route("/image", methods=["POST"]) @jwt_required() -def image(): +def image(session=Session()): """Function to receive and set user image""" current_user = get_jwt_identity() - with Session() as session: - user = session.query(User).filter_by(username=current_user).first() - f = request.files["file"] - Path("dev_data/" + str(user.username)).mkdir(parents=True, exist_ok=True) - f.save( - os.path.join("dev_data/" + str(user.username) + "/", secure_filename(f.filename)) - ) - path = "imgs/dev_data/" + str(user.username) + "/" + secure_filename(f.filename) - user.update_image_address(path) - session.merge(user) - session.commit() + user = session.query(User).filter_by(username=current_user).first() + f = request.files["file"] + Path("dev_data/" + str(user.username)).mkdir(parents=True, exist_ok=True) + f.save( + os.path.join("dev_data/" + str(user.username) + "/", secure_filename(f.filename)) + ) + path = "imgs/dev_data/" + str(user.username) + "/" + secure_filename(f.filename) + user.update_image_address(path) + session.merge(user) + session.commit() return jsonify({"msg": "User image changed"}), 200 @user_blueprint.route("/imgs/") -def images(path): +def images(path, session=Session()): try: im = Image.open(path) # im.thumbnail((w, h), Image.ANTIALIAS) @@ -170,7 +167,7 @@ def images(path): @user_blueprint.route("/logout", methods=["POST"]) @jwt_required() -def logout(): +def logout(session=Session()): """Function to logout user""" jti = get_jwt()["jti"] blocklist.add(jti) @@ -179,11 +176,11 @@ def logout(): @user_blueprint.route("/api-key", methods=["POST", "GET"]) @jwt_required() -def apikey(): +def apikey(session=Session()): """Change and retrieve API-Key""" current_user = get_jwt_identity() with Session() as session: - user = session.query(User).filter(User.username == current_user).first() + user = session.query(User).filter_by(username=current_user).first() if request.method == "GET": api_key = user.session_hash return jsonify({"apikey": api_key}), 200 @@ -196,7 +193,7 @@ def apikey(): @user_blueprint.route("/delete", methods=["GET", "POST"]) @jwt_required() -def delete_user(): +def delete_user(session=Session()): """Delete current user: Frontend and functionality not decided yet""" # current_user = get_jwt_identity() # user = session.query(User).filter(User.email == current_user).first() @@ -206,42 +203,44 @@ def delete_user(): @user_blueprint.route("/forgot-token", methods=["POST"]) -def forgot_token(): +def forgot_token(session=Session()): """Check for forgotten_password_code""" data = request.get_json() - with Session() as session: - user = user_from_token(session, data, "forgotten_password_code") - if user is not None: - return jsonify({"msg": "token confirmed"}), 200 - else: - return jsonify({"msg": "Error"}), 401 + try: + user = user_from_token(data, "forgotten_password_code", session) + if user: + # user verified by token + return jsonify({"msg": "Token confirmed"}), 200 + except ValueError as e: + print(e) + + # user could not be verified by token + return jsonify({"msg": "Error"}), 401 @user_blueprint.route("/resetpassword", methods=["POST"]) -def reset(): +def reset(session=Session()): """Changes user password""" data = request.get_json() - with Session() as session: - user = user_from_token(session, data, "forgotten_password_code") - user.set_password(data["password"]) - session.merge(user) - session.commit() + user = user_from_token(data, "forgotten_password_code", session) + user.set_password(data["password"]) + session.merge(user) + session.commit() return jsonify({"msg": "password changed"}), 200 @user_blueprint.route("/confirmation", methods=["POST"]) -def confirm_user(): +def confirm_user(session=Session()): """Activates user""" data = request.get_json() - with Session() as session: - user = user_from_token(session, data, "activation_code") - user.update_activation() - session.merge(user) - session.commit() + user = user_from_token(data, "activation_code", session) + user.update_activation() + session.merge(user) + session.commit() return jsonify({"msg": "User confirmed"}), 200 -def user_from_token(session: Session, data, token_name): +def user_from_token(data, token_name, session=Session()): url = data["url"] parsed = urlparse(url) (token, ) = parse_qs(parsed.query)["token"] diff --git a/tests/test_user_routes.py b/tests/test_user_routes.py index 49ffdc7a..836366bd 100644 --- a/tests/test_user_routes.py +++ b/tests/test_user_routes.py @@ -9,7 +9,6 @@ @pytest.fixture def db_session(test_client, valid_user, unconfirmed_user): # setup - print("setup") session = Session() session.add(valid_user) session.add(unconfirmed_user) @@ -17,11 +16,9 @@ def db_session(test_client, valid_user, unconfirmed_user): try: # test - print("test") yield session finally: # cleanup - print("cleanup") session.delete(valid_user) session.delete(unconfirmed_user) session.commit() @@ -135,7 +132,6 @@ def test_get_profile(test_client, init_database, db_session, valid_user): def test_profile_changes(test_client, init_database, db_session, valid_user): login(test_client, valid_user.email, "abcabc") - print(db_session, "HELLO2") access_token = str(os.environ.get("TEST_ACCESS_TOKEN")) headers = {"Authorization": "Bearer {}".format(access_token)} From cd6e7d3d62e2431581f2cb6b27df916afa538d65 Mon Sep 17 00:00:00 2001 From: Omosola Odetunde Date: Thu, 18 Sep 2025 00:04:07 +0200 Subject: [PATCH 07/16] fix: UI new API key display/refresh --- .../src/client/app/src/pages/auth/APIKey.js | 8 +++---- server/user/views.py | 22 ++++++++++--------- tests/test_user_routes.py | 1 + 3 files changed, 17 insertions(+), 14 deletions(-) diff --git a/server/src/client/app/src/pages/auth/APIKey.js b/server/src/client/app/src/pages/auth/APIKey.js index ac65cdde..2e3b81af 100644 --- a/server/src/client/app/src/pages/auth/APIKey.js +++ b/server/src/client/app/src/pages/auth/APIKey.js @@ -44,10 +44,10 @@ function CopyableAPIKey({ apikey }) { function APIKey() { const [apikey, setApikey] = useState(''); - const yourConfig = { +const yourConfig = { headers: { - Authorization: "Bearer " + localStorage.getItem("token"), - }, + Authorization: "Bearer " + localStorage.getItem("token") + } }; useEffect(() => { @@ -66,7 +66,7 @@ function APIKey() { axios .post( process.env.REACT_APP_URL_SITE_BACKEND + "api-key", - { resetapikey: true }, + {}, // no form data required yourConfig ) .then((response) => { diff --git a/server/user/views.py b/server/user/views.py index e3301738..f5d998ca 100644 --- a/server/user/views.py +++ b/server/user/views.py @@ -179,16 +179,18 @@ def logout(session=Session()): def apikey(session=Session()): """Change and retrieve API-Key""" current_user = get_jwt_identity() - with Session() as session: - user = session.query(User).filter_by(username=current_user).first() - if request.method == "GET": - api_key = user.session_hash - return jsonify({"apikey": api_key}), 200 - elif request.method == "POST": - user.set_session_hash() - session.merge(user) - session.commit() - return jsonify({"msg": "API Key updated"}), 200 + user = session.query(User).filter_by(username=current_user).first() + if request.method == "GET": + api_key = user.session_hash + return jsonify({"apikey": api_key}), 200 + elif request.method == "POST": + user.set_session_hash() + session.merge(user) + session.commit() + return jsonify({ + "msg": "API Key updated", + "apikey": user.session_hash + }), 200 @user_blueprint.route("/delete", methods=["GET", "POST"]) diff --git a/tests/test_user_routes.py b/tests/test_user_routes.py index 836366bd..b0909d04 100644 --- a/tests/test_user_routes.py +++ b/tests/test_user_routes.py @@ -181,6 +181,7 @@ def test_api_key_post(test_client, init_database, db_session, valid_user): # confirm session hash has been updated assert valid_user.session_hash != old_session_hash + assert response.json["apikey"] == valid_user.session_hash assert response.json["msg"] == "API Key updated" assert response.status_code == 200 From f18a5378fce59022d96069a5f30603d1c7baef43 Mon Sep 17 00:00:00 2001 From: Omosola Odetunde Date: Thu, 18 Sep 2025 00:16:50 +0200 Subject: [PATCH 08/16] fix/test: minor updates to reset() and distinguishing test user fixtures more for test_reset_password --- server/user/views.py | 2 +- tests/test_user_routes.py | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/server/user/views.py b/server/user/views.py index f5d998ca..d8e160a9 100644 --- a/server/user/views.py +++ b/server/user/views.py @@ -228,7 +228,7 @@ def reset(session=Session()): user.set_password(data["password"]) session.merge(user) session.commit() - return jsonify({"msg": "password changed"}), 200 + return jsonify({"msg": "Password changed"}), 200 @user_blueprint.route("/confirmation", methods=["POST"]) diff --git a/tests/test_user_routes.py b/tests/test_user_routes.py index b0909d04..a96522e4 100644 --- a/tests/test_user_routes.py +++ b/tests/test_user_routes.py @@ -6,7 +6,7 @@ from server.user.models import User -@pytest.fixture +@pytest.fixture(scope="function") def db_session(test_client, valid_user, unconfirmed_user): # setup session = Session() @@ -35,7 +35,7 @@ def valid_user(): country="0000", bio="No Bio", session_hash="0000", - forgotten_password_code="0000", + forgotten_password_code="1234", active=1 ) user.set_password("abcabc") @@ -55,7 +55,7 @@ def unconfirmed_user(): bio="No Bio", session_hash="0000", activation_code="0000", - forgotten_password_code="0000", + forgotten_password_code="5678", active=0 ) user.set_password("ff") @@ -231,6 +231,8 @@ def test_reset_password(test_client, init_database, db_session, valid_user): ) db_session.refresh(valid_user) + print(valid_user.id) assert valid_user.check_password(new_password) + assert response.json["msg"] == "Password changed" assert response.status_code == 200 From 67c57ee6e066a3c9514498d32398f7b3afe6ce7a Mon Sep 17 00:00:00 2001 From: Omosola Odetunde Date: Sat, 20 Sep 2025 09:54:35 +0200 Subject: [PATCH 09/16] feat: adding password visibility toggle feature for signup flow and refactor of Signup module --- .../src/client/app/src/pages/auth/SignUp.js | 135 +++++++++++++----- 1 file changed, 102 insertions(+), 33 deletions(-) diff --git a/server/src/client/app/src/pages/auth/SignUp.js b/server/src/client/app/src/pages/auth/SignUp.js index 56d596ab..7ba1d4ff 100755 --- a/server/src/client/app/src/pages/auth/SignUp.js +++ b/server/src/client/app/src/pages/auth/SignUp.js @@ -7,12 +7,18 @@ import { useState } from "react"; import { FormControl, + IconButton, Input, InputLabel, + InputAdornment, Button as MuiButton, Paper, Typography } from "@mui/material"; +import { + Visibility, + VisibilityOff +} from '@mui/icons-material'; import { spacing } from "@mui/system"; import axios from "axios"; @@ -35,48 +41,78 @@ function SignUp() { const [duplicateUser, setDuplicateUser] = useState(false); const [error, setError] = useState(false); const [errormessage, setErrorMessage] = useState(false); - function sendflask(event) { + + const [firstName, setFirstName] = useState(""); + const [lastName, setLastName] = useState(""); + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + + const [showPassword, setShowPassword] = useState(false); + + function handleSubmit(event) { event.preventDefault(); - console.log(event.target.email.value); - console.log(event.target.password.value); - if (event.target.password.value.length < 8) { + var registrationData = { + email: email, + firstName: firstName, + lastName: lastName, + password: password + }; + + console.log("Registration data: ", registrationData); + + if (password.length < 8) { + // Password must be 8+ characters setError(true); setErrorMessage("Password too weak. Use at least 8 characters, with numbers, digits, and special characters."); - } else if ( - /[a-zA-Z0-9]+@(?:[a-zA-Z0-9-]+\.)+[A-Za-z]+$/.test( - event.target.email.value - ) !== true - ) { + } else if ( (/[a-zA-Z0-9]+@(?:[a-zA-Z0-9-]+\.)+[A-Za-z]+$/.test(email)) === false) { + // Email must be in valid format setError(true); setErrorMessage("Please enter valid email"); } else { - axios - .post(process.env.REACT_APP_URL_SITE_BACKEND + "signup", { - first_name: event.target.fname.value, - last_name: event.target.lname.value, - email: event.target.email.value, - password: event.target.password.value - }) - .then(function(response) { - if (response.data.msg === "User created") { - console.log(response.data); - setRegister(true); - } else if (response.data.msg === "User already exists") { - setDuplicateUser(true); - } - }) - .catch(function(error) { - console.log(error.data); - }); + sendflask(registrationData); } + return false; } + + function handleMouseDownPassword(event) { + event.preventDefault(); // Prevents focus loss + } + + function handleClickShowPassword() { + setShowPassword(function(prev) { + return !prev; + }); + } + + function sendflask(registrationData) { + axios + .post(process.env.REACT_APP_URL_SITE_BACKEND + "signup", { + first_name: registrationData.firstName, + last_name: registrationData.lastName, + email: registrationData.email, + password: registrationData.password + }) + .then(function(response) { + if (response.data.msg === "User created") { + console.log(response.data); + setRegister(true); + } else if (response.data.msg === "User alredy exists") { + setDuplicateUser(true); + } + }) + .catch(function(error) { + console.log(error.data); + }) + } + return ( Almost there -
+ + {/* Error Banner */} {duplicateUser && ( User already exists @@ -87,31 +123,64 @@ function SignUp() { {errormessage} )} + + {/* Input fields */} First name - + Last name - + Email Address (we never share your email) - + Password (min 8 characters) + + {showPassword ? : } + + + } /> +