Merge branch 'v0.1.0-user-login'

This commit is contained in:
Blake Ridgway 2025-04-03 14:49:24 -05:00
commit 81d7a80758
9 changed files with 332 additions and 83 deletions

178
.gitignore vendored
View file

@ -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

39
models/User/user.py Normal file
View file

@ -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 = ""
)
)

View file

@ -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')

View file

@ -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"<User {self.username}>"

View file

@ -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

48
routes/user_auth/auth.py Normal file
View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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