From f624792a2ff0a93d7f2be339e98844a9bb64fdf3 Mon Sep 17 00:00:00 2001 From: Blake Ridgway Date: Sat, 15 Feb 2025 21:15:58 -0600 Subject: [PATCH 1/8] (feat): input validation, improved error handling, and additional security measures. --- routes/auth.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/routes/auth.py b/routes/auth.py index f73a97b..82c6aab 100644 --- a/routes/auth.py +++ b/routes/auth.py @@ -6,10 +6,16 @@ user_service = UserService() @auth_bp.route('/signup', methods=['POST']) def signup(): - data = request.json + data = request.get_json() username = data.get('username') password = data.get('password') + if not username or not password: + return jsonify({"error": "Username and password are required"}), 400 + + if len(username) < 3 or len(password) < 8: + return jsonify({"error": "Username must be at least 3 characters and password must be at least 8 characters."}), 400 + try: new_user = user_service.create_user(username, password) return jsonify({"message": "User created successfully", "username": new_user.username}), 201 @@ -18,10 +24,13 @@ def signup(): @auth_bp.route('/login', methods=['POST']) def login(): - data = request.json + data = request.get_json() username = data.get('username') password = data.get('password') + if not username or not password: + return jsonify({"error": "Username and password are required"}), 400 + try: user = user_service.verify_user(username, password) return jsonify({"message": "Login successful", "user_id": user.id}), 200 From 356058e1f93ef4afd7bf10ee1223a031a13ce427 Mon Sep 17 00:00:00 2001 From: Blake Ridgway Date: Sat, 15 Feb 2025 21:30:23 -0600 Subject: [PATCH 2/8] (feaet): Refactor auth routes to use UserService --- routes/auth.py | 20 +++----------------- services/user.py | 12 ++++++++++-- 2 files changed, 13 insertions(+), 19 deletions(-) diff --git a/routes/auth.py b/routes/auth.py index 82c6aab..06ece83 100644 --- a/routes/auth.py +++ b/routes/auth.py @@ -1,3 +1,4 @@ +# routes/auth.py from flask import Blueprint, request, jsonify from services.user import UserService @@ -7,17 +8,8 @@ user_service = UserService() @auth_bp.route('/signup', methods=['POST']) def signup(): data = request.get_json() - username = data.get('username') - password = data.get('password') - - if not username or not password: - return jsonify({"error": "Username and password are required"}), 400 - - if len(username) < 3 or len(password) < 8: - return jsonify({"error": "Username must be at least 3 characters and password must be at least 8 characters."}), 400 - try: - new_user = user_service.create_user(username, password) + new_user = user_service.create_user(data['username'], data['password']) return jsonify({"message": "User created successfully", "username": new_user.username}), 201 except ValueError as e: return jsonify({"message": str(e)}), 400 @@ -25,14 +17,8 @@ def signup(): @auth_bp.route('/login', methods=['POST']) def login(): data = request.get_json() - username = data.get('username') - password = data.get('password') - - if not username or not password: - return jsonify({"error": "Username and password are required"}), 400 - try: - user = user_service.verify_user(username, password) + user = user_service.verify_user(data['username'], data['password']) return jsonify({"message": "Login successful", "user_id": user.id}), 200 except ValueError as e: return jsonify({"error": str(e)}), 401 diff --git a/services/user.py b/services/user.py index 60754df..9c86d08 100644 --- a/services/user.py +++ b/services/user.py @@ -1,13 +1,21 @@ -from werkzeug.security import generate_password_hash, check_password_hash from models.user import User, db +from werkzeug.security import generate_password_hash, check_password_hash class UserService: def create_user(self, username, password): + if not username or not password: + return jsonify({"error": "Username and password are required"}), 400 + + if len(username) < 3 or len(password) < 8: + return jsonify({"error": "Username must be at least 3 characters and password must be at least 8 characters."}), 400 + + existing_user = User.query.filter_by(username=username).first() if existing_user: raise ValueError("User already exists") - new_user = User(username=username, password=password) + hashed_password = generate_password_hash(password) + new_user = User(username=username, password=hashed_password) db.session.add(new_user) db.session.commit() return new_user From d13c5885d8e38def193b4791a46785d3a13d9734 Mon Sep 17 00:00:00 2001 From: Blake Ridgway Date: Sat, 15 Feb 2025 21:54:25 -0600 Subject: [PATCH 3/8] (refactor) boilerplate .gitignore, cleaned up folders --- .gitignore | 178 ++++++++++++++++++++++++++++- models/{ => User}/user.py | 0 routes/{ => user_auth}/auth.py | 2 +- server.py | 2 +- services/{ => UserService}/user.py | 2 +- 5 files changed, 176 insertions(+), 8 deletions(-) rename models/{ => User}/user.py (100%) rename routes/{ => user_auth}/auth.py (94%) rename services/{ => UserService}/user.py (96%) diff --git a/.gitignore b/.gitignore index 27b4c81..1800114 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,174 @@ -venv/ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# UV +# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +#uv.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/latest/usage/project/#working-with-version-control +.pdm.toml +.pdm-python +.pdm-build/ + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments .env -instance -models/__pycache__ -routes/__pycache__ -services/__pycache__ +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +# Ruff stuff: +.ruff_cache/ + +# PyPI configuration file +.pypirc \ No newline at end of file diff --git a/models/user.py b/models/User/user.py similarity index 100% rename from models/user.py rename to models/User/user.py diff --git a/routes/auth.py b/routes/user_auth/auth.py similarity index 94% rename from routes/auth.py rename to routes/user_auth/auth.py index 06ece83..051900b 100644 --- a/routes/auth.py +++ b/routes/user_auth/auth.py @@ -1,6 +1,6 @@ # routes/auth.py from flask import Blueprint, request, jsonify -from services.user import UserService +from services.UserService.user import UserService auth_bp = Blueprint('auth', __name__) user_service = UserService() diff --git a/server.py b/server.py index d78ab25..7be776b 100644 --- a/server.py +++ b/server.py @@ -1,7 +1,7 @@ import os from flask import Flask from models import db, init_db -from routes.auth import auth_bp +from routes.user_auth.auth import auth_bp from dotenv import load_dotenv from flask_cors import CORS diff --git a/services/user.py b/services/UserService/user.py similarity index 96% rename from services/user.py rename to services/UserService/user.py index 9c86d08..fe59325 100644 --- a/services/user.py +++ b/services/UserService/user.py @@ -1,4 +1,4 @@ -from models.user import User, db +from models.User.user import User, db from werkzeug.security import generate_password_hash, check_password_hash class UserService: From 4a4d693d72d4d88162178bd7a7bfe49b14299cfc Mon Sep 17 00:00:00 2001 From: Blake Ridgway Date: Sat, 15 Feb 2025 22:42:50 -0600 Subject: [PATCH 4/8] fix: resolve AttributeError in User model and ensure consistent password handling - Fixed the `AttributeError: 'User' object has no attribute '_password'` by properly mapping the `_password` attribute to the `password` column in the database. - Updated the `User` model to ensure passwords are only hashed once during creation and not re-hashed when retrieved or updated. - Improved the `check_password` method to correctly compare hashed passwords. - Verified the signup and login flow to ensure consistent behavior --- models/User/user.py | 23 ++++++++++++----------- routes/user_auth/auth.py | 14 +++++++++++++- server.py | 1 + services/UserService/user.py | 17 ++++++++++------- 4 files changed, 36 insertions(+), 19 deletions(-) diff --git a/models/User/user.py b/models/User/user.py index a45bbc5..1c4d50b 100644 --- a/models/User/user.py +++ b/models/User/user.py @@ -1,22 +1,23 @@ -from models import db from werkzeug.security import generate_password_hash, check_password_hash +from models import db class User(db.Model): __tablename__ = 'users' id = db.Column(db.Integer, primary_key=True) username = db.Column(db.String(80), unique=True, nullable=False) - password = db.Column(db.String(128), nullable=False) + _password = db.Column("password", db.String(255), nullable=False) - def __init__(self, username, password, hash_password=True): - self.username = username - if hash_password: - self.password = generate_password_hash(password, method="pbkdf2:sha256") + @property + def password(self): + return self._password + + @password.setter + def password(self, raw_password): + if not raw_password.startswith("pbkdf2:sha256:"): + self._password = generate_password_hash(raw_password) else: - self.password = password + self._password = raw_password def check_password(self, password): - return check_password_hash(self.password, password) - - def __repr__(self): - return f"" \ No newline at end of file + return check_password_hash(self._password, password) \ No newline at end of file diff --git a/routes/user_auth/auth.py b/routes/user_auth/auth.py index 051900b..ed8017c 100644 --- a/routes/user_auth/auth.py +++ b/routes/user_auth/auth.py @@ -1,5 +1,5 @@ # routes/auth.py -from flask import Blueprint, request, jsonify +from flask import Blueprint, request, jsonify, session from services.UserService.user import UserService auth_bp = Blueprint('auth', __name__) @@ -17,8 +17,20 @@ def signup(): @auth_bp.route('/login', methods=['POST']) def login(): data = request.get_json() + username = data.get('username') + password = data.get('password') + + print(f"Login attempt: username={username}, password={password}") + try: user = user_service.verify_user(data['username'], data['password']) + session['user_id'] = user.id return jsonify({"message": "Login successful", "user_id": user.id}), 200 except ValueError as e: + print(f"Login failed: {str(e)}") return jsonify({"error": str(e)}), 401 + +@auth_bp.route('/logout', methods=['POST']) +def logout(): + session.clear() + return jsonify({"message": "Logout successful"}), 200 \ No newline at end of file diff --git a/server.py b/server.py index 7be776b..c952050 100644 --- a/server.py +++ b/server.py @@ -10,6 +10,7 @@ load_dotenv() app = Flask(__name__) CORS(app) +app.secret_key = os.getenv('SECRET_KEY') app.config['SQLALCHEMY_DATABASE_URI'] = os.getenv('DATABASE') app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False diff --git a/services/UserService/user.py b/services/UserService/user.py index fe59325..fd0aa3a 100644 --- a/services/UserService/user.py +++ b/services/UserService/user.py @@ -1,27 +1,30 @@ from models.User.user import User, db -from werkzeug.security import generate_password_hash, check_password_hash class UserService: def create_user(self, username, password): if not username or not password: - return jsonify({"error": "Username and password are required"}), 400 + raise ValueError("Username and password are required") if len(username) < 3 or len(password) < 8: - return jsonify({"error": "Username must be at least 3 characters and password must be at least 8 characters."}), 400 - + raise ValueError("Username must be at least 3 characters and password must be at least 8 characters.") existing_user = User.query.filter_by(username=username).first() if existing_user: raise ValueError("User already exists") - hashed_password = generate_password_hash(password) - new_user = User(username=username, password=hashed_password) + new_user = User(username=username, password=password) db.session.add(new_user) db.session.commit() return new_user def verify_user(self, username, password): user = User.query.filter_by(username=username).first() - if not user or not user.check_password(password): + if not user: + print(f"User not found: {username}") raise ValueError("Invalid username or password") + + if not user.check_password(password): + raise ValueError("Invalid username or password") + + print(f"User verified: {username}") return user From 545b31a15fefc638298bfd73e743480cb0587ef6 Mon Sep 17 00:00:00 2001 From: Blake Ridgway Date: Mon, 17 Feb 2025 19:46:36 -0600 Subject: [PATCH 5/8] feat: Added user profile and creation of profile --- models/User/user.py | 18 +++++++++++++++++- models/UserProfile/user_profile.py | 14 ++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 models/UserProfile/user_profile.py diff --git a/models/User/user.py b/models/User/user.py index 1c4d50b..d6f01a4 100644 --- a/models/User/user.py +++ b/models/User/user.py @@ -1,5 +1,7 @@ +from models.UserProfile.user_profile import UserProfile from werkzeug.security import generate_password_hash, check_password_hash from models import db +from sqlalchemy import event class User(db.Model): __tablename__ = 'users' @@ -8,6 +10,8 @@ class User(db.Model): username = db.Column(db.String(80), unique=True, nullable=False) _password = db.Column("password", db.String(255), nullable=False) + profile = db.relationship('UserProfile', back_populates='user', uselist=False, cascade="all, delete-orphan") + @property def password(self): return self._password @@ -20,4 +24,16 @@ class User(db.Model): self._password = raw_password def check_password(self, password): - return check_password_hash(self._password, password) \ No newline at end of file + return check_password_hash(self._password, password) + +@event.listens_for(User, 'after_insert') +def create_user_profile(mapper, connection, target): + connection.execute( + UserProfile.__table__.insert().values ( + user_id = target.id, + first_name = "", + last_name = "", + bio = "", + profile_picture = "" + ) + ) \ No newline at end of file diff --git a/models/UserProfile/user_profile.py b/models/UserProfile/user_profile.py new file mode 100644 index 0000000..2063b39 --- /dev/null +++ b/models/UserProfile/user_profile.py @@ -0,0 +1,14 @@ +from models import db + +class UserProfile(db.Model): + __tablename__ = 'user_profile' + + id = db.Column(db.Integer, primary_key = True) + user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable = False) + first_name = db.Column(db.String(80), nullable = False) + last_name = db.Column(db.String(80), nullable = False) + bio = db.Column(db.Text, nullable = True) + profile_picture = db.Column(db.String(255), nullable = True) + + user = db.relationship('User', back_populates='profile') + \ No newline at end of file From afac3b628144caa2225801c5442d779e397e54d0 Mon Sep 17 00:00:00 2001 From: Blake Ridgway Date: Thu, 3 Apr 2025 10:38:51 -0500 Subject: [PATCH 6/8] refactor: Improve server setup and add health check Added /health endpoint for monitoring Improved readability and config setup Imported auth_bp directly --- server.py | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/server.py b/server.py index c952050..1b00247 100644 --- a/server.py +++ b/server.py @@ -1,25 +1,32 @@ import os from flask import Flask -from models import db, init_db -from routes.user_auth.auth import auth_bp -from dotenv import load_dotenv from flask_cors import CORS +from dotenv import load_dotenv + +from models import db, init_db +from routes.user_auth import auth load_dotenv() app = Flask(__name__) -CORS(app) +app.config["SECRET_KEY"] = os.getenv("SECRET_KEY") +app.config["SQLALCHEMY_DATABASE_URI"] = os.getenv("DATABASE") +app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False -app.secret_key = os.getenv('SECRET_KEY') -app.config['SQLALCHEMY_DATABASE_URI'] = os.getenv('DATABASE') -app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False +CORS(app) # Consider specific origins in production init_db(app) +app.register_blueprint(auth.auth_bp) + + +@app.route("/health") +def health_check(): + """Health check endpoint.""" + return "OK", 200 -app.register_blueprint(auth_bp) with app.app_context(): db.create_all() -if __name__ == '__main__': +if __name__ == "__main__": app.run(debug=True) From de5bff5935082c0fa87a96926a4dd6fe4148f504 Mon Sep 17 00:00:00 2001 From: Blake Ridgway Date: Thu, 3 Apr 2025 10:39:56 -0500 Subject: [PATCH 7/8] (refactor): Enhance error handling and route structure Added url_prefix to auth blueprint Implemented comprehensive exception handling with 500 errors Improved code clarity and structure --- routes/user_auth/auth.py | 42 ++++++++++++++++++++++++++-------------- 1 file changed, 27 insertions(+), 15 deletions(-) diff --git a/routes/user_auth/auth.py b/routes/user_auth/auth.py index ed8017c..366642f 100644 --- a/routes/user_auth/auth.py +++ b/routes/user_auth/auth.py @@ -1,36 +1,48 @@ -# routes/auth.py from flask import Blueprint, request, jsonify, session from services.UserService.user import UserService -auth_bp = Blueprint('auth', __name__) +auth_bp = Blueprint("auth", __name__, url_prefix="/auth") user_service = UserService() -@auth_bp.route('/signup', methods=['POST']) + +@auth_bp.route("/signup", methods=["POST"]) def signup(): data = request.get_json() try: - new_user = user_service.create_user(data['username'], data['password']) - return jsonify({"message": "User created successfully", "username": new_user.username}), 201 + new_user = user_service.create_user(data["username"], data["password"]) + return ( + jsonify({"message": "User created successfully", "username": new_user.username}), + 201, + ) except ValueError as e: return jsonify({"message": str(e)}), 400 + except Exception as e: + # Log the error + print(f"Signup error: {e}") + return jsonify({"message": "Internal server error"}), 500 -@auth_bp.route('/login', methods=['POST']) + +@auth_bp.route("/login", methods=["POST"]) def login(): data = request.get_json() - username = data.get('username') - password = data.get('password') - + username = data.get("username") + password = data.get("password") + print(f"Login attempt: username={username}, password={password}") - + try: - user = user_service.verify_user(data['username'], data['password']) - session['user_id'] = user.id + user = user_service.verify_user(username, password) + session["user_id"] = user.id return jsonify({"message": "Login successful", "user_id": user.id}), 200 except ValueError as e: print(f"Login failed: {str(e)}") return jsonify({"error": str(e)}), 401 - -@auth_bp.route('/logout', methods=['POST']) + except Exception as e: + print(f"Login error: {e}") + return jsonify({"error": "Internal server error"}), 500 + + +@auth_bp.route("/logout", methods=["POST"]) def logout(): session.clear() - return jsonify({"message": "Logout successful"}), 200 \ No newline at end of file + return jsonify({"message": "Logout successful"}), 200 From dc36b21dccf2d6c75957165e62f2437ca82abd5d Mon Sep 17 00:00:00 2001 From: Blake Ridgway Date: Thu, 3 Apr 2025 10:42:01 -0500 Subject: [PATCH 8/8] (refactor): Implement robust logging and database error handling Introduced logging for user operations Added database rollback on user creation failure Enhanced exception handling and error reporting --- services/UserService/user.py | 30 +++++++++++++++++++++--------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/services/UserService/user.py b/services/UserService/user.py index fd0aa3a..b6da4d7 100644 --- a/services/UserService/user.py +++ b/services/UserService/user.py @@ -1,30 +1,42 @@ from models.User.user import User, db +import logging + +logger = logging.getLogger(__name__) + class UserService: def create_user(self, username, password): if not username or not password: raise ValueError("Username and password are required") - + if len(username) < 3 or len(password) < 8: - raise ValueError("Username must be at least 3 characters and password must be at least 8 characters.") - + raise ValueError( + "Username must be at least 3 characters and password must be at least 8 characters." + ) + existing_user = User.query.filter_by(username=username).first() if existing_user: raise ValueError("User already exists") - + new_user = User(username=username, password=password) db.session.add(new_user) - db.session.commit() + try: + db.session.commit() + except Exception as e: + db.session.rollback() + logger.error(f"Error creating user: {e}") + raise ValueError("Could not create user") from e return new_user def verify_user(self, username, password): user = User.query.filter_by(username=username).first() if not user: - print(f"User not found: {username}") + logger.warning(f"User not found: {username}") raise ValueError("Invalid username or password") - + if not user.check_password(password): + logger.warning(f"Invalid password for user: {username}") raise ValueError("Invalid username or password") - - print(f"User verified: {username}") + + logger.info(f"User verified: {username}") return user