From de878989eff185b8ca082055f8bd26eb902284ab Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Thu, 26 Feb 2026 02:46:44 +0000 Subject: [PATCH] Implement Google OAuth integration for web-demo and backend. - Frontend: Update web-demo/js/app.js to use Google Identity Services (GSI) for authorization code flow. - Frontend: Update web-demo/js/api.js to include googleLogin method and handle forceReal option. - Frontend: Add GSI script to web-demo/index.html. - Backend: Add GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET to config.py. - Backend: Update UserCreate schema to allow 'web' platform and add GoogleLoginRequest schema. - Backend: Implement POST /api/auth/google endpoint in users.py to exchange code for tokens and create/login user. Co-authored-by: singhaditya21 <53948039+singhaditya21@users.noreply.github.com> --- backend/api/app/config.py | 4 ++ backend/api/app/routers/users.py | 77 +++++++++++++++++++++++++++++++- backend/api/app/schemas/user.py | 8 +++- web-demo/index.html | 1 + web-demo/js/api.js | 13 +++++- web-demo/js/app.js | 56 +++++++++++------------ 6 files changed, 126 insertions(+), 33 deletions(-) diff --git a/backend/api/app/config.py b/backend/api/app/config.py index bab5e44..48cc493 100644 --- a/backend/api/app/config.py +++ b/backend/api/app/config.py @@ -44,6 +44,10 @@ class Settings(BaseSettings): APPLE_SHARED_SECRET: str = "" # App Store Connect shared secret GOOGLE_SERVICE_ACCOUNT_JSON: str = "" # JSON string of service account key + # OAuth + GOOGLE_CLIENT_ID: str = "" + GOOGLE_CLIENT_SECRET: str = "" + # Feature flags ENABLE_AI_INSIGHTS: bool = True diff --git a/backend/api/app/routers/users.py b/backend/api/app/routers/users.py index 7e3e4eb..eac8843 100644 --- a/backend/api/app/routers/users.py +++ b/backend/api/app/routers/users.py @@ -6,9 +6,11 @@ from sqlalchemy import select from ..database import get_db from ..models import User -from ..schemas.user import UserCreate, UserLogin, UserUpdate, UserResponse, TokenResponse, ForgotPasswordRequest +from ..schemas.user import UserCreate, UserLogin, UserUpdate, UserResponse, TokenResponse, ForgotPasswordRequest, GoogleLoginRequest from ..services.auth import hash_password, verify_password, create_access_token, get_current_user +from ..config import get_settings import uuid +import httpx from datetime import datetime, timedelta router = APIRouter(prefix="/api/auth", tags=["auth"]) @@ -51,6 +53,79 @@ async def register(user_data: UserCreate, db: AsyncSession = Depends(get_db)): ) +@router.post("/google", response_model=TokenResponse) +async def google_login(login_data: GoogleLoginRequest, db: AsyncSession = Depends(get_db)): + """Login with Google.""" + settings = get_settings() + + # Exchange code for tokens + async with httpx.AsyncClient() as client: + token_response = await client.post( + "https://oauth2.googleapis.com/token", + data={ + "code": login_data.code, + "client_id": settings.GOOGLE_CLIENT_ID, + "client_secret": settings.GOOGLE_CLIENT_SECRET, + "redirect_uri": login_data.redirect_uri or "postmessage", + "grant_type": "authorization_code", + }, + ) + + if token_response.status_code != 200: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Invalid Google credentials", + ) + + token_data = token_response.json() + access_token = token_data.get("access_token") + + # Get user info + user_info_response = await client.get( + "https://www.googleapis.com/oauth2/v2/userinfo", + headers={"Authorization": f"Bearer {access_token}"}, + ) + + if user_info_response.status_code != 200: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Failed to get user info from Google", + ) + + user_info = user_info_response.json() + email = user_info.get("email") + + if not email: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Google account must have an email", + ) + + # Find or create user + result = await db.execute(select(User).where(User.email == email)) + user = result.scalar_one_or_none() + + if not user: + user = User( + email=email, + full_name=user_info.get("name"), + platform="web", + is_active=True, + is_verified=True, + ) + db.add(user) + await db.commit() + await db.refresh(user) + + # Create access token + access_token = create_access_token(data={"sub": str(user.id)}) + + return TokenResponse( + access_token=access_token, + user=UserResponse.model_validate(user) + ) + + @router.post("/forgot-password", status_code=status.HTTP_200_OK) async def forgot_password( request: ForgotPasswordRequest, diff --git a/backend/api/app/schemas/user.py b/backend/api/app/schemas/user.py index dcd8cc7..b3a0cb1 100644 --- a/backend/api/app/schemas/user.py +++ b/backend/api/app/schemas/user.py @@ -22,7 +22,7 @@ class UserCreate(BaseModel): email: EmailStr password: str = Field(..., min_length=8, max_length=100) full_name: Optional[str] = Field(None, max_length=255) - platform: str = Field(..., pattern="^(ios|android)$") + platform: str = Field(..., pattern="^(ios|android|web)$") device_token: Optional[str] = None @field_validator("password") @@ -44,6 +44,12 @@ class UserLogin(BaseModel): password: str +class GoogleLoginRequest(BaseModel): + """Schema for Google login.""" + code: str + redirect_uri: Optional[str] = None + + class ForgotPasswordRequest(BaseModel): """Schema for forgot password request.""" email: EmailStr diff --git a/web-demo/index.html b/web-demo/index.html index 960b612..a721c3e 100644 --- a/web-demo/index.html +++ b/web-demo/index.html @@ -307,6 +307,7 @@