-
-
Notifications
You must be signed in to change notification settings - Fork 148
feat: implement JWT authentication and user registration system #302
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
270a009
74898bf
1a7997d
45576a4
c95a1ad
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| import os | ||
| from pydantic_settings import BaseSettings | ||
|
|
||
| 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 | ||
|
|
||
| model_config = {"env_file": ".env"} | ||
|
|
||
| settings = Settings() |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hardcoded weak seed credentials are flagged by secret scanners
🔒 Suggested improvement+import os
...
- "password": "password123",
+ "password": os.getenv("SEED_USER_PASSWORD", "changeme_dev_only"),🤖 Prompt for AI Agents |
||
| 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.") | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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) | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| 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 | ||
| ) | ||
|
Comment on lines
73
to
75
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Additionally, ♻️ Proposed fix (representative — apply to all 6 columns)-from datetime import datetime
+from datetime import datetime, timezone
...
- DateTime(timezone=True), default=datetime.utcnow
+ DateTime(timezone=True), default=lambda: datetime.now(timezone.utc)🤖 Prompt for AI Agents |
||
|
|
||
| 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" | ||
| ) | ||
| ) | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -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.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 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+13
to
+16
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The ♻️ Proposed fix class UserCreate(BaseModel):
username: str
email: EmailStr
password: str
+ role: str = "creator" # 'creator' or 'brand'And in the signup handler: new_user = User(
username=user_data.username,
email=user_data.email,
hashed_password=hash_password(user_data.password),
+ role=user_data.role,
)📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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"} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+22
to
+37
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Signup silently raises a 500 on duplicate username — check uniqueness before inserting. The endpoint checks for duplicate 🐛 Proposed fix `@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")
+ result = await db.execute(select(User).where(User.username == user_data.username))
+ if result.scalars().first():
+ raise HTTPException(status_code=400, detail="Username already taken")
+
new_user = User(📝 Committable suggestion
Suggested change
🧰 Tools🪛 Ruff (0.15.1)[warning] 23-23: Do not perform function call (B008) 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @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 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| from datetime import datetime, timedelta, timezone | ||
| 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): | ||
| """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() | ||
| # 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) |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -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 | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion | 🟠 Major 🧩 Analysis chain🌐 Web query:
💡 Result: In SQLAlchemy 1.4, In SQLAlchemy 2.0, those “future” APIs are effectively the default way of working, so you should stop importing from Also in 2.0, the Citations:
🏁 Script executed: # Check if the files contain the imports mentioned
find Backend -name "get_current_user.py" -o -name "auth.py" | head -20Repository: AOSSIE-Org/InPactAI Length of output: 130 🏁 Script executed: # Find and check the requirements/setup files for SQLAlchemy version
fd -e txt -e cfg -e toml -e ini -e lock | xargs grep -l -i "sqlalchemy" 2>/dev/null | head -10Repository: AOSSIE-Org/InPactAI Length of output: 87 🏁 Script executed: # Check the imports in both files
cat -n Backend/app/services/get_current_user.py | head -10Repository: AOSSIE-Org/InPactAI Length of output: 513 🏁 Script executed: cat -n Backend/app/routes/auth.py | head -10Repository: AOSSIE-Org/InPactAI Length of output: 586 🏁 Script executed: # Check SQLAlchemy version in requirements
cat Backend/requirements.txt | grep -i sqlalchemyRepository: AOSSIE-Org/InPactAI Length of output: 81 Replace
♻️ Fix-from sqlalchemy.future import select
+from sqlalchemy import select📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||
| from ..db.db import get_db | ||||||
| from ..models.models 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") | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Either change 🤖 Prompt for AI Agents |
||||||
|
|
||||||
| 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 | ||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
datetime.now(timezone.utc)passed to aTIMESTAMP WITHOUT TIME ZONEcolumn will crash the seed under asyncpgUser.created_atisColumn(TIMESTAMP, default=datetime.utcnow)— bareTIMESTAMPin SQLAlchemy maps toTIMESTAMP WITHOUT TIME ZONEin PostgreSQL. asyncpg raises aDataErrorbecause timezone-aware Python datetime objects are being passed toTIMESTAMP WITHOUT TIME ZONEcolumns. The specific error is:asyncpg.exceptions.DataError: invalid input for query argument: datetime.datetime(..., tzinfo=datetime.timezone.utc) (can't subtract offset-naive and offset-aware datetimes). This is triggered when code changes from database-generated timestamps to explicit Pythondatetime.now(timezone.utc)datetimes on aTIMESTAMP WITHOUT TIME ZONEcolumn.The seed will fail at
session.commit()on every fresh run.Two consistent fixes — pick one and apply it everywhere
created_atis written:Option A — keep columns naive (minimal change in seed only):
🐛 Proposed fix (Option A)
Option B — make the column timezone-aware throughout (preferred long-term): Apply this to
Backend/app/models/models.pyso the column type and seed value are consistent:With Option B the seed values (
datetime.now(timezone.utc)) are already correct and no change toseed.pyis needed. This also eliminates the use of the deprecateddatetime.utcnowin the model.Also applies to: 30-30, 51-51
🤖 Prompt for AI Agents
🧩 Analysis chain
🏁 Script executed:
Repository: AOSSIE-Org/InPactAI
Length of output: 93
🏁 Script executed:
Repository: AOSSIE-Org/InPactAI
Length of output: 87
🌐 Web query:
asyncpg timezone-aware datetime TIMESTAMP WITHOUT TIME ZONE error💡 Result:
In asyncpg, PostgreSQL type mappings are strict:
TIMESTAMP WITHOUT TIME ZONE(timestamp) ↔ offset-naïvedatetime(notzinfo)TIMESTAMP WITH TIME ZONE(timestamptz) ↔ offset-awaredatetime(hastzinfo)[1]
So you’ll get an error when you pass a timezone-aware Python
datetime(e.g.tzinfo=UTC) into a SQL parameter/column typedTIMESTAMP WITHOUT TIME ZONE, because asyncpg refuses to silently drop timezone info. This is commonly described as asyncpg raising if you try to store a timezone-awaredatetimeinto a “no timezone” column. [2]Fix options (pick one)
Postgres conversions between timestamp types typically assume the session
timezone, andAT TIME ZONElets you specify it explicitly. [3]timestamp, pass a naïve datetime (commonly “UTC but naïve”)timestampcolumn)(
AT TIME ZONEis the standard Postgres way to control the interpretation/conversion.) [3]Rule of thumb: if you want an “instant in time”, use
timestamptz; if you want a “local calendar time” (no timezone meaning), usetimestamp. [3]Sources: asyncpg type mapping table [1], discussion of asyncpg refusing tz-aware datetimes for “no timezone” columns [2], PostgreSQL timestamp/timezone semantics and
AT TIME ZONEbehavior [3].[1] (magicstack.github.io)
[2] (safir.lsst.io)
[3] (postgresql.org)
Citations:
🏁 Script executed:
Repository: AOSSIE-Org/InPactAI
Length of output: 1925
🏁 Script executed:
Repository: AOSSIE-Org/InPactAI
Length of output: 2613
Timezone-aware datetime passed to
TIMESTAMP WITHOUT TIME ZONEcolumn will cause asyncpg to failUser.created_atis defined asColumn(TIMESTAMP, default=datetime.utcnow)— SQLAlchemy maps bareTIMESTAMPtoTIMESTAMP WITHOUT TIME ZONEin PostgreSQL. The seed is passingdatetime.now(timezone.utc)(timezone-aware) to this column at lines 20, 30, and 51, which will cause asyncpg to raiseasyncpg.exceptions.DataErroratsession.commit().Recommended fix — strip timezone from seed values (matches your preference for naive datetimes):
Proposed fix
This keeps the column naive (consistent with the model default
datetime.utcnowand your broader preference for naive datetimes in the codebase).Alternatively, you could make the column timezone-aware throughout (
TIMESTAMP(timezone=True)in the model), but this would require migrating the column type and updating other naive timestamp columns in the schema.Also applies to: 30-30, 51-51
🤖 Prompt for AI Agents