feat: 添加WebSocket票据认证和钱包类型支持
- 新增WebSocket票据服务,用于短期有效的WebSocket连接认证 - 支持Magic和Safe两种钱包类型,分别对应邮箱/OAuth登录和MetaMask用户 - 添加登录频率限制和安全防护 - 优化日志记录,屏蔽敏感信息 - 升级ethers.js到v6.16.0 - 新增多语言钱包类型说明 - 重构代理地址计算逻辑,支持CREATE2计算Magic代理地址
This commit is contained in:
Generated
+18
-5
@@ -11,7 +11,7 @@
|
||||
"antd": "^5.12.0",
|
||||
"antd-mobile": "^5.34.0",
|
||||
"axios": "^1.6.2",
|
||||
"ethers": "^6.9.0",
|
||||
"ethers": "^6.16.0",
|
||||
"i18next": "^25.7.1",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
@@ -158,6 +158,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz",
|
||||
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.27.1",
|
||||
"@babel/generator": "^7.28.5",
|
||||
@@ -1676,6 +1677,7 @@
|
||||
"version": "18.3.27",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.27.tgz",
|
||||
"integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/prop-types": "*",
|
||||
"csstype": "^3.2.2"
|
||||
@@ -1741,6 +1743,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.21.0.tgz",
|
||||
"integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "6.21.0",
|
||||
"@typescript-eslint/types": "6.21.0",
|
||||
@@ -1937,6 +1940,7 @@
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@@ -2255,6 +2259,7 @@
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.8.25",
|
||||
"caniuse-lite": "^1.0.30001754",
|
||||
@@ -2466,7 +2471,8 @@
|
||||
"node_modules/dayjs": {
|
||||
"version": "1.11.19",
|
||||
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.19.tgz",
|
||||
"integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw=="
|
||||
"integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.4.3",
|
||||
@@ -2687,6 +2693,7 @@
|
||||
"integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==",
|
||||
"deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.2.0",
|
||||
"@eslint-community/regexpp": "^4.6.1",
|
||||
@@ -2877,9 +2884,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/ethers": {
|
||||
"version": "6.15.0",
|
||||
"resolved": "https://registry.npmjs.org/ethers/-/ethers-6.15.0.tgz",
|
||||
"integrity": "sha512-Kf/3ZW54L4UT0pZtsY/rf+EkBU7Qi5nnhonjUb8yTXcxH3cdcWrV2cRyk0Xk/4jK6OoHhxxZHriyhje20If2hQ==",
|
||||
"version": "6.16.0",
|
||||
"resolved": "https://registry.npmjs.org/ethers/-/ethers-6.16.0.tgz",
|
||||
"integrity": "sha512-U1wulmetNymijEhpSEQ7Ct/P/Jw9/e7R1j5XIbPRydgV2DjLVMsULDlNksq3RQnFgKoLlZf88ijYtWEXcPa07A==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
@@ -2890,6 +2897,7 @@
|
||||
"url": "https://www.buymeacoffee.com/ricmoo"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@adraffy/ens-normalize": "1.10.1",
|
||||
"@noble/curves": "1.2.0",
|
||||
@@ -3364,6 +3372,7 @@
|
||||
"url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project"
|
||||
}
|
||||
],
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.28.4"
|
||||
},
|
||||
@@ -5432,6 +5441,7 @@
|
||||
"version": "18.3.1",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
|
||||
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0"
|
||||
},
|
||||
@@ -5443,6 +5453,7 @@
|
||||
"version": "18.3.1",
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
|
||||
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0",
|
||||
"scheduler": "^0.23.2"
|
||||
@@ -6010,6 +6021,7 @@
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"devOptional": true,
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@@ -6182,6 +6194,7 @@
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz",
|
||||
"integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.21.3",
|
||||
"postcss": "^8.4.43",
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"antd": "^5.12.0",
|
||||
"antd-mobile": "^5.34.0",
|
||||
"axios": "^1.6.2",
|
||||
"ethers": "^6.9.0",
|
||||
"ethers": "^6.16.0",
|
||||
"i18next": "^25.7.1",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
import { useState } from 'react'
|
||||
import { Form, Input, Button, Radio, Space, Alert } from 'antd'
|
||||
import { Form, Input, Button, Radio, Space, Alert, Tooltip } from 'antd'
|
||||
import { QuestionCircleOutlined } from '@ant-design/icons'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useAccountStore } from '../store/accountStore'
|
||||
import {
|
||||
getAddressFromPrivateKey,
|
||||
import {
|
||||
getAddressFromPrivateKey,
|
||||
getAddressFromMnemonic,
|
||||
getPrivateKeyFromMnemonic,
|
||||
isValidWalletAddress,
|
||||
isValidWalletAddress,
|
||||
isValidPrivateKey,
|
||||
isValidMnemonic
|
||||
} from '../utils'
|
||||
import { useMediaQuery } from 'react-responsive'
|
||||
|
||||
type ImportType = 'privateKey' | 'mnemonic'
|
||||
type WalletType = 'magic' | 'safe'
|
||||
|
||||
interface AccountImportFormProps {
|
||||
form: any
|
||||
@@ -33,6 +35,7 @@ const AccountImportForm: React.FC<AccountImportFormProps> = ({
|
||||
const isMobile = useMediaQuery({ maxWidth: 768 })
|
||||
const { importAccount, loading } = useAccountStore()
|
||||
const [importType, setImportType] = useState<ImportType>('privateKey')
|
||||
const [walletType, setWalletType] = useState<WalletType>('magic')
|
||||
const [derivedAddress, setDerivedAddress] = useState<string>('')
|
||||
const [addressError, setAddressError] = useState<string>('')
|
||||
|
||||
@@ -141,7 +144,8 @@ const AccountImportForm: React.FC<AccountImportFormProps> = ({
|
||||
await importAccount({
|
||||
privateKey: privateKey,
|
||||
walletAddress: walletAddress,
|
||||
accountName: values.accountName
|
||||
accountName: values.accountName,
|
||||
walletType: walletType
|
||||
})
|
||||
|
||||
// 等待store更新
|
||||
@@ -189,8 +193,8 @@ const AccountImportForm: React.FC<AccountImportFormProps> = ({
|
||||
size={isMobile ? 'middle' : 'large'}
|
||||
>
|
||||
<Form.Item label={t('accountImport.importMethod')}>
|
||||
<Radio.Group
|
||||
value={importType}
|
||||
<Radio.Group
|
||||
value={importType}
|
||||
onChange={(e) => {
|
||||
setImportType(e.target.value)
|
||||
setDerivedAddress('')
|
||||
@@ -202,6 +206,29 @@ const AccountImportForm: React.FC<AccountImportFormProps> = ({
|
||||
<Radio value="mnemonic">{t('accountImport.mnemonic')}</Radio>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={
|
||||
<span>
|
||||
{t('accountImport.walletType')}{' '}
|
||||
<Tooltip title={t('accountImport.walletTypeHelp')}>
|
||||
<QuestionCircleOutlined style={{ color: '#999' }} />
|
||||
</Tooltip>
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<Radio.Group
|
||||
value={walletType}
|
||||
onChange={(e) => setWalletType(e.target.value)}
|
||||
>
|
||||
<Radio value="magic">
|
||||
{t('accountImport.walletTypeMagic')}
|
||||
</Radio>
|
||||
<Radio value="safe">
|
||||
{t('accountImport.walletTypeSafe')}
|
||||
</Radio>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
|
||||
{importType === 'privateKey' ? (
|
||||
<>
|
||||
|
||||
@@ -199,7 +199,11 @@
|
||||
"importFailed": "Failed to import account",
|
||||
"derivedAddress": "Derived Address",
|
||||
"addressError": "Cannot derive address from private key",
|
||||
"addressErrorMnemonic": "Cannot derive address from mnemonic"
|
||||
"addressErrorMnemonic": "Cannot derive address from mnemonic",
|
||||
"walletType": "Wallet Type",
|
||||
"walletTypeHelp": "Magic: Polymarket accounts logged in via email or social accounts (Google, Twitter, etc.); MetaMask: Polymarket accounts connected via browser wallets like MetaMask",
|
||||
"walletTypeMagic": "Magic (Email/Social Login)",
|
||||
"walletTypeSafe": "MetaMask (Browser Wallet)"
|
||||
},
|
||||
"leader": {
|
||||
"title": "Leader Management",
|
||||
|
||||
@@ -199,7 +199,11 @@
|
||||
"importFailed": "导入账户失败",
|
||||
"derivedAddress": "推导地址",
|
||||
"addressError": "无法从私钥推导地址",
|
||||
"addressErrorMnemonic": "无法从助记词推导地址"
|
||||
"addressErrorMnemonic": "无法从助记词推导地址",
|
||||
"walletType": "钱包类型",
|
||||
"walletTypeHelp": "Magic:通过邮箱或社交账号(如 Google、Twitter)登录的 Polymarket 账户;MetaMask:使用 MetaMask 等浏览器钱包连接的 Polymarket 账户",
|
||||
"walletTypeMagic": "Magic(邮箱/社交账号登录)",
|
||||
"walletTypeSafe": "MetaMask(浏览器钱包)"
|
||||
},
|
||||
"leader": {
|
||||
"title": "Leader 管理",
|
||||
|
||||
@@ -199,7 +199,11 @@
|
||||
"importFailed": "導入賬戶失敗",
|
||||
"derivedAddress": "推導地址",
|
||||
"addressError": "無法從私鑰推導地址",
|
||||
"addressErrorMnemonic": "無法從助記詞推導地址"
|
||||
"addressErrorMnemonic": "無法從助記詞推導地址",
|
||||
"walletType": "錢包類型",
|
||||
"walletTypeHelp": "Magic:透過郵箱或社群帳號(如 Google、Twitter)登入的 Polymarket 帳戶;MetaMask:使用 MetaMask 等瀏覽器錢包連接的 Polymarket 帳戶",
|
||||
"walletTypeMagic": "Magic(郵箱/社群帳號登入)",
|
||||
"walletTypeSafe": "MetaMask(瀏覽器錢包)"
|
||||
},
|
||||
"leader": {
|
||||
"title": "Leader 管理",
|
||||
|
||||
@@ -181,18 +181,25 @@ export const apiService = {
|
||||
*/
|
||||
login: (data: { username: string; password: string }) =>
|
||||
apiClient.post<ApiResponse<{ token: string }>>('/auth/login', data),
|
||||
|
||||
|
||||
/**
|
||||
* 重置密码
|
||||
*/
|
||||
resetPassword: (data: { resetKey: string; username: string; newPassword: string }) =>
|
||||
apiClient.post<ApiResponse<void>>('/auth/reset-password', data),
|
||||
|
||||
|
||||
/**
|
||||
* 检查是否首次使用
|
||||
*/
|
||||
checkFirstUse: () =>
|
||||
apiClient.post<ApiResponse<{ isFirstUse: boolean }>>('/auth/check-first-use', {})
|
||||
apiClient.post<ApiResponse<{ isFirstUse: boolean }>>('/auth/check-first-use', {}),
|
||||
|
||||
/**
|
||||
* 获取 WebSocket 连接票据
|
||||
* 返回一个短期有效(30秒)的一次性票据
|
||||
*/
|
||||
getWebSocketTicket: () =>
|
||||
apiClient.post<ApiResponse<{ ticket: string }>>('/auth/ws-ticket', {})
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
@@ -51,30 +51,33 @@ class WebSocketManager {
|
||||
|
||||
/**
|
||||
* 连接 WebSocket(全局共享连接)
|
||||
* 使用短期票据认证,避免在 URL 中暴露 JWT
|
||||
*/
|
||||
connect(): void {
|
||||
async connect(): Promise<void> {
|
||||
// 检查是否有token,未登录不允许连接
|
||||
const token = this.getToken()
|
||||
if (!token) {
|
||||
console.log('[WebSocket] 未登录,不建立连接')
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
// 如果已经连接或正在连接,直接返回
|
||||
if (this.ws?.readyState === WebSocket.OPEN || this.isConnecting) {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
// 如果正在卸载,不允许连接
|
||||
if (this.isUnmounting) {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
this.isConnecting = true
|
||||
const wsUrl = this.getWebSocketUrl()
|
||||
console.log('[WebSocket] 正在连接:', wsUrl)
|
||||
|
||||
|
||||
try {
|
||||
// 获取短期票据
|
||||
const wsUrl = await this.getWebSocketUrl()
|
||||
console.log('[WebSocket] 正在连接...')
|
||||
|
||||
// 如果已经有连接(但状态不是 OPEN),先关闭
|
||||
if (this.ws) {
|
||||
try {
|
||||
@@ -84,10 +87,10 @@ class WebSocketManager {
|
||||
}
|
||||
this.ws = null
|
||||
}
|
||||
|
||||
|
||||
const ws = new WebSocket(wsUrl)
|
||||
this.ws = ws
|
||||
|
||||
|
||||
ws.onopen = () => {
|
||||
console.log('[WebSocket] 连接成功')
|
||||
this.isConnecting = false
|
||||
@@ -95,17 +98,17 @@ class WebSocketManager {
|
||||
this.startPing()
|
||||
this.resubscribeAll() // 重新订阅所有频道
|
||||
}
|
||||
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
this.handleMessage(event.data)
|
||||
}
|
||||
|
||||
|
||||
ws.onerror = (error) => {
|
||||
console.error('[WebSocket] 连接错误:', error)
|
||||
this.isConnecting = false
|
||||
this.notifyConnectionStatus(false)
|
||||
}
|
||||
|
||||
|
||||
ws.onclose = () => {
|
||||
console.log('[WebSocket] 连接关闭')
|
||||
this.isConnecting = false
|
||||
@@ -324,14 +327,14 @@ class WebSocketManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 WebSocket URL(带token认证)
|
||||
* 获取 WebSocket URL(使用短期票据认证)
|
||||
* 默认使用相对路径 /ws(通过反向代理转发)
|
||||
* 如果设置了 VITE_WS_URL 环境变量,则使用完整 URL(用于跨域场景)
|
||||
*/
|
||||
private getWebSocketUrl(): string {
|
||||
private async getWebSocketUrl(): Promise<string> {
|
||||
const envWsUrl = import.meta.env.VITE_WS_URL
|
||||
let wsBaseUrl: string
|
||||
|
||||
|
||||
if (envWsUrl) {
|
||||
// 如果设置了环境变量,使用完整 URL(支持跨域)
|
||||
wsBaseUrl = envWsUrl
|
||||
@@ -341,10 +344,22 @@ class WebSocketManager {
|
||||
const host = window.location.host
|
||||
wsBaseUrl = `${protocol}//${host}`
|
||||
}
|
||||
|
||||
|
||||
// 获取短期票据(避免在 URL 中暴露 JWT)
|
||||
// 使用动态导入避免循环依赖
|
||||
try {
|
||||
const { apiService } = await import('./api')
|
||||
const response = await apiService.auth.getWebSocketTicket()
|
||||
if (response.data.code === 0 && response.data.data?.ticket) {
|
||||
return `${wsBaseUrl}/ws?ticket=${encodeURIComponent(response.data.data.ticket)}`
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('[WebSocket] 获取票据失败,尝试使用 token 认证:', error)
|
||||
}
|
||||
|
||||
// 兼容旧方式:如果获取票据失败,回退到使用 token(不推荐)
|
||||
const token = this.getToken()
|
||||
if (token) {
|
||||
// 通过查询参数传递token
|
||||
return `${wsBaseUrl}/ws?token=${encodeURIComponent(token)}`
|
||||
}
|
||||
return `${wsBaseUrl}/ws`
|
||||
|
||||
Reference in New Issue
Block a user