Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 63 additions & 1 deletion backend/app/api/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from app.core.security import create_session_token, verify_session_token
from app.config import get_settings
from app.models import User
from app.schemas import GoogleAuthRequest, AuthResponse, UserResponse
from app.schemas import GoogleAuthRequest, DevLoginRequest, AuthResponse, UserResponse

router = APIRouter()
settings = get_settings()
Expand Down Expand Up @@ -135,6 +135,68 @@ async def google_auth(
)


@router.post("/dev-login", response_model=AuthResponse)
async def dev_login(
request: DevLoginRequest,
response: Response,
db: AsyncSession = Depends(get_db),
):
"""
Dummy/demo login — creates (or reuses) a local demo user and issues a
session token. Disabled automatically in production.

This is intentionally no-OAuth so the app can be tried without Google
credentials. Do not enable in production.
"""
if settings.is_production:
raise HTTPException(status_code=404, detail="Not found")

email = (request.email or "demo@cortexlab.local").strip().lower()
name = (request.name or "Demo Researcher").strip() or "Demo Researcher"
# Stable synthetic google_id so the same demo email always maps to the same user
google_id = f"demo:{email}"

result = await db.execute(select(User).where(User.google_id == google_id))
user = result.scalar_one_or_none()

if not user:
user = User(
google_id=google_id,
email=email,
name=name,
avatar_url=None,
)
db.add(user)
await db.commit()
Comment on lines +154 to +170

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Handle demo email collisions before inserting.

The route looks up only by google_id, then inserts email. If an unauthenticated caller supplies an email already used by another account, the unique User.email constraint will fail and return a 500. Reject non-demo-domain emails or explicitly detect collisions before db.add().

🛡️ Proposed collision guard
     email = (request.email or "demo@cortexlab.local").strip().lower()
     name = (request.name or "Demo Researcher").strip() or "Demo Researcher"
+
+    if not email.endswith("@cortexlab.local"):
+        raise HTTPException(status_code=400, detail="Demo login requires a demo email address")
+
     # Stable synthetic google_id so the same demo email always maps to the same user
     google_id = f"demo:{email}"
 
     result = await db.execute(select(User).where(User.google_id == google_id))
     user = result.scalar_one_or_none()
 
     if not user:
+        existing_email_result = await db.execute(select(User).where(User.email == email))
+        existing_email_user = existing_email_result.scalar_one_or_none()
+        if existing_email_user:
+            raise HTTPException(status_code=409, detail="Demo email is already in use")
+
         user = User(
             google_id=google_id,
             email=email,
             name=name,
             avatar_url=None,
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
email = (request.email or "demo@cortexlab.local").strip().lower()
name = (request.name or "Demo Researcher").strip() or "Demo Researcher"
# Stable synthetic google_id so the same demo email always maps to the same user
google_id = f"demo:{email}"
result = await db.execute(select(User).where(User.google_id == google_id))
user = result.scalar_one_or_none()
if not user:
user = User(
google_id=google_id,
email=email,
name=name,
avatar_url=None,
)
db.add(user)
await db.commit()
email = (request.email or "demo@cortexlab.local").strip().lower()
name = (request.name or "Demo Researcher").strip() or "Demo Researcher"
if not email.endswith("@cortexlab.local"):
raise HTTPException(status_code=400, detail="Demo login requires a demo email address")
# Stable synthetic google_id so the same demo email always maps to the same user
google_id = f"demo:{email}"
result = await db.execute(select(User).where(User.google_id == google_id))
user = result.scalar_one_or_none()
if not user:
existing_email_result = await db.execute(select(User).where(User.email == email))
existing_email_user = existing_email_result.scalar_one_or_none()
if existing_email_user:
raise HTTPException(status_code=409, detail="Demo email is already in use")
user = User(
google_id=google_id,
email=email,
name=name,
avatar_url=None,
)
db.add(user)
await db.commit()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/app/api/auth.py` around lines 154 - 170, The current demo signup
creates a User by google_id but can violate the unique User.email constraint if
an unauthenticated caller supplies an email already owned by another account;
before db.add() perform a lookup by email (select(User).where(User.email ==
email)) and if a record exists with a different google_id return a
400/validation error (or reject non-demo-domain emails by checking the domain
portion of request.email and returning 400 for non-demo domains); only proceed
to create and db.add() when no conflicting email owner exists. Ensure you
reference and compare the existing_user.email and existing_user.google_id
against the computed google_id to detect collisions.

await db.refresh(user)
else:
# Keep display name fresh if the caller provided a new one
if request.name and user.name != name:
user.name = name
await db.commit()

session_token = create_session_token(user.id)

response.set_cookie(
key="session_token",
value=session_token,
httponly=True,
secure=settings.is_production,
samesite="lax",
max_age=86400 * 7,
)

return AuthResponse(
user=UserResponse(
id=user.id,
email=user.email,
name=user.name,
avatar_url=user.avatar_url,
),
session_token=session_token,
)


@router.post("/logout")
async def logout(response: Response):
"""
Expand Down
2 changes: 2 additions & 0 deletions backend/app/schemas/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from app.schemas.auth import (
GoogleAuthRequest,
DevLoginRequest,
UserResponse,
AuthResponse,
SessionValidation,
Expand Down Expand Up @@ -38,6 +39,7 @@
__all__ = [
# Auth
"GoogleAuthRequest",
"DevLoginRequest",
"UserResponse",
"AuthResponse",
"SessionValidation",
Expand Down
6 changes: 6 additions & 0 deletions backend/app/schemas/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ class GoogleAuthRequest(BaseModel):
id_token: str


class DevLoginRequest(BaseModel):
"""Request body for dummy/demo login (dev & demos only)."""
name: Optional[str] = None
email: Optional[str] = None
Comment on lines +16 to +19

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Inspect declared Pydantic version so the validation API can be matched to the installed major version.
fd -i '^(pyproject\.toml|requirements.*\.txt|poetry\.lock|Pipfile)$' \
  -x sh -c 'echo "--- $1"; rg -n "pydantic|email-validator" "$1" || true' sh {}

Repository: Aniket25042003/CortexLab

Length of output: 144


🏁 Script executed:

rg -n "email-validator" --type txt --type toml

Repository: Aniket25042003/CortexLab

Length of output: 50


🏁 Script executed:

fd -i 'user.py' -path '*/models/*' -x cat -n {}

Repository: Aniket25042003/CortexLab

Length of output: 236


🏁 Script executed:

rg -n "demo:" --type py

Repository: Aniket25042003/CortexLab

Length of output: 127


🏁 Script executed:

rg -n "DevLoginRequest" --type py -A 5

Repository: Aniket25042003/CortexLab

Length of output: 1757


🏁 Script executed:

rg -n "email-validator" backend/

Repository: Aniket25042003/CortexLab

Length of output: 50


🏁 Script executed:

fd -i 'user.py' -path '*/models/*' -x head -100 {}

Repository: Aniket25042003/CortexLab

Length of output: 236


🏁 Script executed:

sed -n '140,170p' backend/app/api/auth.py

Repository: Aniket25042003/CortexLab

Length of output: 1117


🏁 Script executed:

find backend -name "*.py" -path "*/models/*" | head -5

Repository: Aniket25042003/CortexLab

Length of output: 231


🏁 Script executed:

rg -n "class User" backend/ --type py -A 10

Repository: Aniket25042003/CortexLab

Length of output: 1230


🏁 Script executed:

cat backend/requirements.txt

Repository: Aniket25042003/CortexLab

Length of output: 753


🏁 Script executed:

cat -n backend/app/models/user.py

Repository: Aniket25042003/CortexLab

Length of output: 1698


Validate demo login input at the schema boundary.

email can currently be any length, but User.email and User.google_id are both String(255). Since the endpoint prefixes google_id with demo:, an oversized email would overflow the column. Add max_length validation to prevent DB errors.

🛡️ Proposed validation
-from pydantic import BaseModel
+from pydantic import BaseModel, Field
 from typing import Optional

 class DevLoginRequest(BaseModel):
     """Request body for dummy/demo login (dev & demos only)."""
-    name: Optional[str] = None
-    email: Optional[str] = None
+    name: Optional[str] = Field(default=None, max_length=255)
+    email: Optional[str] = Field(default=None, max_length=250)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/app/schemas/auth.py` around lines 16 - 19, The DevLoginRequest schema
allows arbitrarily long emails which can overflow the DB String(255) columns
(and the endpoint prefixes google_id with "demo:"), so add length validation at
the schema boundary: update the DevLoginRequest.email declaration to enforce
max_length=255 (e.g. email: Optional[str] = Field(None, max_length=255) or use
constr(max_length=255)), import Field/constr from pydantic as needed, and keep
the Optional typing; this prevents oversized values before hitting
User.email/User.google_id.



class UserResponse(BaseModel):
"""User data response."""
id: str
Expand Down
Loading