124-async def google_start(request: Request, user=Depends(get_current_user)): 125- return await google_start_impl(user) 126- 127- 128-@router.get("/google/callback") 129:async def google_callback(request: Request, code: str = "", state: str = "", error: str = "", 130- db: AsyncSession = Depends(get_db)): 131- """Handle Google's response: verify state, exchange code for tokens, store encrypted.""" 132- from app.models import GmailAccount, OAuthClientConfig, OAuthToken 133- 134- if error: 135- return HTMLResponse(f"

Google authorization failed: {error}

" 136- f"

Back

") 137- if not code or not state: 138- raise HTTPException(400, "Missing code or state") 139- 140- # 1) verify state (CSRF protection of OAuth handshake) 141- user_id = None 142- if _oauth_redis is not None: 143- raw = _oauth_redis.get(f"oauthstate:{state}") 144- if not raw: 145- raise HTTPException(400, "Invalid or expired OAuth state") 146- data = json.loads(raw) 147- _oauth_redis.delete(f"oauthstate:{state}") 148- user_id = data["user_id"] 149- else: 150- entry = _pending_oauth_states.pop(state, None) 151- if not entry: 152- raise HTTPException(400, "Invalid or expired OAuth state") 153- user_id = entry["user_id"] 154- 155- # 2) resolve client config (latest imported by this user, else server default) 156- cfg_row = (await db.execute( 157- select(OAuthClientConfig) 158- .where(OAuthClientConfig.user_id == user_id, OAuthClientConfig.is_active == True) # noqa 159- .order_by(OAuthClientConfig.id.desc()))).scalars().first() 160- client = gmail_service.resolve_client_config(cfg_row) 161- 162- # 3) exchange code for tokens (real HTTP call to Google) 163- try: 164- tokens = gmail_service.exchange_code_for_tokens(client, code) 165- except gmail_service.GmailOauthError as e: 166- return HTMLResponse(f"

Token exchange failed

{str(e)[:500]}
") 167- 168- # 4) identify the Gmail address that authorized 169- try: