""" 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"})