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/user.py b/models/User/user.py new file mode 100644 index 0000000..d6f01a4 --- /dev/null +++ b/models/User/user.py @@ -0,0 +1,39 @@ +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' + + id = db.Column(db.Integer, primary_key=True) + 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 + + @password.setter + def password(self, raw_password): + if not raw_password.startswith("pbkdf2:sha256:"): + self._password = generate_password_hash(raw_password) + else: + self._password = raw_password + + def check_password(self, password): + 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 diff --git a/models/user.py b/models/user.py deleted file mode 100644 index a45bbc5..0000000 --- a/models/user.py +++ /dev/null @@ -1,22 +0,0 @@ -from models import db -from werkzeug.security import generate_password_hash, check_password_hash - -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) - - def __init__(self, username, password, hash_password=True): - self.username = username - if hash_password: - self.password = generate_password_hash(password, method="pbkdf2:sha256") - else: - self.password = password - - def check_password(self, password): - return check_password_hash(self.password, password) - - def __repr__(self): - return f"" \ No newline at end of file diff --git a/routes/auth.py b/routes/auth.py deleted file mode 100644 index f73a97b..0000000 --- a/routes/auth.py +++ /dev/null @@ -1,29 +0,0 @@ -from flask import Blueprint, request, jsonify -from services.user import UserService - -auth_bp = Blueprint('auth', __name__) -user_service = UserService() - -@auth_bp.route('/signup', methods=['POST']) -def signup(): - data = request.json - username = data.get('username') - password = data.get('password') - - try: - new_user = user_service.create_user(username, password) - return jsonify({"message": "User created successfully", "username": new_user.username}), 201 - except ValueError as e: - return jsonify({"message": str(e)}), 400 - -@auth_bp.route('/login', methods=['POST']) -def login(): - data = request.json - username = data.get('username') - password = data.get('password') - - try: - user = user_service.verify_user(username, password) - return jsonify({"message": "Login successful", "user_id": user.id}), 200 - except ValueError as e: - return jsonify({"error": str(e)}), 401 diff --git a/routes/user_auth/auth.py b/routes/user_auth/auth.py new file mode 100644 index 0000000..366642f --- /dev/null +++ b/routes/user_auth/auth.py @@ -0,0 +1,48 @@ +from flask import Blueprint, request, jsonify, session +from services.UserService.user import UserService + +auth_bp = Blueprint("auth", __name__, url_prefix="/auth") +user_service = UserService() + + +@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, + ) + 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"]) +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(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 + 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 diff --git a/server.py b/server.py index d78ab25..1b00247 100644 --- a/server.py +++ b/server.py @@ -1,24 +1,32 @@ import os from flask import Flask -from models import db, init_db -from routes.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.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) diff --git a/services/UserService/user.py b/services/UserService/user.py new file mode 100644 index 0000000..b6da4d7 --- /dev/null +++ b/services/UserService/user.py @@ -0,0 +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." + ) + + 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) + 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: + 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") + + logger.info(f"User verified: {username}") + return user diff --git a/services/user.py b/services/user.py deleted file mode 100644 index 60754df..0000000 --- a/services/user.py +++ /dev/null @@ -1,19 +0,0 @@ -from werkzeug.security import generate_password_hash, check_password_hash -from models.user import User, db - -class UserService: - def create_user(self, username, password): - 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() - 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): - raise ValueError("Invalid username or password") - return user