feat: 支持x-login-as三参数认证、auth_url跟随base_url

- 认证请求增加x-login-as header支持连接账户
- auth_url根据base_url动态构建,不再硬编码生产环境
- 默认Base URL改为demo环境
- 设置页面新增Account ID字段,带tooltip说明
This commit is contained in:
zqq61
2026-03-16 00:44:27 +08:00
parent 01773500af
commit 81d9c3a7e1
3 changed files with 45 additions and 15 deletions

View File

@@ -2,16 +2,20 @@
import logging import logging
import httpx import httpx
from datetime import datetime, timezone, timedelta from datetime import datetime, timezone, timedelta
from typing import Optional
from airwallex.client import AirwallexClient from airwallex.client import AirwallexClient
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class ProxiedAirwallexClient(AirwallexClient): 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._proxy_url = proxy_url
self._login_as = login_as
super().__init__(**kwargs) super().__init__(**kwargs)
# Replace the default httpx client with a proxied one # Replace the default httpx client with a proxied one
if proxy_url: if proxy_url:
@@ -27,26 +31,30 @@ class ProxiedAirwallexClient(AirwallexClient):
logger.info("Airwallex client initialized without proxy") logger.info("Airwallex client initialized without proxy")
def authenticate(self) -> None: 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: if self._token and self._token_expiry and datetime.now(timezone.utc) < self._token_expiry:
return return
logger.info("Authenticating with Airwallex API at %s (proxy: %s)", logger.info("Authenticating with Airwallex API at %s (proxy: %s, login_as: %s)",
self.auth_url, bool(self._proxy_url)) self.auth_url, bool(self._proxy_url), self._login_as or "none")
auth_kwargs: dict = {"timeout": self.request_timeout} auth_kwargs: dict = {"timeout": self.request_timeout}
if self._proxy_url: if self._proxy_url:
auth_kwargs["proxy"] = 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) auth_client = httpx.Client(**auth_kwargs)
try: try:
response = auth_client.post( response = auth_client.post(
self.auth_url, self.auth_url,
headers={ headers=auth_headers,
"Content-Type": "application/json",
"x-client-id": self.client_id,
"x-api-key": self.api_key,
},
content="{}", content="{}",
) )
@@ -61,3 +69,11 @@ class ProxiedAirwallexClient(AirwallexClient):
logger.info("Authentication successful, token expires in 28 minutes") logger.info("Authentication successful, token expires in 28 minutes")
finally: finally:
auth_client.close() 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

View File

@@ -43,13 +43,14 @@ def get_client(db: Session) -> ProxiedAirwallexClient:
client_id = _get_setting(db, "airwallex_client_id") client_id = _get_setting(db, "airwallex_client_id")
api_key = _get_setting(db, "airwallex_api_key") 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) proxy_url = _build_proxy_url(db)
if not client_id or not api_key: if not client_id or not api_key:
raise HTTPException(status_code=400, detail="Airwallex 凭证未配置,请在系统设置中填写 Client ID 和 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: if _client_instance and _client_config_hash == config_hash:
return _client_instance return _client_instance
@@ -60,11 +61,16 @@ def get_client(db: Session) -> ProxiedAirwallexClient:
except Exception: except Exception:
pass pass
# auth_url must match base_url domain
auth_url = base_url.rstrip("/") + "/api/v1/authentication/login"
_client_instance = ProxiedAirwallexClient( _client_instance = ProxiedAirwallexClient(
proxy_url=proxy_url, proxy_url=proxy_url,
login_as=login_as or None,
client_id=client_id, client_id=client_id,
api_key=api_key, api_key=api_key,
base_url=base_url, base_url=base_url,
auth_url=auth_url,
) )
_client_config_hash = config_hash _client_config_hash = config_hash
return _client_instance return _client_instance

View File

@@ -139,19 +139,27 @@ export default function Settings() {
<Form form={form} layout="vertical" disabled={loading}> <Form form={form} layout="vertical" disabled={loading}>
<Card title="Airwallex 凭证" style={{ marginBottom: 16 }}> <Card title="Airwallex 凭证" style={{ marginBottom: 16 }}>
<Form.Item <Form.Item
label="Client ID" label="Client ID (x-client-id)"
name="airwallex_client_id" name="airwallex_client_id"
rules={[{ required: true, message: '请输入 Client ID' }]} rules={[{ required: true, message: '请输入 Client ID' }]}
tooltip="平台级 Client ID通常以 org_ 开头"
> >
<Input placeholder="请输入 Client ID" /> <Input placeholder="例如: org_xxxxx" />
</Form.Item> </Form.Item>
<Form.Item <Form.Item
label="API Key" label="API Key (x-api-key)"
name="airwallex_api_key" name="airwallex_api_key"
rules={[{ required: true, message: '请输入 API Key' }]} rules={[{ required: true, message: '请输入 API Key' }]}
> >
<Input.Password placeholder="请输入 API Key" /> <Input.Password placeholder="请输入 API Key" />
</Form.Item> </Form.Item>
<Form.Item
label="Account ID (x-login-as)"
name="airwallex_login_as"
tooltip="连接账户 ID通常以 acct_ 开头。如果不需要代登录可留空"
>
<Input placeholder="例如: acct_xxxxx可选" />
</Form.Item>
<Form.Item label="Base URL" name="airwallex_base_url"> <Form.Item label="Base URL" name="airwallex_base_url">
<Input placeholder="https://api-demo.airwallex.com/" /> <Input placeholder="https://api-demo.airwallex.com/" />
</Form.Item> </Form.Item>