@router.get("/google/callback") async def google_callback(request: Request, code: str = "", state: str = "", error: str = "", db: AsyncSession = Depends(get_db)): """Handle Google's response: verify state, exchange code for tokens, store encrypted.""" from app.models import GmailAccount, OAuthClientConfig, OAuthToken if error: return HTMLResponse(f"

Google authorization failed: {error}

" f"

Back

") if not code or not state: raise HTTPException(400, "Missing code or state") # 1) verify state (CSRF protection of OAuth handshake) user_id = None if _oauth_redis is not None: raw = _oauth_redis.get(f"oauthstate:{state}") if not raw: raise HTTPException(400, "Invalid or expired OAuth state") data = json.loads(raw) _oauth_redis.delete(f"oauthstate:{state}") user_id = data["user_id"] else: entry = _pending_oauth_states.pop(state, None) if not entry: raise HTTPException(400, "Invalid or expired OAuth state") user_id = entry["user_id"] # 2) resolve client config (latest imported by this user, else server default) cfg_row = (await db.execute( select(OAuthClientConfig) .where(OAuthClientConfig.user_id == user_id, OAuthClientConfig.is_active == True) # noqa .order_by(OAuthClientConfig.id.desc()))).scalars().first() client = gmail_service.resolve_client_config(cfg_row) # 3) exchange code for tokens (real HTTP call to Google) try: tokens = gmail_service.exchange_code_for_tokens(client, code) except gmail_service.GmailOauthError as e: return HTMLResponse(f"

Token exchange failed

{str(e)[:500]}
") # 4) identify the Gmail address that authorized try: userinfo = gmail_service.get_userinfo(tokens["access_token"]) gmail_email = userinfo.get("email", "").lower() except Exception as e: raise HTTPException(502, f"Could not fetch userinfo: {e}") if not gmail_email: raise HTTPException(502, "Google did not return an email address") # 5) persist account + encrypted tokens account = (await db.execute(select(GmailAccount) .where(GmailAccount.user_id == user_id, GmailAccount.email == gmail_email))).scalar_one_or_none() from datetime import timedelta as _td expires_at = datetime.now(timezone.utc) + _td(seconds=int(tokens.get("expires_in", 3600))) if account: account.status = "authorized" account.sending_method = account.sending_method or "api" account.token_expiry = expires_at if account.oauth_token: