53:@router.post("/accounts/connect") 54-async def connect_start(user: User = Depends(get_current_user_csrf), 55- db: AsyncSession = Depends(get_db)): 56- """Return the Google authorization URL for this user to open (real OAuth start). 57- Uses the user's most recently imported client config when available.""" 58- from sqlalchemy import select as _select 59- 60- cfg_row = (await db.execute( 61- _select(OAuthClientConfig) 62- .where(OAuthClientConfig.user_id == user.id, OAuthClientConfig.is_active == True) # noqa 63- .order_by(OAuthClientConfig.id.desc()))).scalars().first() 64- if cfg_row is not None: 65- client = gmail_service.resolve_client_config(cfg_row) 66- else: 67- try: 68- client = gmail_service.resolve_client_config() 69- except gmail_service.GmailOauthError: 70- raise HTTPException(400, "No Google OAuth client configuration imported yet. " 71- "Use 'Import Gmail Configuration' first.") 72- from app.api.routers.auth import google_start_impl 73- return await google_start_impl(user, client) 74- 75- 76-@router.post("/accounts/{account_id}/test") 77-async def test_account(account_id: int, user: User = Depends(get_current_user_csrf), 78- db: AsyncSession = Depends(get_db)): 79- """Real connection test: verify OAuth creds and Gmail API profile.""" 80- account = await db.get(GmailAccount, account_id) 81- if not account or account.user_id != user.id: 82- raise HTTPException(404, "Account not found") 83- token = account.oauth_token 84- if not token: 85- return {"ok": False, "detail": "No OAuth token stored for this account"} 86- from app.db.session import SyncSession 87- with SyncSession() as s: 88- acc_sync = s.get(GmailAccount, account.id) 89- tok_sync = s.get(OAuthToken, token.id) 90- try: 91- client = gmail_service.build_gmail_client_from_account(acc_sync, tok_sync) 92- result = client.verify_connection() 93- except Exception as e: ===parse_client_json=== 81:def parse_client_json(content: bytes) -> dict: 82- """Validate an uploaded client_secret JSON. Returns normalized dict; raises on invalid.""" 83- try: 84- data = json.loads(content.decode("utf-8")) 85- except Exception as e: 86- raise GmailOauthError(f"File is not valid JSON: {e}") 87- inst = data.get("installed") or data.get("web") 88- if not inst or not inst.get("client_id") or not inst.get("client_secret"): 89- raise GmailOauthError("JSON must contain an 'installed' or 'web' object with client_id and client_secret") 90- return { 91- "project_id": inst.get("project", "") or inst.get("project_id", ""), 92- "client_id": inst["client_id"], 93- "client_secret": inst["client_secret"], 94- "redirect_uris": inst.get("redirect_uris", []), 95- "auth_uri": inst.get("auth_uri", ""), 96- "token_uri": inst.get("token_uri", ""), 97- } 98- 99- 100-# --------------------------------------------------------------------------- 101-# Authorization flow 102-# --------------------------------------------------------------------------- 103-def generate_state() -> str: 104- return secrets.token_urlsafe(32) 105- 106- 107-def build_authorization_url(client: dict, state: str) -> str: 108- params = { 109- "client_id": client["client_id"], 110- "redirect_uri": client["redirect_uri"], 111- "response_type": "code",