feat: 添加WebSocket票据认证和钱包类型支持

- 新增WebSocket票据服务,用于短期有效的WebSocket连接认证
- 支持Magic和Safe两种钱包类型,分别对应邮箱/OAuth登录和MetaMask用户
- 添加登录频率限制和安全防护
- 优化日志记录,屏蔽敏感信息
- 升级ethers.js到v6.16.0
- 新增多语言钱包类型说明
- 重构代理地址计算逻辑,支持CREATE2计算Magic代理地址
This commit is contained in:
wry5560
2026-01-03 12:27:59 +08:00
parent 591e678f73
commit 5a83444b56
26 changed files with 828 additions and 180 deletions
+34 -7
View File
@@ -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' ? (
<>
+5 -1
View File
@@ -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",
+5 -1
View File
@@ -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 管理",
+5 -1
View File
@@ -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 管理",
+10 -3
View File
@@ -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', {})
},
/**
+32 -17
View File
@@ -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 URLtoken认证
* 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`