101:async def google_start_impl(user, client: dict = None): 102- """Shared implementation: generate state, persist, return Google's authorization URL. 103- `client` may be pre-resolved (e.g. the user's imported OAuthClientConfig row); 104- when omitted the server-level configuration is used (honest 400 if absent).""" 105- if client is None: 106- try: 107- client = gmail_service.resolve_client_config() 108- except gmail_service.GmailOauthError as e: 109- # Be honest: no client config -> no authorization URL (never fabricate one) 110- raise HTTPException(400, f"Google OAuth is not configured: {e}") 111- state = gmail_service.generate_state() 112- if _oauth_redis is not None: 113- _oauth_redis.setex(f"oauthstate:{state}", 600, json.dumps({"user_id": user.id})) 114- else: 115- _pending_oauth_states[state] = {"user_id": user.id} 116- auth_url = gmail_service.build_authorization_url(client, state) 117- return {"authorization_url": auth_url} 118- 119- 120-_pending_oauth_states: dict = {} 121- 122- 123-@router.get("/google/start") 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 ===get_current_user_csrf=== app/api/deps.py:74:async def get_current_user_csrf(user: "User" = Depends(get_current_user), app/api/deps.py-75- _: None = Depends(require_csrf)) -> "User": app/api/deps.py-76- """Combined dependency: authenticate first (401 for anonymous), then CSRF (403). app/api/deps.py-77- app/api/deps.py-78- Used by state-changing endpoints in routers where CSRF must be enforced but app/api/deps.py-79- anonymous callers should still receive the semantically-correct 401. app/api/deps.py-80- """ app/api/deps.py-81- return user app/api/deps.py-82- app/api/deps.py-83- app/api/deps.py-84-def rate_limit_user(request: Request, user=Depends(get_current_user)): app/api/deps.py-85- key = f"u:{user.id}:{request.url.path.split('?')[0]}" app/api/deps.py-86- allowed, remaining = rate_limit(key) app/api/deps.py-87- if not allowed: app/api/deps.py-88- raise HTTPException(status_code=429, detail="Rate limit exceeded; try again shortly") app/api/deps.py-89- return True