diff --git a/.dockerignore b/.dockerignore index ce46172..d9b625e 100644 --- a/.dockerignore +++ b/.dockerignore @@ -2,7 +2,7 @@ __pycache__/ *.py[cod] *.log -.env +!.env venv/ .venv/ dist/ diff --git a/Dockerfile b/Dockerfile index 1468378..c72dff0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -16,17 +16,17 @@ RUN python -m pip install --upgrade pip && \ pip wheel --no-deps -r requirements.txt -w /wheels && \ pip wheel --no-deps gunicorn -w /wheels - FROM python:3.10-slim AS runtime ENV PYTHONDONTWRITEBYTECODE=1 \ PYTHONUNBUFFERED=1 \ PIP_NO_CACHE_DIR=1 \ - PORT=8000 \ + PORT=5000 \ WSGI_MODULE=server:app \ GUNICORN_WORKERS=2 \ GUNICORN_THREADS=4 \ - GUNICORN_TIMEOUT=60 + GUNICORN_TIMEOUT=60 \ + FLASK_APP=server.py WORKDIR /app @@ -35,13 +35,19 @@ RUN groupadd -g 10001 app && useradd -m -u 10001 -g app app COPY --from=builder /wheels /wheels RUN pip install --no-cache-dir /wheels/* && rm -rf /wheels +# Install python-dotenv if not already in requirements.txt +RUN pip install python-dotenv + USER app COPY --chown=app:app . . -EXPOSE 8000 +# Copy .env file specifically +COPY --chown=app:app .env .env + +EXPOSE 5000 HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \ - CMD python -c "import os,socket; s=socket.socket(); s.settimeout(2); s.connect(('127.0.0.1', int(os.getenv('PORT', '8000')))); s.close()" + CMD python -c "import os,socket; s=socket.socket(); s.settimeout(2); s.connect(('127.0.0.1', int(os.getenv('PORT', '5000')))); s.close()" CMD ["sh", "-c", "exec gunicorn $WSGI_MODULE --bind=0.0.0.0:$PORT --workers=$GUNICORN_WORKERS --threads=$GUNICORN_THREADS --timeout=$GUNICORN_TIMEOUT --access-logfile=- --error-logfile=- --keep-alive=5"] \ No newline at end of file diff --git a/models/User/user.py b/models/User/user.py index d6f01a4..552796c 100644 --- a/models/User/user.py +++ b/models/User/user.py @@ -8,6 +8,7 @@ class User(db.Model): id = db.Column(db.Integer, primary_key=True) username = db.Column(db.String(80), unique=True, nullable=False) + email = db.Column(db.String(120), unique=True, nullable=False) # Add email field _password = db.Column("password", db.String(255), nullable=False) profile = db.relationship('UserProfile', back_populates='user', uselist=False, cascade="all, delete-orphan") @@ -29,11 +30,11 @@ class User(db.Model): @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 = "" + 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 index 2063b39..d3fa194 100644 --- a/models/UserProfile/user_profile.py +++ b/models/UserProfile/user_profile.py @@ -1,14 +1,13 @@ from models import db class UserProfile(db.Model): - __tablename__ = 'user_profile' + __tablename__ = 'user_profiles' - 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) + 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(50), nullable=False, default="") + last_name = db.Column(db.String(50), nullable=False, default="") + bio = db.Column(db.Text, default="") + profile_picture = db.Column(db.String(255), default="") - user = db.relationship('User', back_populates='profile') - \ No newline at end of file + user = db.relationship('User', back_populates='profile') \ No newline at end of file diff --git a/routes/user_auth/auth.py b/routes/user_auth/auth.py index 366642f..899d7ba 100644 --- a/routes/user_auth/auth.py +++ b/routes/user_auth/auth.py @@ -1,19 +1,35 @@ from flask import Blueprint, request, jsonify, session from services.UserService.user import UserService -auth_bp = Blueprint("auth", __name__, url_prefix="/auth") +auth_bp = Blueprint("auth", __name__, url_prefix="/api") user_service = UserService() - @auth_bp.route("/signup", methods=["POST"]) def signup(): data = request.get_json() + if not data: + return jsonify({"message": "No data provided"}), 400 + + required_fields = ['username', 'password'] + for field in required_fields: + if not data.get(field): + return jsonify({"message": f"{field} is required"}), 400 + 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( + username=data["username"], + password=data["password"], + email=data.get("email"), + first_name=data.get("first_name"), + last_name=data.get("last_name") ) + + return jsonify({ + "message": "User created successfully", + "username": new_user.username, + "user_id": new_user.id + }), 201 + except ValueError as e: return jsonify({"message": str(e)}), 400 except Exception as e: @@ -21,15 +37,12 @@ def signup(): 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 @@ -41,8 +54,7 @@ def login(): 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 + return jsonify({"message": "Logout successful"}), 200 \ No newline at end of file diff --git a/scripts/migrate.sh b/scripts/migrate.sh new file mode 100644 index 0000000..405f399 --- /dev/null +++ b/scripts/migrate.sh @@ -0,0 +1,8 @@ +#!/bin/bash +set -e + +echo "Running database migrations..." +flask db upgrade + +echo "Starting application..." +exec "$@" \ No newline at end of file diff --git a/services/UserService/user.py b/services/UserService/user.py index b6da4d7..6f1c030 100644 --- a/services/UserService/user.py +++ b/services/UserService/user.py @@ -1,42 +1,60 @@ -from models.User.user import User, db -import logging - -logger = logging.getLogger(__name__) - +from models.User.user import User +from models.UserProfile.user_profile import UserProfile +from models import db +import re class UserService: - def create_user(self, username, password): + def create_user(self, username, password, email=None, first_name=None, last_name=None): 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 email: + email_regex = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$' + if not re.match(email_regex, email): + raise ValueError("Invalid email format") + + existing_user = User.query.filter( + (User.username == username) | (User.email == email) + ).first() + if existing_user: - raise ValueError("User already exists") - - new_user = User(username=username, password=password) - db.session.add(new_user) + if existing_user.username == username: + raise ValueError("Username already exists") + else: + raise ValueError("Email already exists") + + if len(password) < 8: + raise ValueError("Password must be at least 8 characters long") + try: + new_user = User( + username=username, + email=email or "", + password=password + ) + + db.session.add(new_user) + db.session.flush() + + user_profile = UserProfile( + user_id=new_user.id, + first_name=first_name or "", + last_name=last_name or "", + bio="", + profile_picture="" + ) + + db.session.add(user_profile) db.session.commit() + + return new_user + 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 - + raise Exception(f"Error creating user: {str(e)}") + def verify_user(self, username, password): user = User.query.filter_by(username=username).first() - if not user: - logger.warning(f"User not found: {username}") + if not user or not user.check_password(password): 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 + return user \ No newline at end of file