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:
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user