diff --git a/backend/app/proxy_client.py b/backend/app/proxy_client.py index 3904fc3..bf44882 100644 --- a/backend/app/proxy_client.py +++ b/backend/app/proxy_client.py @@ -2,16 +2,20 @@ import logging import httpx from datetime import datetime, timezone, timedelta +from typing import Optional from airwallex.client import AirwallexClient logger = logging.getLogger(__name__) class ProxiedAirwallexClient(AirwallexClient): - """AirwallexClient that routes requests through an HTTP or SOCKS5 proxy.""" + """AirwallexClient that routes requests through an HTTP or SOCKS5 proxy. + Also supports x-login-as header for connected accounts. + """ - def __init__(self, proxy_url: str | None = None, **kwargs): + def __init__(self, proxy_url: str | None = None, login_as: str | None = None, **kwargs): self._proxy_url = proxy_url + self._login_as = login_as super().__init__(**kwargs) # Replace the default httpx client with a proxied one if proxy_url: @@ -27,26 +31,30 @@ class ProxiedAirwallexClient(AirwallexClient): logger.info("Airwallex client initialized without proxy") def authenticate(self) -> None: - """Override authenticate to use proxy for auth requests too.""" + """Override authenticate to use proxy and x-login-as for auth requests.""" if self._token and self._token_expiry and datetime.now(timezone.utc) < self._token_expiry: return - logger.info("Authenticating with Airwallex API at %s (proxy: %s)", - self.auth_url, bool(self._proxy_url)) + logger.info("Authenticating with Airwallex API at %s (proxy: %s, login_as: %s)", + self.auth_url, bool(self._proxy_url), self._login_as or "none") auth_kwargs: dict = {"timeout": self.request_timeout} if self._proxy_url: auth_kwargs["proxy"] = self._proxy_url + auth_headers = { + "Content-Type": "application/json", + "x-client-id": self.client_id, + "x-api-key": self.api_key, + } + if self._login_as: + auth_headers["x-login-as"] = self._login_as + auth_client = httpx.Client(**auth_kwargs) try: response = auth_client.post( self.auth_url, - headers={ - "Content-Type": "application/json", - "x-client-id": self.client_id, - "x-api-key": self.api_key, - }, + headers=auth_headers, content="{}", ) @@ -61,3 +69,11 @@ class ProxiedAirwallexClient(AirwallexClient): logger.info("Authentication successful, token expires in 28 minutes") finally: auth_client.close() + + @property + def headers(self): + """Add x-login-as to all API request headers.""" + h = super().headers + if self._login_as: + h["x-login-as"] = self._login_as + return h diff --git a/backend/app/services/airwallex_service.py b/backend/app/services/airwallex_service.py index 98694fd..5f2c965 100644 --- a/backend/app/services/airwallex_service.py +++ b/backend/app/services/airwallex_service.py @@ -43,13 +43,14 @@ def get_client(db: Session) -> ProxiedAirwallexClient: client_id = _get_setting(db, "airwallex_client_id") api_key = _get_setting(db, "airwallex_api_key") - base_url = _get_setting(db, "airwallex_base_url", "https://api.airwallex.com/") + base_url = _get_setting(db, "airwallex_base_url", "https://api-demo.airwallex.com/") + login_as = _get_setting(db, "airwallex_login_as") proxy_url = _build_proxy_url(db) if not client_id or not api_key: raise HTTPException(status_code=400, detail="Airwallex 凭证未配置,请在系统设置中填写 Client ID 和 API Key") - config_hash = f"{client_id}:{api_key}:{base_url}:{proxy_url}" + config_hash = f"{client_id}:{api_key}:{base_url}:{login_as}:{proxy_url}" if _client_instance and _client_config_hash == config_hash: return _client_instance @@ -60,11 +61,16 @@ def get_client(db: Session) -> ProxiedAirwallexClient: except Exception: pass + # auth_url must match base_url domain + auth_url = base_url.rstrip("/") + "/api/v1/authentication/login" + _client_instance = ProxiedAirwallexClient( proxy_url=proxy_url, + login_as=login_as or None, client_id=client_id, api_key=api_key, base_url=base_url, + auth_url=auth_url, ) _client_config_hash = config_hash return _client_instance diff --git a/frontend/src/pages/Settings.tsx b/frontend/src/pages/Settings.tsx index 1908961..1a46c48 100644 --- a/frontend/src/pages/Settings.tsx +++ b/frontend/src/pages/Settings.tsx @@ -139,19 +139,27 @@ export default function Settings() {