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: