@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: account.oauth_token.access_token = tokens["access_token"] account.oauth_token.refresh_token = tokens.get("refresh_token") or account.oauth_token.refresh_token account.oauth_token.expires_at = expires_at account.oauth_token.granted_scopes = tokens.get("scope", "") else: account.oauth_token = OAuthToken( access_token=tokens["access_token"], refresh_token=tokens.get("refresh_token", ""), scope=tokens.get("scope", ""), expires_at=expires_at, granted_scopes=tokens.get("scope", ""), ) else: account = GmailAccount( user_id=user_id, email=gmail_email, display_name=userinfo.get("name", ""), status="authorized", sending_method="api", google_user_id=userinfo.get("sub", ""), token_expiry=expires_at, oauth_config_id=cfg_row.id if cfg_row else None, ) account.oauth_token = OAuthToken( access_token=tokens["access_token"], refresh_token=tokens.get("refresh_token", ""), scope=tokens.get("scope", ""), expires_at=expires_at, granted_scopes=tokens.get("scope", ""), ) db.add(account) await db.commit() await log_audit(db, user_id, "gmail.authorized", "gmail_account", account.id, {"email": gmail_email}, request.client.host) redirect = f"{settings.FRONTEND_BASE_URL}/gmail-accounts.html?connected={gmail_email}" return RedirectResponse(redirect, status_code=303)