Merge pull request 'feat(api): add full signup/login flow with email & profile, ENV support, and port fix' (#18) from refactor/username into main
Reviewed-on: #18
This commit is contained in:
		
						commit
						db6f26df5d
					
				
					 7 changed files with 106 additions and 62 deletions
				
			
		|  | @ -2,7 +2,7 @@ | ||||||
| __pycache__/ | __pycache__/ | ||||||
| *.py[cod] | *.py[cod] | ||||||
| *.log | *.log | ||||||
| .env | !.env | ||||||
| venv/ | venv/ | ||||||
| .venv/ | .venv/ | ||||||
| dist/ | dist/ | ||||||
|  |  | ||||||
							
								
								
									
										16
									
								
								Dockerfile
									
										
									
									
									
								
							
							
						
						
									
										16
									
								
								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 -r requirements.txt -w /wheels && \ | ||||||
|     pip wheel --no-deps gunicorn -w /wheels |     pip wheel --no-deps gunicorn -w /wheels | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
| FROM python:3.10-slim AS runtime | FROM python:3.10-slim AS runtime | ||||||
| 
 | 
 | ||||||
| ENV PYTHONDONTWRITEBYTECODE=1 \ | ENV PYTHONDONTWRITEBYTECODE=1 \ | ||||||
|     PYTHONUNBUFFERED=1 \ |     PYTHONUNBUFFERED=1 \ | ||||||
|     PIP_NO_CACHE_DIR=1 \ |     PIP_NO_CACHE_DIR=1 \ | ||||||
|     PORT=8000 \ |     PORT=5000 \ | ||||||
|     WSGI_MODULE=server:app \ |     WSGI_MODULE=server:app \ | ||||||
|     GUNICORN_WORKERS=2 \ |     GUNICORN_WORKERS=2 \ | ||||||
|     GUNICORN_THREADS=4 \ |     GUNICORN_THREADS=4 \ | ||||||
|     GUNICORN_TIMEOUT=60  |     GUNICORN_TIMEOUT=60 \ | ||||||
|  |     FLASK_APP=server.py | ||||||
| 
 | 
 | ||||||
| WORKDIR /app | WORKDIR /app | ||||||
| 
 | 
 | ||||||
|  | @ -35,13 +35,19 @@ RUN groupadd -g 10001 app && useradd -m -u 10001 -g app app | ||||||
| COPY --from=builder /wheels /wheels | COPY --from=builder /wheels /wheels | ||||||
| RUN pip install --no-cache-dir /wheels/* && rm -rf /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 | USER app | ||||||
| 
 | 
 | ||||||
| COPY --chown=app: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 \ | 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"] | 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"] | ||||||
|  | @ -8,6 +8,7 @@ class User(db.Model): | ||||||
|      |      | ||||||
|     id = db.Column(db.Integer, primary_key=True) |     id = db.Column(db.Integer, primary_key=True) | ||||||
|     username = db.Column(db.String(80), unique=True, nullable=False) |     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)     |     _password = db.Column("password", db.String(255), nullable=False)     | ||||||
|      |      | ||||||
|     profile = db.relationship('UserProfile', back_populates='user', uselist=False, cascade="all, delete-orphan") |     profile = db.relationship('UserProfile', back_populates='user', uselist=False, cascade="all, delete-orphan") | ||||||
|  |  | ||||||
|  | @ -1,14 +1,13 @@ | ||||||
| from models import db | from models import db | ||||||
| 
 | 
 | ||||||
| class UserProfile(db.Model): | class UserProfile(db.Model): | ||||||
|     __tablename__ = 'user_profile' |     __tablename__ = 'user_profiles' | ||||||
|      |      | ||||||
|     id = db.Column(db.Integer, primary_key=True) |     id = db.Column(db.Integer, primary_key=True) | ||||||
|     user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False) |     user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False) | ||||||
|     first_name = db.Column(db.String(80), nullable = False) |     first_name = db.Column(db.String(50), nullable=False, default="") | ||||||
|     last_name = db.Column(db.String(80), nullable = False) |     last_name = db.Column(db.String(50), nullable=False, default="") | ||||||
|     bio = db.Column(db.Text, nullable = True) |     bio = db.Column(db.Text, default="") | ||||||
|     profile_picture = db.Column(db.String(255), nullable = True) |     profile_picture = db.Column(db.String(255), default="") | ||||||
|      |      | ||||||
|     user = db.relationship('User', back_populates='profile') |     user = db.relationship('User', back_populates='profile') | ||||||
|      |  | ||||||
|  | @ -1,19 +1,35 @@ | ||||||
| from flask import Blueprint, request, jsonify, session | from flask import Blueprint, request, jsonify, session | ||||||
| from services.UserService.user import UserService | 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() | user_service = UserService() | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
| @auth_bp.route("/signup", methods=["POST"]) | @auth_bp.route("/signup", methods=["POST"]) | ||||||
| def signup(): | def signup(): | ||||||
|     data = request.get_json() |     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: |     try: | ||||||
|         new_user = user_service.create_user(data["username"], data["password"]) |         new_user = user_service.create_user( | ||||||
|         return ( |             username=data["username"], | ||||||
|             jsonify({"message": "User created successfully", "username": new_user.username}), |             password=data["password"], | ||||||
|             201, |             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: |     except ValueError as e: | ||||||
|         return jsonify({"message": str(e)}), 400 |         return jsonify({"message": str(e)}), 400 | ||||||
|     except Exception as e: |     except Exception as e: | ||||||
|  | @ -21,15 +37,12 @@ def signup(): | ||||||
|         print(f"Signup error: {e}") |         print(f"Signup error: {e}") | ||||||
|         return jsonify({"message": "Internal server error"}), 500 |         return jsonify({"message": "Internal server error"}), 500 | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
| @auth_bp.route("/login", methods=["POST"]) | @auth_bp.route("/login", methods=["POST"]) | ||||||
| def login(): | def login(): | ||||||
|     data = request.get_json() |     data = request.get_json() | ||||||
|     username = data.get("username") |     username = data.get("username") | ||||||
|     password = data.get("password") |     password = data.get("password") | ||||||
| 
 |  | ||||||
|     print(f"Login attempt: username={username}, password={password}") |     print(f"Login attempt: username={username}, password={password}") | ||||||
| 
 |  | ||||||
|     try: |     try: | ||||||
|         user = user_service.verify_user(username, password) |         user = user_service.verify_user(username, password) | ||||||
|         session["user_id"] = user.id |         session["user_id"] = user.id | ||||||
|  | @ -41,7 +54,6 @@ def login(): | ||||||
|         print(f"Login error: {e}") |         print(f"Login error: {e}") | ||||||
|         return jsonify({"error": "Internal server error"}), 500 |         return jsonify({"error": "Internal server error"}), 500 | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
| @auth_bp.route("/logout", methods=["POST"]) | @auth_bp.route("/logout", methods=["POST"]) | ||||||
| def logout(): | def logout(): | ||||||
|     session.clear() |     session.clear() | ||||||
|  |  | ||||||
							
								
								
									
										8
									
								
								scripts/migrate.sh
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								scripts/migrate.sh
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,8 @@ | ||||||
|  | #!/bin/bash | ||||||
|  | set -e | ||||||
|  | 
 | ||||||
|  | echo "Running database migrations..." | ||||||
|  | flask db upgrade | ||||||
|  | 
 | ||||||
|  | echo "Starting application..." | ||||||
|  | exec "$@" | ||||||
|  | @ -1,42 +1,60 @@ | ||||||
| from models.User.user import User, db | from models.User.user import User | ||||||
| import logging | from models.UserProfile.user_profile import UserProfile | ||||||
| 
 | from models import db | ||||||
| logger = logging.getLogger(__name__) | import re | ||||||
| 
 |  | ||||||
| 
 | 
 | ||||||
| class UserService: | 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: |         if not username or not password: | ||||||
|             raise ValueError("Username and password are required") |             raise ValueError("Username and password are required") | ||||||
|          |          | ||||||
|         if len(username) < 3 or len(password) < 8: |         if email: | ||||||
|             raise ValueError( |             email_regex = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$' | ||||||
|                 "Username must be at least 3 characters and password must be at least 8 characters." |             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: | ||||||
|  |             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 | ||||||
|             ) |             ) | ||||||
|              |              | ||||||
|         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.add(new_user) | ||||||
|         try: |             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() |             db.session.commit() | ||||||
|  |              | ||||||
|  |             return new_user | ||||||
|  |              | ||||||
|         except Exception as e: |         except Exception as e: | ||||||
|             db.session.rollback() |             db.session.rollback() | ||||||
|             logger.error(f"Error creating user: {e}") |             raise Exception(f"Error creating user: {str(e)}") | ||||||
|             raise ValueError("Could not create user") from e |  | ||||||
|         return new_user |  | ||||||
|      |      | ||||||
|     def verify_user(self, username, password): |     def verify_user(self, username, password): | ||||||
|         user = User.query.filter_by(username=username).first() |         user = User.query.filter_by(username=username).first() | ||||||
|         if not user: |         if not user or not user.check_password(password): | ||||||
|             logger.warning(f"User not found: {username}") |  | ||||||
|             raise ValueError("Invalid username or 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 | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue