Compare commits
	
		
			7 commits
		
	
	
		
			a8bcd5e249
			...
			db6f26df5d
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| db6f26df5d | |||
| 
							 | 
						ac0b09ad0e | ||
| 
							 | 
						0698ba5c1f | ||
| 
							 | 
						f5f86a2bc7 | ||
| 
							 | 
						52bb003980 | ||
| 
							 | 
						f396c98cbe | ||
| 
							 | 
						6c994db855 | 
					 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")
 | 
				
			||||||
| 
						 | 
					@ -29,11 +30,11 @@ class User(db.Model):
 | 
				
			||||||
@event.listens_for(User, 'after_insert')
 | 
					@event.listens_for(User, 'after_insert')
 | 
				
			||||||
def create_user_profile(mapper, connection, target):
 | 
					def create_user_profile(mapper, connection, target):
 | 
				
			||||||
    connection.execute(
 | 
					    connection.execute(
 | 
				
			||||||
        UserProfile.__table__.insert().values (
 | 
					        UserProfile.__table__.insert().values(
 | 
				
			||||||
            user_id = target.id,
 | 
					            user_id=target.id,
 | 
				
			||||||
            first_name = "",
 | 
					            first_name="",
 | 
				
			||||||
            last_name = "",
 | 
					            last_name="",
 | 
				
			||||||
            bio = "",
 | 
					            bio="",
 | 
				
			||||||
            profile_picture = ""
 | 
					            profile_picture=""
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
| 
						 | 
					@ -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()
 | 
					            db.session.add(new_user)
 | 
				
			||||||
        if existing_user:
 | 
					            db.session.flush()
 | 
				
			||||||
            raise ValueError("User already exists")
 | 
					 | 
				
			||||||
            
 | 
					            
 | 
				
			||||||
        new_user = User(username=username, password=password)
 | 
					            user_profile = UserProfile(
 | 
				
			||||||
        db.session.add(new_user)
 | 
					                user_id=new_user.id,
 | 
				
			||||||
        try:
 | 
					                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