From 270a009aed238893bfa1eb042b74a7118034d1f8 Mon Sep 17 00:00:00 2001 From: shivansh023023 Date: Wed, 18 Feb 2026 22:08:29 +0530 Subject: [PATCH 1/5] feat: implement JWT authentication and user registration system --- Backend/app/config.py | 11 +++++ Backend/app/main.py | 2 + Backend/app/models/users.py | 17 +++++++ Backend/app/routes/auth.py | 63 ++++++++++++++++++++++-- Backend/app/services/auth_service.py | 18 +++++++ Backend/app/services/get_current_user.py | 37 ++++++++++++++ 6 files changed, 143 insertions(+), 5 deletions(-) create mode 100644 Backend/app/services/auth_service.py create mode 100644 Backend/app/services/get_current_user.py diff --git a/Backend/app/config.py b/Backend/app/config.py index e69de29..f5cb971 100644 --- a/Backend/app/config.py +++ b/Backend/app/config.py @@ -0,0 +1,11 @@ +import os +from dotenv import load_dotenv + +load_dotenv() + +class Settings: + SECRET_KEY: str = os.getenv("SECRET_KEY", "your-super-secret-key-change-me") + ALGORITHM: str = "HS256" + ACCESS_TOKEN_EXPIRE_MINUTES: int = 60 * 24 # 24 hours + +settings = Settings() \ No newline at end of file diff --git a/Backend/app/main.py b/Backend/app/main.py index 86d892a..e641278 100644 --- a/Backend/app/main.py +++ b/Backend/app/main.py @@ -7,6 +7,7 @@ from .routes.chat import router as chat_router from .routes.match import router as match_router from sqlalchemy.exc import SQLAlchemyError +from .routes import auth import logging import os from dotenv import load_dotenv @@ -56,6 +57,7 @@ async def lifespan(app: FastAPI): app.include_router(match_router) app.include_router(ai.router) app.include_router(ai.youtube_router) +app.include_router(auth.router) @app.get("/") diff --git a/Backend/app/models/users.py b/Backend/app/models/users.py index e69de29..ae0e69d 100644 --- a/Backend/app/models/users.py +++ b/Backend/app/models/users.py @@ -0,0 +1,17 @@ +from sqlalchemy import Column, String, DateTime +from sqlalchemy.dialects.postgresql import UUID +import uuid +from datetime import datetime +from ..db.db import Base + +class User(Base): + __tablename__ = "users" + + id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + username = Column(String, unique=True, index=True, nullable=False) + email = Column(String, unique=True, index=True, nullable=False) + hashed_password = Column(String, nullable=False) + role = Column(String, default="creator") # 'creator' or 'brand' + profile_image = Column(String, nullable=True) + bio = Column(String, nullable=True) + created_at = Column(DateTime, default=datetime.utcnow) \ No newline at end of file diff --git a/Backend/app/routes/auth.py b/Backend/app/routes/auth.py index 19d59a2..461d648 100644 --- a/Backend/app/routes/auth.py +++ b/Backend/app/routes/auth.py @@ -1,7 +1,60 @@ -from fastapi import APIRouter +from fastapi import APIRouter, Depends, HTTPException, status +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.future import select +from ..db.db import get_db +from ..models.users import User +from ..services.auth_service import hash_password, verify_password, create_access_token +from pydantic import BaseModel, EmailStr +from ..services.get_current_user import get_current_user -router = APIRouter() +router = APIRouter(prefix="/auth", tags=["Authentication"]) -@router.get("/auth/ping") -def ping(): - return {"message": "Auth route is working!"} +# Pydantic schemas for data validation +class UserCreate(BaseModel): + username: str + email: EmailStr + password: str + +class UserLogin(BaseModel): + email: EmailStr + password: str + +@router.post("/signup") +async def signup(user_data: UserCreate, db: AsyncSession = Depends(get_db)): + # Check if user already exists + result = await db.execute(select(User).where(User.email == user_data.email)) + if result.scalars().first(): + raise HTTPException(status_code=400, detail="Email already registered") + + new_user = User( + username=user_data.username, + email=user_data.email, + hashed_password=hash_password(user_data.password) + ) + db.add(new_user) + await db.commit() + await db.refresh(new_user) + return {"message": "User created successfully"} + +@router.post("/login") +async def login(credentials: UserLogin, db: AsyncSession = Depends(get_db)): + result = await db.execute(select(User).where(User.email == credentials.email)) + user = result.scalars().first() + + if not user or not verify_password(credentials.password, user.hashed_password): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Incorrect email or password" + ) + + access_token = create_access_token(data={"sub": str(user.id)}) + return {"access_token": access_token, "token_type": "bearer"} + +@router.get("/me") +async def get_me(current_user: User = Depends(get_current_user)): + return { + "id": str(current_user.id), + "username": current_user.username, + "email": current_user.email, + "role": current_user.role + } \ No newline at end of file diff --git a/Backend/app/services/auth_service.py b/Backend/app/services/auth_service.py new file mode 100644 index 0000000..bbe1c11 --- /dev/null +++ b/Backend/app/services/auth_service.py @@ -0,0 +1,18 @@ +from datetime import datetime, timedelta +from jose import jwt +from passlib.context import CryptContext +from ..config import settings + +pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") + +def hash_password(password: str): + return pwd_context.hash(password) + +def verify_password(plain_password: str, hashed_password: str): + return pwd_context.verify(plain_password, hashed_password) + +def create_access_token(data: dict): + to_encode = data.copy() + expire = datetime.utcnow() + timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES) + to_encode.update({"exp": expire}) + return jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM) \ No newline at end of file diff --git a/Backend/app/services/get_current_user.py b/Backend/app/services/get_current_user.py new file mode 100644 index 0000000..56827aa --- /dev/null +++ b/Backend/app/services/get_current_user.py @@ -0,0 +1,37 @@ +from fastapi import Depends, HTTPException, status +from fastapi.security import OAuth2PasswordBearer +from jose import JWTError, jwt +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.future import select +from ..db.db import get_db +from ..models.users import User +from ..config import settings + +# This tells FastAPI where to look for the token (the /auth/login route) +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="auth/login") + +async def get_current_user( + token: str = Depends(oauth2_scheme), + db: AsyncSession = Depends(get_db) +): + credentials_exception = HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Could not validate credentials", + headers={"WWW-Authenticate": "Bearer"}, + ) + try: + # Decode the JWT token + payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM]) + user_id: str = payload.get("sub") + if user_id is None: + raise credentials_exception + except JWTError: + raise credentials_exception + + # Fetch the user from the DB + result = await db.execute(select(User).where(User.id == user_id)) + user = result.scalars().first() + + if user is None: + raise credentials_exception + return user \ No newline at end of file From 74898bff4cc4c14505310ed7fa4935fdffce7941 Mon Sep 17 00:00:00 2001 From: shivansh023023 Date: Wed, 18 Feb 2026 22:12:43 +0530 Subject: [PATCH 2/5] chore: add security dependencies to requirements.txt --- Backend/requirements.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Backend/requirements.txt b/Backend/requirements.txt index ea1ab73..ffa2f37 100644 --- a/Backend/requirements.txt +++ b/Backend/requirements.txt @@ -53,3 +53,6 @@ urllib3==2.3.0 uvicorn==0.34.0 websockets==14.2 yarl==1.18.3 +passlib[bcrypt]==1.7.4 +python-jose[cryptography]==3.3.0 +email-validator==2.1.0.post1 From 1a7997dedb7207e23f2b8a4091656f0dc3a3ee37 Mon Sep 17 00:00:00 2001 From: shivansh023023 Date: Wed, 18 Feb 2026 22:19:35 +0530 Subject: [PATCH 3/5] refactor: integrate JWT auth with existing User model and models.py --- Backend/app/models/models.py | 33 ++++++++++++++---------- Backend/app/models/users.py | 17 ------------ Backend/app/routes/auth.py | 2 +- Backend/app/services/get_current_user.py | 2 +- 4 files changed, 21 insertions(+), 33 deletions(-) delete mode 100644 Backend/app/models/users.py diff --git a/Backend/app/models/models.py b/Backend/app/models/models.py index 56681ab..c0991d4 100644 --- a/Backend/app/models/models.py +++ b/Backend/app/models/models.py @@ -26,17 +26,22 @@ class User(Base): __tablename__ = "users" id = Column(String, primary_key=True, default=generate_uuid) - username = Column(String, unique=True, nullable=False) - email = Column(String, unique=True, nullable=False) - # password_hash = Column(Text, nullable=False) # Removed as Supabase handles auth - role = Column(String, nullable=False) # 'creator' or 'brand' + # Added index=True to username and email for faster login lookups + username = Column(String, unique=True, index=True, nullable=False) + email = Column(String, unique=True, index=True, nullable=False) + + # Restored hashed_password for custom JWT authentication + hashed_password = Column(String, nullable=False) + + role = Column(String, nullable=False, default="creator") # 'creator' or 'brand' profile_image = Column(Text, nullable=True) bio = Column(Text, nullable=True) created_at = Column(TIMESTAMP, default=datetime.utcnow) - is_online = Column(Boolean, default=False) # ✅ Track if user is online + is_online = Column(Boolean, default=False) last_seen = Column(TIMESTAMP, default=datetime.utcnow) + # Existing Relationships audience = relationship("AudienceInsights", back_populates="user", uselist=False) sponsorships = relationship("Sponsorship", back_populates="brand") posts = relationship("UserPost", back_populates="user") @@ -66,7 +71,7 @@ class AudienceInsights(Base): time_of_attention = Column(Integer) # in seconds price_expectation = Column(DECIMAL(10, 2)) created_at = Column( - DateTime(timezone=True), default=lambda: datetime.now(timezone.utc) + DateTime(timezone=True), default=datetime.utcnow ) user = relationship("User", back_populates="audience") @@ -80,12 +85,12 @@ class Sponsorship(Base): brand_id = Column(String, ForeignKey("users.id"), nullable=False) title = Column(String, nullable=False) description = Column(Text, nullable=False) - required_audience = Column(JSON) # {"age": ["18-24"], "location": ["USA", "UK"]} + required_audience = Column(JSON) budget = Column(DECIMAL(10, 2)) engagement_minimum = Column(Float) status = Column(String, default="open") created_at = Column( - DateTime(timezone=True), default=lambda: datetime.now(timezone.utc) + DateTime(timezone=True), default=datetime.utcnow ) brand = relationship("User", back_populates="sponsorships") @@ -102,9 +107,9 @@ class UserPost(Base): content = Column(Text, nullable=False) post_url = Column(Text, nullable=True) category = Column(String, nullable=True) - engagement_metrics = Column(JSON) # {"likes": 500, "comments": 100, "shares": 50} + engagement_metrics = Column(JSON) created_at = Column( - DateTime(timezone=True), default=lambda: datetime.now(timezone.utc) + DateTime(timezone=True), default=datetime.utcnow ) user = relationship("User", back_populates="posts") @@ -121,7 +126,7 @@ class SponsorshipApplication(Base): proposal = Column(Text, nullable=False) status = Column(String, default="pending") applied_at = Column( - DateTime(timezone=True), default=lambda: datetime.now(timezone.utc) + DateTime(timezone=True), default=datetime.utcnow ) creator = relationship("User", back_populates="applications") @@ -138,7 +143,7 @@ class Collaboration(Base): collaboration_details = Column(Text, nullable=False) status = Column(String, default="pending") created_at = Column( - DateTime(timezone=True), default=lambda: datetime.now(timezone.utc) + DateTime(timezone=True), default=datetime.utcnow ) @@ -153,10 +158,10 @@ class SponsorshipPayment(Base): amount = Column(DECIMAL(10, 2), nullable=False) status = Column(String, default="pending") transaction_date = Column( - DateTime(timezone=True), default=lambda: datetime.now(timezone.utc) + DateTime(timezone=True), default=datetime.utcnow ) creator = relationship("User", foreign_keys=[creator_id], back_populates="payments") brand = relationship( "User", foreign_keys=[brand_id], back_populates="brand_payments" - ) + ) \ No newline at end of file diff --git a/Backend/app/models/users.py b/Backend/app/models/users.py deleted file mode 100644 index ae0e69d..0000000 --- a/Backend/app/models/users.py +++ /dev/null @@ -1,17 +0,0 @@ -from sqlalchemy import Column, String, DateTime -from sqlalchemy.dialects.postgresql import UUID -import uuid -from datetime import datetime -from ..db.db import Base - -class User(Base): - __tablename__ = "users" - - id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) - username = Column(String, unique=True, index=True, nullable=False) - email = Column(String, unique=True, index=True, nullable=False) - hashed_password = Column(String, nullable=False) - role = Column(String, default="creator") # 'creator' or 'brand' - profile_image = Column(String, nullable=True) - bio = Column(String, nullable=True) - created_at = Column(DateTime, default=datetime.utcnow) \ No newline at end of file diff --git a/Backend/app/routes/auth.py b/Backend/app/routes/auth.py index 461d648..c8b6383 100644 --- a/Backend/app/routes/auth.py +++ b/Backend/app/routes/auth.py @@ -2,7 +2,7 @@ from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.future import select from ..db.db import get_db -from ..models.users import User +from ..models.models import User from ..services.auth_service import hash_password, verify_password, create_access_token from pydantic import BaseModel, EmailStr from ..services.get_current_user import get_current_user diff --git a/Backend/app/services/get_current_user.py b/Backend/app/services/get_current_user.py index 56827aa..04e5007 100644 --- a/Backend/app/services/get_current_user.py +++ b/Backend/app/services/get_current_user.py @@ -4,7 +4,7 @@ from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.future import select from ..db.db import get_db -from ..models.users import User +from ..models.models import User from ..config import settings # This tells FastAPI where to look for the token (the /auth/login route) From 45576a4a313607d4c13e234d675ae9e7c7fdedf4 Mon Sep 17 00:00:00 2001 From: shivansh023023 Date: Wed, 18 Feb 2026 22:53:03 +0530 Subject: [PATCH 4/5] refactor: address security reviews, harden config, and add docstrings --- Backend/app/config.py | 14 ++++++++------ Backend/app/services/auth_service.py | 8 ++++++-- Backend/requirements.txt | 6 +++--- 3 files changed, 17 insertions(+), 11 deletions(-) diff --git a/Backend/app/config.py b/Backend/app/config.py index f5cb971..39f658d 100644 --- a/Backend/app/config.py +++ b/Backend/app/config.py @@ -1,11 +1,13 @@ import os -from dotenv import load_dotenv +from pydantic_settings import BaseSettings -load_dotenv() - -class Settings: - SECRET_KEY: str = os.getenv("SECRET_KEY", "your-super-secret-key-change-me") +class Settings(BaseSettings): + """Handles application configuration using environment variables.""" + # This will now throw an error if SECRET_KEY is missing from .env + SECRET_KEY: str ALGORITHM: str = "HS256" - ACCESS_TOKEN_EXPIRE_MINUTES: int = 60 * 24 # 24 hours + ACCESS_TOKEN_EXPIRE_MINUTES: int = 60 * 24 + + model_config = {"env_file": ".env"} settings = Settings() \ No newline at end of file diff --git a/Backend/app/services/auth_service.py b/Backend/app/services/auth_service.py index bbe1c11..f366e85 100644 --- a/Backend/app/services/auth_service.py +++ b/Backend/app/services/auth_service.py @@ -1,4 +1,4 @@ -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone from jose import jwt from passlib.context import CryptContext from ..config import settings @@ -6,13 +6,17 @@ pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") def hash_password(password: str): + """Hashes a plain text password using bcrypt.""" return pwd_context.hash(password) def verify_password(plain_password: str, hashed_password: str): + """Verifies a plain text password against a stored hash.""" return pwd_context.verify(plain_password, hashed_password) def create_access_token(data: dict): + """Generates a secure JWT access token with a timezone-aware expiry.""" to_encode = data.copy() - expire = datetime.utcnow() + timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES) + # Using timezone-aware UTC to fix CodeRabbit's 'Minor' issue + expire = datetime.now(timezone.utc) + timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES) to_encode.update({"exp": expire}) return jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM) \ No newline at end of file diff --git a/Backend/requirements.txt b/Backend/requirements.txt index ffa2f37..17115d2 100644 --- a/Backend/requirements.txt +++ b/Backend/requirements.txt @@ -31,7 +31,6 @@ postgrest==1.0.1 propcache==0.3.1 pydantic==2.11.1 pydantic_core==2.33.0 -PyJWT==2.10.1 pytest==8.3.5 pytest-mock==3.14.0 python-dateutil==2.9.0.post0 @@ -54,5 +53,6 @@ uvicorn==0.34.0 websockets==14.2 yarl==1.18.3 passlib[bcrypt]==1.7.4 -python-jose[cryptography]==3.3.0 -email-validator==2.1.0.post1 +bcrypt==4.0.1 +python-jose[cryptography]==3.4.0 +email-validator==2.1.0.post1pydantic-settings==2.7.1 From c95a1ad36d7290ee5e18c921e13c1631f69216a9 Mon Sep 17 00:00:00 2001 From: shivansh023023 Date: Wed, 18 Feb 2026 22:56:33 +0530 Subject: [PATCH 5/5] fix: update seed script with hashed passwords and tz-aware timestamps --- Backend/app/db/seed.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/Backend/app/db/seed.py b/Backend/app/db/seed.py index 77a015e..548f8bb 100644 --- a/Backend/app/db/seed.py +++ b/Backend/app/db/seed.py @@ -1,9 +1,13 @@ -from datetime import datetime +from datetime import datetime, timezone from app.db.db import AsyncSessionLocal from app.models.models import User - +from ..services.auth_service import hash_password # Import the hashing utility async def seed_db(): + """ + Seeds the database with initial creator and brand users. + Includes hashed passwords to satisfy database constraints. + """ users = [ { "id": "aabb1fd8-ba93-4e8c-976e-35e5c40b809c", @@ -13,7 +17,7 @@ async def seed_db(): "role": "creator", "bio": "Lifestyle and travel content creator", "profile_image": None, - "created_at": datetime.utcnow() + "created_at": datetime.now(timezone.utc) # Timezone-aware }, { "id": "6dbfcdd5-795f-49c1-8f7a-a5538b8c6f6f", @@ -23,27 +27,24 @@ async def seed_db(): "role": "brand", "bio": "Sustainable fashion brand looking for influencers", "profile_image": None, - "created_at": datetime.utcnow() + "created_at": datetime.now(timezone.utc) # Timezone-aware }, ] - # Insert or update the users async with AsyncSessionLocal() as session: for user_data in users: - # Check if user exists - existing_user = await session.execute( - User.__table__.select().where(User.email == user_data["email"]) - ) - existing_user = existing_user.scalar_one_or_none() + # Correct check for user existence + existing_user = await session.get(User, user_data["id"]) if existing_user: continue else: - # Create new user + # Create new user with hashed_password user = User( id=user_data["id"], username=user_data["username"], email=user_data["email"], + hashed_password=hash_password(user_data["password"]), # Fixes seed failure role=user_data["role"], profile_image=user_data["profile_image"], bio=user_data["bio"], @@ -52,6 +53,5 @@ async def seed_db(): session.add(user) print(f"Created user: {user_data['email']}") - # Commit the session await session.commit() - print("✅ Users seeded successfully.") + print("✅ Users seeded successfully.") \ No newline at end of file