TradingAgents/backend/app/api/auth.py

300 lines
8.8 KiB
Python

"""
Google OAuth authentication routes
"""
import os
import logging
import httpx
from datetime import datetime
from typing import Optional
from fastapi import APIRouter, HTTPException, Depends, Query
from fastapi.responses import RedirectResponse
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from backend.app.db import get_db, User, UserSettings
from backend.app.services.auth_utils import create_access_token
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/api/auth", tags=["Authentication"])
# Google OAuth Configuration - read at request time for dynamic updates
def get_google_client_id():
return os.getenv("GOOGLE_CLIENT_ID", "")
def get_google_client_secret():
return os.getenv("GOOGLE_CLIENT_SECRET", "")
def get_frontend_url():
url = os.getenv("FRONTEND_URL", "http://localhost:3000")
logger.info(f"FRONTEND_URL resolved to: {url}")
return url
# Google OAuth URLs
GOOGLE_AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth"
GOOGLE_TOKEN_URL = "https://oauth2.googleapis.com/token"
GOOGLE_USERINFO_URL = "https://www.googleapis.com/oauth2/v2/userinfo"
@router.get("/google/login")
async def google_login():
"""
Redirect to Google OAuth login page
"""
client_id = get_google_client_id()
frontend_url = get_frontend_url()
if not client_id:
raise HTTPException(status_code=500, detail="Google OAuth not configured")
# Build the authorization URL
redirect_uri = f"{frontend_url}/api/auth/callback/google"
# For backend-handled callback (alternative):
# redirect_uri = f"{os.getenv('BACKEND_URL', 'http://localhost:8000')}/api/auth/google/callback"
params = {
"client_id": client_id,
"redirect_uri": redirect_uri,
"response_type": "code",
"scope": "openid email profile",
"access_type": "offline",
"prompt": "consent",
}
query_string = "&".join(f"{k}={v}" for k, v in params.items())
auth_url = f"{GOOGLE_AUTH_URL}?{query_string}"
return RedirectResponse(url=auth_url)
@router.get("/google/callback")
async def google_callback(
code: str = Query(...),
db: AsyncSession = Depends(get_db)
):
"""
Handle Google OAuth callback (backend-handled flow)
Exchange authorization code for tokens and create/update user
"""
client_id = get_google_client_id()
client_secret = get_google_client_secret()
frontend_url = get_frontend_url()
if not client_id or not client_secret:
raise HTTPException(status_code=500, detail="Google OAuth not configured")
# Determine redirect URI (must match what was used in the login request)
redirect_uri = f"{os.getenv('BACKEND_URL', 'http://localhost:8000')}/api/auth/google/callback"
# Exchange code for tokens
async with httpx.AsyncClient() as client:
token_response = await client.post(
GOOGLE_TOKEN_URL,
data={
"code": code,
"client_id": client_id,
"client_secret": client_secret,
"redirect_uri": redirect_uri,
"grant_type": "authorization_code",
}
)
if token_response.status_code != 200:
raise HTTPException(
status_code=400,
detail=f"Failed to exchange code: {token_response.text}"
)
tokens = token_response.json()
access_token = tokens.get("access_token")
# Get user info from Google
userinfo_response = await client.get(
GOOGLE_USERINFO_URL,
headers={"Authorization": f"Bearer {access_token}"}
)
if userinfo_response.status_code != 200:
raise HTTPException(status_code=400, detail="Failed to get user info")
userinfo = userinfo_response.json()
# Find or create user
google_id = userinfo["id"]
email = userinfo["email"]
name = userinfo.get("name")
avatar_url = userinfo.get("picture")
# Check if user exists
result = await db.execute(
select(User).where(User.google_id == google_id)
)
user = result.scalar_one_or_none()
if user:
# Update existing user
user.last_login_at = datetime.utcnow()
user.name = name
user.avatar_url = avatar_url
else:
# Create new user
user = User(
google_id=google_id,
email=email,
name=name,
avatar_url=avatar_url,
last_login_at=datetime.utcnow()
)
db.add(user)
await db.commit()
await db.refresh(user)
# Create JWT token
jwt_token = create_access_token({
"id": user.id,
"email": user.email,
"name": user.name,
"avatar_url": user.avatar_url,
})
# Redirect to frontend with token
redirect_url = f"{frontend_url}/auth/callback?token={jwt_token}"
return RedirectResponse(url=redirect_url)
from pydantic import BaseModel
class TokenExchangeRequest(BaseModel):
code: str
redirect_uri: str
@router.post("/google/token")
async def exchange_google_token(
request: TokenExchangeRequest,
db: AsyncSession = Depends(get_db)
):
"""
Exchange Google authorization code for JWT token (frontend-handled flow)
This is called by the frontend after receiving the code from Google
"""
client_id = get_google_client_id()
client_secret = get_google_client_secret()
if not client_id or not client_secret:
raise HTTPException(status_code=500, detail="Google OAuth not configured")
# Exchange code for tokens
async with httpx.AsyncClient() as client:
token_response = await client.post(
GOOGLE_TOKEN_URL,
data={
"code": request.code,
"client_id": client_id,
"client_secret": client_secret,
"redirect_uri": request.redirect_uri,
"grant_type": "authorization_code",
}
)
if token_response.status_code != 200:
error_detail = token_response.json() if token_response.headers.get("content-type", "").startswith("application/json") else token_response.text
raise HTTPException(
status_code=400,
detail=f"Failed to exchange code: {error_detail}"
)
tokens = token_response.json()
access_token = tokens.get("access_token")
# Get user info from Google
userinfo_response = await client.get(
GOOGLE_USERINFO_URL,
headers={"Authorization": f"Bearer {access_token}"}
)
if userinfo_response.status_code != 200:
raise HTTPException(status_code=400, detail="Failed to get user info")
userinfo = userinfo_response.json()
# Find or create user
google_id = userinfo["id"]
email = userinfo["email"]
name = userinfo.get("name")
avatar_url = userinfo.get("picture")
# Check if user exists
result = await db.execute(
select(User).where(User.google_id == google_id)
)
user = result.scalar_one_or_none()
if user:
# Update existing user
user.last_login_at = datetime.utcnow()
user.name = name
user.avatar_url = avatar_url
else:
# Create new user
user = User(
google_id=google_id,
email=email,
name=name,
avatar_url=avatar_url,
last_login_at=datetime.utcnow()
)
db.add(user)
await db.commit()
await db.refresh(user)
# Create JWT token
jwt_token = create_access_token({
"id": user.id,
"email": user.email,
"name": user.name,
"avatar_url": user.avatar_url,
})
return {
"access_token": jwt_token,
"token_type": "bearer",
"user": {
"id": str(user.id),
"email": user.email,
"name": user.name,
"avatar_url": user.avatar_url,
}
}
@router.get("/me")
async def get_current_user(
authorization: Optional[str] = None,
db: AsyncSession = Depends(get_db)
):
"""
Get current user info from JWT token
Returns None if not authenticated (optional auth)
"""
from backend.app.services.auth_utils import verify_access_token
if not authorization or not authorization.startswith("Bearer "):
return {"user": None}
token = authorization.replace("Bearer ", "")
payload = verify_access_token(token)
if not payload:
return {"user": None}
return {
"user": {
"id": payload.get("sub"),
"email": payload.get("email"),
"name": payload.get("name"),
"avatar_url": payload.get("avatar_url"),
}
}