130 lines
4.6 KiB
Python
130 lines
4.6 KiB
Python
"""
|
|
Google OAuth 2.0 flow:
|
|
1. GET /api/auth/google/login → redirect to Google consent screen
|
|
2. GET /api/auth/google/callback → exchange code for tokens → get user info
|
|
→ upsert user in MongoDB → issue JWT
|
|
→ redirect to frontend with token
|
|
3. GET /api/auth/me → return current user (protected)
|
|
4. POST /api/auth/logout → client-side only (stateless JWT), just 200
|
|
"""
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, Request
|
|
from fastapi.responses import RedirectResponse, JSONResponse
|
|
import httpx
|
|
from datetime import datetime, timezone
|
|
from app.config import get_settings
|
|
from app.database import get_db
|
|
from app.dependencies import create_access_token, get_current_user
|
|
from app.models.user import UserOut
|
|
|
|
router = APIRouter(prefix="/api/auth", tags=["auth"])
|
|
settings = get_settings()
|
|
|
|
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/v3/userinfo"
|
|
|
|
SCOPES = "openid email profile"
|
|
|
|
|
|
# ── Step 1: Redirect to Google ─────────────────────────────────
|
|
@router.get("/google/login")
|
|
async def google_login():
|
|
redirect_uri = f"{settings.backend_url}/api/auth/google/callback"
|
|
params = (
|
|
f"?client_id={settings.google_client_id}"
|
|
f"&redirect_uri={redirect_uri}"
|
|
f"&response_type=code"
|
|
f"&scope={SCOPES}"
|
|
f"&access_type=offline"
|
|
f"&prompt=select_account"
|
|
)
|
|
return RedirectResponse(url=GOOGLE_AUTH_URL + params)
|
|
|
|
|
|
# ── Step 2: Callback from Google ───────────────────────────────
|
|
@router.get("/google/callback")
|
|
async def google_callback(code: str, db=Depends(get_db)):
|
|
redirect_uri = f"{settings.backend_url}/api/auth/google/callback"
|
|
|
|
# Exchange authorization code for tokens
|
|
async with httpx.AsyncClient() as client:
|
|
token_response = await client.post(
|
|
GOOGLE_TOKEN_URL,
|
|
data={
|
|
"code": code,
|
|
"client_id": settings.google_client_id,
|
|
"client_secret": settings.google_client_secret,
|
|
"redirect_uri": redirect_uri,
|
|
"grant_type": "authorization_code",
|
|
},
|
|
)
|
|
|
|
if token_response.status_code != 200:
|
|
error_url = f"{settings.frontend_url}/login?error=google_token_failed"
|
|
return RedirectResponse(url=error_url)
|
|
|
|
tokens = token_response.json()
|
|
access_token = tokens.get("access_token")
|
|
|
|
# Fetch user profile from Google
|
|
async with httpx.AsyncClient() as client:
|
|
userinfo_response = await client.get(
|
|
GOOGLE_USERINFO_URL,
|
|
headers={"Authorization": f"Bearer {access_token}"},
|
|
)
|
|
|
|
if userinfo_response.status_code != 200:
|
|
error_url = f"{settings.frontend_url}/login?error=google_userinfo_failed"
|
|
return RedirectResponse(url=error_url)
|
|
|
|
google_user = userinfo_response.json()
|
|
google_id = google_user.get("sub")
|
|
email = google_user.get("email", "")
|
|
name = google_user.get("name", "")
|
|
picture = google_user.get("picture", "")
|
|
|
|
# Upsert user in MongoDB
|
|
now = datetime.now(timezone.utc)
|
|
await db["users"].update_one(
|
|
{"google_id": google_id},
|
|
{
|
|
"$set": {
|
|
"email": email,
|
|
"name": name,
|
|
"picture": picture,
|
|
"last_login": now,
|
|
},
|
|
"$setOnInsert": {
|
|
"google_id": google_id,
|
|
"created_at": now,
|
|
},
|
|
},
|
|
upsert=True,
|
|
)
|
|
|
|
# Issue JWT
|
|
jwt_token = create_access_token(
|
|
data={"sub": google_id, "email": email, "name": name, "picture": picture}
|
|
)
|
|
|
|
# Redirect to frontend with token
|
|
frontend_redirect = f"{settings.frontend_url}/auth/callback?token={jwt_token}"
|
|
return RedirectResponse(url=frontend_redirect)
|
|
|
|
|
|
# ── Get current user (protected) ──────────────────────────────
|
|
@router.get("/me", response_model=UserOut)
|
|
async def get_me(current_user: dict = Depends(get_current_user)):
|
|
return UserOut(
|
|
google_id=current_user["google_id"],
|
|
email=current_user["email"],
|
|
name=current_user["name"],
|
|
picture=current_user.get("picture"),
|
|
)
|
|
|
|
|
|
# ── Logout (stateless — client drops the JWT) ─────────────────
|
|
@router.post("/logout")
|
|
async def logout():
|
|
return JSONResponse({"message": "Logged out successfully"})
|