feat: 添加WebSocket票据认证和钱包类型支持
- 新增WebSocket票据服务,用于短期有效的WebSocket连接认证 - 支持Magic和Safe两种钱包类型,分别对应邮箱/OAuth登录和MetaMask用户 - 添加登录频率限制和安全防护 - 优化日志记录,屏蔽敏感信息 - 升级ethers.js到v6.16.0 - 新增多语言钱包类型说明 - 重构代理地址计算逻辑,支持CREATE2计算Magic代理地址
This commit is contained in:
@@ -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