Skip to content
Toggle navigation
P
Projects
G
Groups
S
Snippets
Help
phsl
/
new-api
This project
Loading...
Sign in
Toggle navigation
Go to a project
Project
Repository
Issues
0
Merge Requests
0
Pipelines
Wiki
Snippets
Members
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Commit
8eace6bb
authored
Sep 30, 2025
by
Seefs
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: 通用二步验证
parent
722e2c2a
Hide whitespace changes
Inline
Side-by-side
Showing
12 changed files
with
800 additions
and
323 deletions
+800
-323
controller/channel.go
+4
-119
controller/secure_verification.go
+313
-0
middleware/secure_verification.go
+131
-0
router/api-router.go
+5
-2
web/src/components/common/modals/SecureVerificationModal.jsx
+156
-142
web/src/components/settings/SystemSetting.jsx
+1
-1
web/src/components/table/channels/modals/EditChannelModal.jsx
+31
-12
web/src/helpers/secureApiCall.js
+63
-0
web/src/hooks/common/useSecureVerification.jsx
+37
-10
web/src/i18n/locales/en.json
+1
-0
web/src/i18n/locales/zh.json
+2
-1
web/src/services/secureVerification.js
+56
-36
No files found.
controller/channel.go
View file @
8eace6bb
...
...
@@ -384,19 +384,9 @@ func GetChannel(c *gin.Context) {
return
}
// GetChannelKey 验证2FA或Passkey后获取渠道密钥
// GetChannelKey 获取渠道密钥(需要通过安全验证中间件)
// 此函数依赖 SecureVerificationRequired 中间件,确保用户已通过安全验证
func
GetChannelKey
(
c
*
gin
.
Context
)
{
type
GetChannelKeyRequest
struct
{
Code
string
`json:"code,omitempty"`
// 2FA验证码或备用码
Method
string
`json:"method,omitempty"`
// 验证方式: "2fa" 或 "passkey"
}
var
req
GetChannelKeyRequest
if
err
:=
c
.
ShouldBindJSON
(
&
req
);
err
!=
nil
{
common
.
ApiError
(
c
,
fmt
.
Errorf
(
"参数错误: %v"
,
err
))
return
}
userId
:=
c
.
GetInt
(
"id"
)
channelId
,
err
:=
strconv
.
Atoi
(
c
.
Param
(
"id"
))
if
err
!=
nil
{
...
...
@@ -404,111 +394,6 @@ func GetChannelKey(c *gin.Context) {
return
}
// 检查用户支持的验证方式
twoFA
,
err
:=
model
.
GetTwoFAByUserId
(
userId
)
if
err
!=
nil
{
common
.
ApiError
(
c
,
fmt
.
Errorf
(
"获取2FA信息失败: %v"
,
err
))
return
}
passkey
,
passkeyErr
:=
model
.
GetPasskeyByUserID
(
userId
)
hasPasskey
:=
passkeyErr
==
nil
&&
passkey
!=
nil
has2FA
:=
twoFA
!=
nil
&&
twoFA
.
IsEnabled
// 至少需要启用一种验证方式
if
!
has2FA
&&
!
hasPasskey
{
common
.
ApiError
(
c
,
fmt
.
Errorf
(
"用户未启用2FA或Passkey,无法查看密钥"
))
return
}
// 根据请求的验证方式进行验证
switch
req
.
Method
{
case
"2fa"
:
if
!
has2FA
{
common
.
ApiError
(
c
,
fmt
.
Errorf
(
"用户未启用2FA"
))
return
}
if
req
.
Code
==
""
{
common
.
ApiError
(
c
,
fmt
.
Errorf
(
"2FA验证码不能为空"
))
return
}
if
!
validateTwoFactorAuth
(
twoFA
,
req
.
Code
)
{
common
.
ApiError
(
c
,
fmt
.
Errorf
(
"验证码或备用码错误,请重试"
))
return
}
case
"passkey"
:
if
!
hasPasskey
{
common
.
ApiError
(
c
,
fmt
.
Errorf
(
"用户未启用Passkey"
))
return
}
// Passkey验证已在前端完成,这里只需要检查是否有有效的Passkey验证会话
// 由于Passkey验证是基于WebAuthn协议的,验证过程已经在PasskeyVerifyFinish中完成
// 这里我们可以设置一个临时标记来验证Passkey验证是否成功
default
:
// 自动选择验证方式:如果提供了code则使用2FA,否则需要用户明确指定
if
req
.
Code
!=
""
&&
has2FA
{
if
!
validateTwoFactorAuth
(
twoFA
,
req
.
Code
)
{
common
.
ApiError
(
c
,
fmt
.
Errorf
(
"验证码或备用码错误,请重试"
))
return
}
}
else
{
common
.
ApiError
(
c
,
fmt
.
Errorf
(
"请指定验证方式(method: '2fa' 或 'passkey')"
))
return
}
}
// 获取渠道信息(包含密钥)
channel
,
err
:=
model
.
GetChannelById
(
channelId
,
true
)
if
err
!=
nil
{
common
.
ApiError
(
c
,
fmt
.
Errorf
(
"获取渠道信息失败: %v"
,
err
))
return
}
if
channel
==
nil
{
common
.
ApiError
(
c
,
fmt
.
Errorf
(
"渠道不存在"
))
return
}
// 记录操作日志
logMethod
:=
req
.
Method
if
logMethod
==
""
{
logMethod
=
"2fa"
}
model
.
RecordLog
(
userId
,
model
.
LogTypeSystem
,
fmt
.
Sprintf
(
"查看渠道密钥信息 (渠道ID: %d, 验证方式: %s)"
,
channelId
,
logMethod
))
// 统一的成功响应格式
c
.
JSON
(
http
.
StatusOK
,
gin
.
H
{
"success"
:
true
,
"message"
:
"验证成功"
,
"data"
:
map
[
string
]
interface
{}{
"key"
:
channel
.
Key
,
},
})
}
// GetChannelKeyWithPasskey 使用Passkey验证查看渠道密钥
func
GetChannelKeyWithPasskey
(
c
*
gin
.
Context
)
{
userId
:=
c
.
GetInt
(
"id"
)
channelId
,
err
:=
strconv
.
Atoi
(
c
.
Param
(
"id"
))
if
err
!=
nil
{
common
.
ApiError
(
c
,
fmt
.
Errorf
(
"渠道ID格式错误: %v"
,
err
))
return
}
// 检查用户是否已绑定Passkey
passkey
,
err
:=
model
.
GetPasskeyByUserID
(
userId
)
if
err
!=
nil
{
common
.
ApiError
(
c
,
fmt
.
Errorf
(
"用户未绑定Passkey,无法使用此验证方式"
))
return
}
if
passkey
==
nil
{
common
.
ApiError
(
c
,
fmt
.
Errorf
(
"用户未绑定Passkey"
))
return
}
// 获取渠道信息(包含密钥)
channel
,
err
:=
model
.
GetChannelById
(
channelId
,
true
)
if
err
!=
nil
{
...
...
@@ -522,12 +407,12 @@ func GetChannelKeyWithPasskey(c *gin.Context) {
}
// 记录操作日志
model
.
RecordLog
(
userId
,
model
.
LogTypeSystem
,
fmt
.
Sprintf
(
"查看渠道密钥信息 (渠道ID: %d
, 验证方式: passkey
)"
,
channelId
))
model
.
RecordLog
(
userId
,
model
.
LogTypeSystem
,
fmt
.
Sprintf
(
"查看渠道密钥信息 (渠道ID: %d)"
,
channelId
))
// 返回渠道密钥
c
.
JSON
(
http
.
StatusOK
,
gin
.
H
{
"success"
:
true
,
"message"
:
"
Passkey验证
成功"
,
"message"
:
"
获取
成功"
,
"data"
:
map
[
string
]
interface
{}{
"key"
:
channel
.
Key
,
},
...
...
controller/secure_verification.go
0 → 100644
View file @
8eace6bb
package
controller
import
(
"fmt"
"net/http"
"one-api/common"
"one-api/model"
passkeysvc
"one-api/service/passkey"
"one-api/setting/system_setting"
"time"
"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"
)
const
(
// SecureVerificationSessionKey 安全验证的 session key
SecureVerificationSessionKey
=
"secure_verified_at"
// SecureVerificationTimeout 验证有效期(秒)
SecureVerificationTimeout
=
300
// 5分钟
)
type
UniversalVerifyRequest
struct
{
Method
string
`json:"method"`
// "2fa" 或 "passkey"
Code
string
`json:"code,omitempty"`
}
type
VerificationStatusResponse
struct
{
Verified
bool
`json:"verified"`
ExpiresAt
int64
`json:"expires_at,omitempty"`
}
// UniversalVerify 通用验证接口
// 支持 2FA 和 Passkey 验证,验证成功后在 session 中记录时间戳
func
UniversalVerify
(
c
*
gin
.
Context
)
{
userId
:=
c
.
GetInt
(
"id"
)
if
userId
==
0
{
c
.
JSON
(
http
.
StatusUnauthorized
,
gin
.
H
{
"success"
:
false
,
"message"
:
"未登录"
,
})
return
}
var
req
UniversalVerifyRequest
if
err
:=
c
.
ShouldBindJSON
(
&
req
);
err
!=
nil
{
common
.
ApiError
(
c
,
fmt
.
Errorf
(
"参数错误: %v"
,
err
))
return
}
// 获取用户信息
user
:=
&
model
.
User
{
Id
:
userId
}
if
err
:=
user
.
FillUserById
();
err
!=
nil
{
common
.
ApiError
(
c
,
fmt
.
Errorf
(
"获取用户信息失败: %v"
,
err
))
return
}
if
user
.
Status
!=
common
.
UserStatusEnabled
{
common
.
ApiError
(
c
,
fmt
.
Errorf
(
"该用户已被禁用"
))
return
}
// 检查用户的验证方式
twoFA
,
_
:=
model
.
GetTwoFAByUserId
(
userId
)
has2FA
:=
twoFA
!=
nil
&&
twoFA
.
IsEnabled
passkey
,
passkeyErr
:=
model
.
GetPasskeyByUserID
(
userId
)
hasPasskey
:=
passkeyErr
==
nil
&&
passkey
!=
nil
if
!
has2FA
&&
!
hasPasskey
{
common
.
ApiError
(
c
,
fmt
.
Errorf
(
"用户未启用2FA或Passkey"
))
return
}
// 根据验证方式进行验证
var
verified
bool
var
verifyMethod
string
switch
req
.
Method
{
case
"2fa"
:
if
!
has2FA
{
common
.
ApiError
(
c
,
fmt
.
Errorf
(
"用户未启用2FA"
))
return
}
if
req
.
Code
==
""
{
common
.
ApiError
(
c
,
fmt
.
Errorf
(
"验证码不能为空"
))
return
}
verified
=
validateTwoFactorAuth
(
twoFA
,
req
.
Code
)
verifyMethod
=
"2FA"
case
"passkey"
:
if
!
hasPasskey
{
common
.
ApiError
(
c
,
fmt
.
Errorf
(
"用户未启用Passkey"
))
return
}
// Passkey 验证需要先调用 PasskeyVerifyBegin 和 PasskeyVerifyFinish
// 这里只是验证 Passkey 验证流程是否已经完成
// 实际上,前端应该先调用这两个接口,然后再调用本接口
verified
=
true
// Passkey 验证逻辑已在 PasskeyVerifyFinish 中完成
verifyMethod
=
"Passkey"
default
:
common
.
ApiError
(
c
,
fmt
.
Errorf
(
"不支持的验证方式: %s"
,
req
.
Method
))
return
}
if
!
verified
{
common
.
ApiError
(
c
,
fmt
.
Errorf
(
"验证失败,请检查验证码"
))
return
}
// 验证成功,在 session 中记录时间戳
session
:=
sessions
.
Default
(
c
)
now
:=
time
.
Now
()
.
Unix
()
session
.
Set
(
SecureVerificationSessionKey
,
now
)
if
err
:=
session
.
Save
();
err
!=
nil
{
common
.
ApiError
(
c
,
fmt
.
Errorf
(
"保存验证状态失败: %v"
,
err
))
return
}
// 记录日志
model
.
RecordLog
(
userId
,
model
.
LogTypeSystem
,
fmt
.
Sprintf
(
"通用安全验证成功 (验证方式: %s)"
,
verifyMethod
))
c
.
JSON
(
http
.
StatusOK
,
gin
.
H
{
"success"
:
true
,
"message"
:
"验证成功"
,
"data"
:
gin
.
H
{
"verified"
:
true
,
"expires_at"
:
now
+
SecureVerificationTimeout
,
},
})
}
// GetVerificationStatus 获取验证状态
func
GetVerificationStatus
(
c
*
gin
.
Context
)
{
userId
:=
c
.
GetInt
(
"id"
)
if
userId
==
0
{
c
.
JSON
(
http
.
StatusUnauthorized
,
gin
.
H
{
"success"
:
false
,
"message"
:
"未登录"
,
})
return
}
session
:=
sessions
.
Default
(
c
)
verifiedAtRaw
:=
session
.
Get
(
SecureVerificationSessionKey
)
if
verifiedAtRaw
==
nil
{
c
.
JSON
(
http
.
StatusOK
,
gin
.
H
{
"success"
:
true
,
"message"
:
""
,
"data"
:
VerificationStatusResponse
{
Verified
:
false
,
},
})
return
}
verifiedAt
,
ok
:=
verifiedAtRaw
.
(
int64
)
if
!
ok
{
c
.
JSON
(
http
.
StatusOK
,
gin
.
H
{
"success"
:
true
,
"message"
:
""
,
"data"
:
VerificationStatusResponse
{
Verified
:
false
,
},
})
return
}
elapsed
:=
time
.
Now
()
.
Unix
()
-
verifiedAt
if
elapsed
>=
SecureVerificationTimeout
{
// 验证已过期
session
.
Delete
(
SecureVerificationSessionKey
)
_
=
session
.
Save
()
c
.
JSON
(
http
.
StatusOK
,
gin
.
H
{
"success"
:
true
,
"message"
:
""
,
"data"
:
VerificationStatusResponse
{
Verified
:
false
,
},
})
return
}
c
.
JSON
(
http
.
StatusOK
,
gin
.
H
{
"success"
:
true
,
"message"
:
""
,
"data"
:
VerificationStatusResponse
{
Verified
:
true
,
ExpiresAt
:
verifiedAt
+
SecureVerificationTimeout
,
},
})
}
// CheckSecureVerification 检查是否已通过安全验证
// 返回 true 表示验证有效,false 表示需要重新验证
func
CheckSecureVerification
(
c
*
gin
.
Context
)
bool
{
session
:=
sessions
.
Default
(
c
)
verifiedAtRaw
:=
session
.
Get
(
SecureVerificationSessionKey
)
if
verifiedAtRaw
==
nil
{
return
false
}
verifiedAt
,
ok
:=
verifiedAtRaw
.
(
int64
)
if
!
ok
{
return
false
}
elapsed
:=
time
.
Now
()
.
Unix
()
-
verifiedAt
if
elapsed
>=
SecureVerificationTimeout
{
// 验证已过期,清除 session
session
.
Delete
(
SecureVerificationSessionKey
)
_
=
session
.
Save
()
return
false
}
return
true
}
// PasskeyVerifyAndSetSession Passkey 验证完成后设置 session
// 这是一个辅助函数,供 PasskeyVerifyFinish 调用
func
PasskeyVerifyAndSetSession
(
c
*
gin
.
Context
)
{
session
:=
sessions
.
Default
(
c
)
now
:=
time
.
Now
()
.
Unix
()
session
.
Set
(
SecureVerificationSessionKey
,
now
)
_
=
session
.
Save
()
}
// PasskeyVerifyForSecure 用于安全验证的 Passkey 验证流程
// 整合了 begin 和 finish 流程
func
PasskeyVerifyForSecure
(
c
*
gin
.
Context
)
{
if
!
system_setting
.
GetPasskeySettings
()
.
Enabled
{
c
.
JSON
(
http
.
StatusOK
,
gin
.
H
{
"success"
:
false
,
"message"
:
"管理员未启用 Passkey 登录"
,
})
return
}
userId
:=
c
.
GetInt
(
"id"
)
if
userId
==
0
{
c
.
JSON
(
http
.
StatusUnauthorized
,
gin
.
H
{
"success"
:
false
,
"message"
:
"未登录"
,
})
return
}
user
:=
&
model
.
User
{
Id
:
userId
}
if
err
:=
user
.
FillUserById
();
err
!=
nil
{
common
.
ApiError
(
c
,
fmt
.
Errorf
(
"获取用户信息失败: %v"
,
err
))
return
}
if
user
.
Status
!=
common
.
UserStatusEnabled
{
common
.
ApiError
(
c
,
fmt
.
Errorf
(
"该用户已被禁用"
))
return
}
credential
,
err
:=
model
.
GetPasskeyByUserID
(
userId
)
if
err
!=
nil
{
c
.
JSON
(
http
.
StatusOK
,
gin
.
H
{
"success"
:
false
,
"message"
:
"该用户尚未绑定 Passkey"
,
})
return
}
wa
,
err
:=
passkeysvc
.
BuildWebAuthn
(
c
.
Request
)
if
err
!=
nil
{
common
.
ApiError
(
c
,
err
)
return
}
waUser
:=
passkeysvc
.
NewWebAuthnUser
(
user
,
credential
)
sessionData
,
err
:=
passkeysvc
.
PopSessionData
(
c
,
passkeysvc
.
VerifySessionKey
)
if
err
!=
nil
{
common
.
ApiError
(
c
,
err
)
return
}
_
,
err
=
wa
.
FinishLogin
(
waUser
,
*
sessionData
,
c
.
Request
)
if
err
!=
nil
{
common
.
ApiError
(
c
,
err
)
return
}
// 更新凭证的最后使用时间
now
:=
time
.
Now
()
credential
.
LastUsedAt
=
&
now
if
err
:=
model
.
UpsertPasskeyCredential
(
credential
);
err
!=
nil
{
common
.
ApiError
(
c
,
err
)
return
}
// 验证成功,设置 session
PasskeyVerifyAndSetSession
(
c
)
// 记录日志
model
.
RecordLog
(
userId
,
model
.
LogTypeSystem
,
"Passkey 安全验证成功"
)
c
.
JSON
(
http
.
StatusOK
,
gin
.
H
{
"success"
:
true
,
"message"
:
"Passkey 验证成功"
,
"data"
:
gin
.
H
{
"verified"
:
true
,
"expires_at"
:
time
.
Now
()
.
Unix
()
+
SecureVerificationTimeout
,
},
})
}
middleware/secure_verification.go
0 → 100644
View file @
8eace6bb
package
middleware
import
(
"net/http"
"time"
"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"
)
const
(
// SecureVerificationSessionKey 安全验证的 session key(与 controller 保持一致)
SecureVerificationSessionKey
=
"secure_verified_at"
// SecureVerificationTimeout 验证有效期(秒)
SecureVerificationTimeout
=
300
// 5分钟
)
// SecureVerificationRequired 安全验证中间件
// 检查用户是否在有效时间内通过了安全验证
// 如果未验证或验证已过期,返回 401 错误
func
SecureVerificationRequired
()
gin
.
HandlerFunc
{
return
func
(
c
*
gin
.
Context
)
{
// 检查用户是否已登录
userId
:=
c
.
GetInt
(
"id"
)
if
userId
==
0
{
c
.
JSON
(
http
.
StatusUnauthorized
,
gin
.
H
{
"success"
:
false
,
"message"
:
"未登录"
,
})
c
.
Abort
()
return
}
// 检查 session 中的验证时间戳
session
:=
sessions
.
Default
(
c
)
verifiedAtRaw
:=
session
.
Get
(
SecureVerificationSessionKey
)
if
verifiedAtRaw
==
nil
{
c
.
JSON
(
http
.
StatusForbidden
,
gin
.
H
{
"success"
:
false
,
"message"
:
"需要安全验证"
,
"code"
:
"VERIFICATION_REQUIRED"
,
})
c
.
Abort
()
return
}
verifiedAt
,
ok
:=
verifiedAtRaw
.
(
int64
)
if
!
ok
{
// session 数据格式错误
session
.
Delete
(
SecureVerificationSessionKey
)
_
=
session
.
Save
()
c
.
JSON
(
http
.
StatusForbidden
,
gin
.
H
{
"success"
:
false
,
"message"
:
"验证状态异常,请重新验证"
,
"code"
:
"VERIFICATION_INVALID"
,
})
c
.
Abort
()
return
}
// 检查验证是否过期
elapsed
:=
time
.
Now
()
.
Unix
()
-
verifiedAt
if
elapsed
>=
SecureVerificationTimeout
{
// 验证已过期,清除 session
session
.
Delete
(
SecureVerificationSessionKey
)
_
=
session
.
Save
()
c
.
JSON
(
http
.
StatusForbidden
,
gin
.
H
{
"success"
:
false
,
"message"
:
"验证已过期,请重新验证"
,
"code"
:
"VERIFICATION_EXPIRED"
,
})
c
.
Abort
()
return
}
// 验证有效,继续处理请求
c
.
Next
()
}
}
// OptionalSecureVerification 可选的安全验证中间件
// 如果用户已验证,则在 context 中设置标记,但不阻止请求继续
// 用于某些需要区分是否已验证的场景
func
OptionalSecureVerification
()
gin
.
HandlerFunc
{
return
func
(
c
*
gin
.
Context
)
{
userId
:=
c
.
GetInt
(
"id"
)
if
userId
==
0
{
c
.
Set
(
"secure_verified"
,
false
)
c
.
Next
()
return
}
session
:=
sessions
.
Default
(
c
)
verifiedAtRaw
:=
session
.
Get
(
SecureVerificationSessionKey
)
if
verifiedAtRaw
==
nil
{
c
.
Set
(
"secure_verified"
,
false
)
c
.
Next
()
return
}
verifiedAt
,
ok
:=
verifiedAtRaw
.
(
int64
)
if
!
ok
{
c
.
Set
(
"secure_verified"
,
false
)
c
.
Next
()
return
}
elapsed
:=
time
.
Now
()
.
Unix
()
-
verifiedAt
if
elapsed
>=
SecureVerificationTimeout
{
session
.
Delete
(
SecureVerificationSessionKey
)
_
=
session
.
Save
()
c
.
Set
(
"secure_verified"
,
false
)
c
.
Next
()
return
}
c
.
Set
(
"secure_verified"
,
true
)
c
.
Set
(
"secure_verified_at"
,
verifiedAt
)
c
.
Next
()
}
}
// ClearSecureVerification 清除安全验证状态
// 用于用户登出或需要强制重新验证的场景
func
ClearSecureVerification
(
c
*
gin
.
Context
)
{
session
:=
sessions
.
Default
(
c
)
session
.
Delete
(
SecureVerificationSessionKey
)
_
=
session
.
Save
()
}
router/api-router.go
View file @
8eace6bb
...
...
@@ -40,6 +40,10 @@ func SetApiRouter(router *gin.Engine) {
apiRouter
.
POST
(
"/stripe/webhook"
,
controller
.
StripeWebhook
)
// Universal secure verification routes
apiRouter
.
POST
(
"/verify"
,
middleware
.
UserAuth
(),
middleware
.
CriticalRateLimit
(),
controller
.
UniversalVerify
)
apiRouter
.
GET
(
"/verify/status"
,
middleware
.
UserAuth
(),
controller
.
GetVerificationStatus
)
userRoute
:=
apiRouter
.
Group
(
"/user"
)
{
userRoute
.
POST
(
"/register"
,
middleware
.
CriticalRateLimit
(),
middleware
.
TurnstileCheck
(),
controller
.
Register
)
...
...
@@ -124,8 +128,7 @@ func SetApiRouter(router *gin.Engine) {
channelRoute
.
GET
(
"/models"
,
controller
.
ChannelListModels
)
channelRoute
.
GET
(
"/models_enabled"
,
controller
.
EnabledListModels
)
channelRoute
.
GET
(
"/:id"
,
controller
.
GetChannel
)
channelRoute
.
POST
(
"/:id/key"
,
middleware
.
CriticalRateLimit
(),
middleware
.
DisableCache
(),
controller
.
GetChannelKey
)
channelRoute
.
POST
(
"/:id/key/passkey"
,
middleware
.
CriticalRateLimit
(),
middleware
.
DisableCache
(),
controller
.
GetChannelKeyWithPasskey
)
channelRoute
.
POST
(
"/:id/key"
,
middleware
.
CriticalRateLimit
(),
middleware
.
DisableCache
(),
middleware
.
SecureVerificationRequired
(),
controller
.
GetChannelKey
)
channelRoute
.
GET
(
"/test"
,
controller
.
TestAllChannels
)
channelRoute
.
GET
(
"/test/:id"
,
controller
.
TestChannel
)
channelRoute
.
GET
(
"/update_balance"
,
controller
.
UpdateAllChannelsBalance
)
...
...
web/src/components/common/modals/SecureVerificationModal.jsx
View file @
8eace6bb
...
...
@@ -17,9 +17,9 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import
React
from
'react'
;
import
React
,
{
useEffect
,
useState
}
from
'react'
;
import
{
useTranslation
}
from
'react-i18next'
;
import
{
Modal
,
Button
,
Input
,
Typography
,
Tabs
,
TabPane
,
Card
}
from
'@douyinfe/semi-ui'
;
import
{
Modal
,
Button
,
Input
,
Typography
,
Tabs
,
TabPane
,
Space
,
Spin
}
from
'@douyinfe/semi-ui'
;
/**
* 通用安全验证模态框组件
...
...
@@ -47,14 +47,28 @@ const SecureVerificationModal = ({
description
,
})
=>
{
const
{
t
}
=
useTranslation
();
const
[
isAnimating
,
setIsAnimating
]
=
useState
(
false
);
const
[
verifySuccess
,
setVerifySuccess
]
=
useState
(
false
);
const
{
has2FA
,
hasPasskey
,
passkeySupported
}
=
verificationMethods
;
const
{
method
,
loading
,
code
}
=
verificationState
;
useEffect
(()
=>
{
if
(
visible
)
{
setIsAnimating
(
true
);
setVerifySuccess
(
false
);
}
else
{
setIsAnimating
(
false
);
}
},
[
visible
]);
const
handleKeyDown
=
(
e
)
=>
{
if
(
e
.
key
===
'Enter'
&&
code
.
trim
()
&&
!
loading
&&
method
===
'2fa'
)
{
onVerify
(
method
,
code
);
}
if
(
e
.
key
===
'Escape'
&&
!
loading
)
{
onCancel
();
}
};
// 如果用户没有启用任何验证方式
...
...
@@ -101,165 +115,165 @@ const SecureVerificationModal = ({
return
(
<
Modal
title=
{
<
div
className=
'flex items-center'
>
<
div
className=
'w-8 h-8 rounded-full bg-blue-100 dark:bg-blue-900 flex items-center justify-center mr-3'
>
<
svg
className=
'w-4 h-4 text-blue-600 dark:text-blue-400'
fill=
'currentColor'
viewBox=
'0 0 20 20'
>
<
path
fillRule=
'evenodd'
d=
'M5 9V7a5 5 0 0110 0v2a2 2 0 012 2v5a2 2 0 01-2 2H5a2 2 0 01-2-2v-5a2 2 0 012-2zm8-2v2H7V7a3 3 0 016 0z'
clipRule=
'evenodd'
/>
</
svg
>
</
div
>
{
title
||
t
(
'安全验证'
)
}
</
div
>
}
title=
{
title
||
t
(
'安全验证'
)
}
visible=
{
visible
}
onCancel=
{
onCancel
}
onCancel=
{
loading
?
undefined
:
onCancel
}
closeOnEsc=
{
!
loading
}
footer=
{
null
}
width=
{
600
}
style=
{
{
maxWidth
:
'90vw'
}
}
width=
{
460
}
centered
style=
{
{
maxWidth
:
'calc(100vw - 32px)'
}
}
bodyStyle=
{
{
padding
:
'20px 24px'
}
}
>
<
div
className=
'space-y-6'
>
{
/* 安全提示 */
}
<
div
className=
'bg-blue-50 dark:bg-blue-900 rounded-lg p-4'
>
<
div
className=
'flex items-start'
>
<
svg
className=
'w-5 h-5 text-blue-600 dark:text-blue-400 mt-0.5 mr-3 flex-shrink-0'
fill=
'currentColor'
viewBox=
'0 0 20 20'
>
<
path
fillRule=
'evenodd'
d=
'M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z'
clipRule=
'evenodd'
/>
</
svg
>
<
div
>
<
Typography
.
Text
strong
className=
'text-blue-800 dark:text-blue-200'
>
{
t
(
'安全验证'
)
}
</
Typography
.
Text
>
<
Typography
.
Text
className=
'block text-blue-700 dark:text-blue-300 text-sm mt-1'
>
{
description
||
t
(
'为了保护账户安全,请选择一种方式进行验证。'
)
}
</
Typography
.
Text
>
</
div
>
</
div
>
</
div
>
<
div
style=
{
{
width
:
'100%'
}
}
>
{
/* 描述信息 */
}
{
description
&&
(
<
Typography
.
Paragraph
type=
"tertiary"
style=
{
{
margin
:
'0 0 20px 0'
,
fontSize
:
'14px'
,
lineHeight
:
'1.6'
}
}
>
{
description
}
</
Typography
.
Paragraph
>
)
}
{
/* 验证方式选择 */
}
<
Tabs
activeKey=
{
method
}
onChange=
{
onMethodSwitch
}
type=
'card'
>
<
Tabs
activeKey=
{
method
}
onChange=
{
onMethodSwitch
}
type=
'line'
size=
'default'
style=
{
{
margin
:
0
}
}
>
{
has2FA
&&
(
<
TabPane
tab=
{
<
div
className=
'flex items-center space-x-2'
>
<
svg
className=
'w-4 h-4'
fill=
'currentColor'
viewBox=
'0 0 20 20'
>
<
path
d=
'M10 12a2 2 0 100-4 2 2 0 000 4z'
/>
<
path
fillRule=
'evenodd'
d=
'M.458 10C1.732 5.943 5.522 3 10 3s8.268 2.943 9.542 7c-1.274 4.057-5.064 7-9.542 7S1.732 14.057.458 10zM14 10a4 4 0 11-8 0 4 4 0 018 0z'
clipRule=
'evenodd'
/>
</
svg
>
<
span
>
{
t
(
'两步验证'
)
}
</
span
>
</
div
>
}
tab=
{
t
(
'两步验证'
)
}
itemKey=
'2fa'
>
<
Card
className=
'border-0 shadow-none bg-transparent'
>
<
div
className=
'space-y-4'
>
<
div
>
<
Typography
.
Text
strong
className=
'block mb-2'
>
{
t
(
'验证码'
)
}
</
Typography
.
Text
>
<
Input
placeholder=
{
t
(
'请输入认证器验证码或备用码'
)
}
value=
{
code
}
onChange=
{
onCodeChange
}
size=
'large'
maxLength=
{
8
}
onKeyDown=
{
handleKeyDown
}
autoFocus=
{
method
===
'2fa'
}
/>
<
Typography
.
Text
type=
'tertiary'
size=
'small'
className=
'mt-2 block'
>
{
t
(
'支持6位TOTP验证码或8位备用码'
)
}
</
Typography
.
Text
>
</
div
>
<
div
className=
'flex justify-end space-x-3'
>
<
Button
onClick=
{
onCancel
}
>
{
t
(
'取消'
)
}
</
Button
>
<
Button
type=
'primary'
loading=
{
loading
}
disabled=
{
!
code
.
trim
()
||
loading
}
onClick=
{
()
=>
onVerify
(
method
,
code
)
}
>
{
t
(
'验证'
)
}
</
Button
>
</
div
>
<
div
style=
{
{
paddingTop
:
'20px'
}
}
>
<
div
style=
{
{
marginBottom
:
'12px'
}
}
>
<
Input
placeholder=
{
t
(
'请输入6位验证码或8位备用码'
)
}
value=
{
code
}
onChange=
{
onCodeChange
}
size=
'large'
maxLength=
{
8
}
onKeyDown=
{
handleKeyDown
}
autoFocus=
{
method
===
'2fa'
}
disabled=
{
loading
}
prefix=
{
<
svg
style=
{
{
width
:
16
,
height
:
16
,
marginRight
:
8
,
flexShrink
:
0
}
}
fill=
'currentColor'
viewBox=
'0 0 20 20'
>
<
path
fillRule=
'evenodd'
d=
'M18 8a6 6 0 01-7.743 5.743L10 14l-1 1-1 1H6v2H2v-4l4.257-4.257A6 6 0 1118 8zm-6-4a1 1 0 100 2 2 2 0 012 2 1 1 0 102 0 4 4 0 00-4-4z'
clipRule=
'evenodd'
/>
</
svg
>
}
style=
{
{
width
:
'100%'
}
}
/>
</
div
>
</
Card
>
<
Typography
.
Text
type=
"tertiary"
size=
"small"
style=
{
{
display
:
'block'
,
marginBottom
:
'20px'
,
fontSize
:
'13px'
,
lineHeight
:
'1.5'
}
}
>
{
t
(
'从认证器应用中获取验证码,或使用备用码'
)
}
</
Typography
.
Text
>
<
div
style=
{
{
display
:
'flex'
,
justifyContent
:
'flex-end'
,
gap
:
'8px'
,
flexWrap
:
'wrap'
}
}
>
<
Button
onClick=
{
onCancel
}
disabled=
{
loading
}
>
{
t
(
'取消'
)
}
</
Button
>
<
Button
theme=
'solid'
type=
'primary'
loading=
{
loading
}
disabled=
{
!
code
.
trim
()
||
loading
}
onClick=
{
()
=>
onVerify
(
method
,
code
)
}
>
{
t
(
'验证'
)
}
</
Button
>
</
div
>
</
div
>
</
TabPane
>
)
}
{
hasPasskey
&&
passkeySupported
&&
(
<
TabPane
tab=
{
<
div
className=
'flex items-center space-x-2'
>
<
svg
className=
'w-4 h-4'
fill=
'currentColor'
viewBox=
'0 0 20 20'
>
<
path
fillRule=
'evenodd'
d=
'M18 8a6 6 0 01-7.743 5.743L10 14l-1 1-1 1H6v2H2v-4l4.257-4.257A6 6 0 1118 8zm-6-4a1 1 0 100 2 2 2 0 012 2 1 1 0 102 0 4 4 0 00-4-4z'
clipRule=
'evenodd'
/>
</
svg
>
<
span
>
{
t
(
'Passkey'
)
}
</
span
>
</
div
>
}
tab=
{
t
(
'Passkey'
)
}
itemKey=
'passkey'
>
<
Card
className=
'border-0 shadow-none bg-transparent'
>
<
div
className=
'space-y-4'
>
<
div
className=
'text-center py-4'
>
<
div
className=
'mb-4'
>
<
svg
className=
'w-16 h-16 text-blue-500 mx-auto'
fill=
'currentColor'
viewBox=
'0 0 20 20'
>
<
path
fillRule=
'evenodd'
d=
'M18 8a6 6 0 01-7.743 5.743L10 14l-1 1-1 1H6v2H2v-4l4.257-4.257A6 6 0 1118 8zm-6-4a1 1 0 100 2 2 2 0 012 2 1 1 0 102 0 4 4 0 00-4-4z'
clipRule=
'evenodd'
/>
</
svg
>
</
div
>
<
Typography
.
Text
strong
className=
'block mb-2'
>
{
t
(
'使用 Passkey 验证'
)
}
</
Typography
.
Text
>
<
Typography
.
Text
type=
'tertiary'
className=
'block mb-4'
>
{
t
(
'点击下方按钮,使用您的生物特征或安全密钥进行验证'
)
}
</
Typography
.
Text
>
</
div
>
<
div
className=
'flex justify-end space-x-3'
>
<
Button
onClick=
{
onCancel
}
>
{
t
(
'取消'
)
}
</
Button
>
<
Button
type=
'primary'
loading=
{
loading
}
disabled=
{
loading
}
onClick=
{
()
=>
onVerify
(
method
)
}
>
{
loading
?
t
(
'验证中...'
)
:
t
(
'验证 Passkey'
)
}
</
Button
>
<
div
style=
{
{
paddingTop
:
'20px'
}
}
>
<
div
style=
{
{
textAlign
:
'center'
,
padding
:
'24px 16px'
,
marginBottom
:
'20px'
}
}
>
<
div
style=
{
{
width
:
56
,
height
:
56
,
margin
:
'0 auto 16px'
,
display
:
'flex'
,
alignItems
:
'center'
,
justifyContent
:
'center'
,
borderRadius
:
'50%'
,
background
:
'var(--semi-color-primary-light-default)'
,
}
}
>
<
svg
style=
{
{
width
:
28
,
height
:
28
,
color
:
'var(--semi-color-primary)'
}
}
fill=
'currentColor'
viewBox=
'0 0 20 20'
>
<
path
fillRule=
'evenodd'
d=
'M18 8a6 6 0 01-7.743 5.743L10 14l-1 1-1 1H6v2H2v-4l4.257-4.257A6 6 0 1118 8zm-6-4a1 1 0 100 2 2 2 0 012 2 1 1 0 102 0 4 4 0 00-4-4z'
clipRule=
'evenodd'
/>
</
svg
>
</
div
>
<
Typography
.
Title
heading=
{
5
}
style=
{
{
margin
:
'0 0 8px'
,
fontSize
:
'16px'
}
}
>
{
t
(
'使用 Passkey 验证'
)
}
</
Typography
.
Title
>
<
Typography
.
Text
type=
'tertiary'
style=
{
{
display
:
'block'
,
margin
:
0
,
fontSize
:
'13px'
,
lineHeight
:
'1.5'
}
}
>
{
t
(
'点击验证按钮,使用您的生物特征或安全密钥'
)
}
</
Typography
.
Text
>
</
div
>
<
div
style=
{
{
display
:
'flex'
,
justifyContent
:
'flex-end'
,
gap
:
'8px'
,
flexWrap
:
'wrap'
}
}
>
<
Button
onClick=
{
onCancel
}
disabled=
{
loading
}
>
{
t
(
'取消'
)
}
</
Button
>
<
Button
theme=
'solid'
type=
'primary'
loading=
{
loading
}
disabled=
{
loading
}
onClick=
{
()
=>
onVerify
(
method
)
}
>
{
t
(
'验证 Passkey'
)
}
</
Button
>
</
div
>
</
Card
>
</
div
>
</
TabPane
>
)
}
</
Tabs
>
...
...
web/src/components/settings/SystemSetting.jsx
View file @
8eace6bb
...
...
@@ -1043,7 +1043,7 @@ const SystemSetting = () => {
handleCheckboxChange
(
'passkey.enabled'
,
e
)
}
>
{
t
(
'允许通过 Passkey 登录 &
注册
'
)
}
{
t
(
'允许通过 Passkey 登录 &
认证
'
)
}
</
Form
.
Checkbox
>
</
Col
>
</
Row
>
...
...
web/src/components/table/channels/modals/EditChannelModal.jsx
View file @
8eace6bb
...
...
@@ -206,7 +206,7 @@ const EditChannelModal = (props) => {
isModalVisible
,
verificationMethods
,
verificationState
,
start
Verification
,
with
Verification
,
executeVerification
,
cancelVerification
,
setVerificationCode
,
...
...
@@ -214,12 +214,20 @@ const EditChannelModal = (props) => {
}
=
useSecureVerification
({
onSuccess
:
(
result
)
=>
{
// 验证成功后显示密钥
if
(
result
.
success
&&
result
.
data
?.
key
)
{
console
.
log
(
'Verification success, result:'
,
result
);
if
(
result
&&
result
.
success
&&
result
.
data
?.
key
)
{
showSuccess
(
t
(
'密钥获取成功'
));
setKeyDisplayState
({
showModal
:
true
,
keyData
:
result
.
data
.
key
,
});
}
else
if
(
result
&&
result
.
key
)
{
// 直接返回了 key(没有包装在 data 中)
showSuccess
(
t
(
'密钥获取成功'
));
setKeyDisplayState
({
showModal
:
true
,
keyData
:
result
.
key
,
});
}
},
});
...
...
@@ -604,19 +612,30 @@ const EditChannelModal = (props) => {
}
};
//
显示安全验证模态框并开始验证流程
//
查看渠道密钥(透明验证)
const
handleShow2FAModal
=
async
()
=>
{
try
{
const
apiCall
=
createApiCalls
.
viewChannelKey
(
channelId
);
await
startVerification
(
apiCall
,
{
title
:
t
(
'查看渠道密钥'
),
description
:
t
(
'为了保护账户安全,请验证您的身份。'
),
preferredMethod
:
'passkey'
,
// 优先使用 Passkey
});
// 使用 withVerification 包装,会自动处理需要验证的情况
const
result
=
await
withVerification
(
createApiCalls
.
viewChannelKey
(
channelId
),
{
title
:
t
(
'查看渠道密钥'
),
description
:
t
(
'为了保护账户安全,请验证您的身份。'
),
preferredMethod
:
'passkey'
,
// 优先使用 Passkey
}
);
// 如果直接返回了结果(已验证),显示密钥
if
(
result
&&
result
.
success
&&
result
.
data
?.
key
)
{
showSuccess
(
t
(
'密钥获取成功'
));
setKeyDisplayState
({
showModal
:
true
,
keyData
:
result
.
data
.
key
,
});
}
}
catch
(
error
)
{
console
.
error
(
'Failed to
start verification
:'
,
error
);
showError
(
error
.
message
||
t
(
'
启动验证
失败'
));
console
.
error
(
'Failed to
view channel key
:'
,
error
);
showError
(
error
.
message
||
t
(
'
获取密钥
失败'
));
}
};
...
...
web/src/helpers/secureApiCall.js
0 → 100644
View file @
8eace6bb
/*
Copyright (C) 2025 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
/**
* 安全 API 调用包装器
* 自动处理需要验证的 403 错误,透明地触发验证流程
*/
/**
* 检查错误是否是需要安全验证的错误
* @param {Error} error - 错误对象
* @returns {boolean}
*/
export
function
isVerificationRequiredError
(
error
)
{
if
(
!
error
.
response
)
return
false
;
const
{
status
,
data
}
=
error
.
response
;
// 检查是否是 403 错误且包含验证相关的错误码
if
(
status
===
403
&&
data
)
{
const
verificationCodes
=
[
'VERIFICATION_REQUIRED'
,
'VERIFICATION_EXPIRED'
,
'VERIFICATION_INVALID'
];
return
verificationCodes
.
includes
(
data
.
code
);
}
return
false
;
}
/**
* 从错误中提取验证需求信息
* @param {Error} error - 错误对象
* @returns {Object} 验证需求信息
*/
export
function
extractVerificationInfo
(
error
)
{
const
data
=
error
.
response
?.
data
||
{};
return
{
code
:
data
.
code
,
message
:
data
.
message
||
'需要安全验证'
,
required
:
true
};
}
\ No newline at end of file
web/src/hooks/common/useSecureVerification.jsx
View file @
8eace6bb
...
...
@@ -21,6 +21,7 @@ import { useState, useEffect, useCallback } from 'react';
import
{
useTranslation
}
from
'react-i18next'
;
import
{
SecureVerificationService
}
from
'../../services/secureVerification'
;
import
{
showError
,
showSuccess
}
from
'../../helpers'
;
import
{
isVerificationRequiredError
}
from
'../../helpers/secureApiCall'
;
/**
* 通用安全验证 Hook
...
...
@@ -82,10 +83,10 @@ export const useSecureVerification = ({
// 开始验证流程
const
startVerification
=
useCallback
(
async
(
apiCall
,
options
=
{})
=>
{
const
{
preferredMethod
,
title
,
description
}
=
options
;
// 检查验证方式
const
methods
=
await
checkVerificationMethods
();
if
(
!
methods
.
has2FA
&&
!
methods
.
hasPasskey
)
{
const
errorMessage
=
t
(
'您需要先启用两步验证或 Passkey 才能执行此操作'
);
showError
(
errorMessage
);
...
...
@@ -111,7 +112,7 @@ export const useSecureVerification = ({
description
}));
setIsModalVisible
(
true
);
return
true
;
},
[
checkVerificationMethods
,
onError
,
t
]);
...
...
@@ -125,10 +126,11 @@ export const useSecureVerification = ({
setVerificationState
(
prev
=>
({
...
prev
,
loading
:
true
}));
try
{
const
result
=
await
SecureVerificationService
.
verify
(
method
,
{
code
,
apiCall
:
verificationState
.
apiCall
});
// 先调用验证 API,成功后后端会设置 session
await
SecureVerificationService
.
verify
(
method
,
code
);
// 验证成功,调用业务 API(此时中间件会通过)
const
result
=
await
verificationState
.
apiCall
();
// 显示成功消息
if
(
successMessage
)
{
...
...
@@ -191,12 +193,36 @@ export const useSecureVerification = ({
return
null
;
},
[
verificationMethods
]);
/**
* 包装 API 调用,自动处理验证错误
* 当 API 返回需要验证的错误时,自动弹出验证模态框
* @param {Function} apiCall - API 调用函数
* @param {Object} options - 验证选项(同 startVerification)
* @returns {Promise<any>}
*/
const
withVerification
=
useCallback
(
async
(
apiCall
,
options
=
{})
=>
{
try
{
// 直接尝试调用 API
return
await
apiCall
();
}
catch
(
error
)
{
// 检查是否是需要验证的错误
if
(
isVerificationRequiredError
(
error
))
{
// 自动触发验证流程
await
startVerification
(
apiCall
,
options
);
// 不抛出错误,让验证模态框处理
return
null
;
}
// 其他错误继续抛出
throw
error
;
}
},
[
startVerification
]);
return
{
// 状态
isModalVisible
,
verificationMethods
,
verificationState
,
// 方法
startVerification
,
executeVerification
,
...
...
@@ -205,11 +231,12 @@ export const useSecureVerification = ({
setVerificationCode
,
switchVerificationMethod
,
checkVerificationMethods
,
// 辅助方法
canUseMethod
,
getRecommendedMethod
,
withVerification
,
// 新增:自动处理验证的包装函数
// 便捷属性
hasAnyVerificationMethod
:
verificationMethods
.
has2FA
||
verificationMethods
.
hasPasskey
,
isLoading
:
verificationState
.
loading
,
...
...
web/src/i18n/locales/en.json
View file @
8eace6bb
...
...
@@ -333,6 +333,7 @@
"通过密码注册时需要进行邮箱验证"
:
"Email verification is required when registering via password"
,
"允许通过 GitHub 账户登录 & 注册"
:
"Allow login & registration via GitHub account"
,
"允许通过微信登录 & 注册"
:
"Allow login & registration via WeChat"
,
"允许通过 Passkey 登录 & 认证"
:
"Allow login & authentication via Passkey"
,
"允许新用户注册(此项为否时,新用户将无法以任何方式进行注册"
:
"Allow new user registration (if this option is off, new users will not be able to register in any way"
,
"启用 Turnstile 用户校验"
:
"Enable Turnstile user verification"
,
"配置 SMTP"
:
"Configure SMTP"
,
...
...
web/src/i18n/locales/zh.json
View file @
8eace6bb
...
...
@@ -87,5 +87,6 @@
"此操作将禁用该用户当前的两步验证配置,下次登录将不再强制输入验证码,直到用户重新启用。"
:
"此操作将禁用该用户当前的两步验证配置,下次登录将不再强制输入验证码,直到用户重新启用。"
,
"目标用户:{{username}}"
:
"目标用户:{{username}}"
,
"Passkey 已重置"
:
"Passkey 已重置"
,
"二步验证已重置"
:
"二步验证已重置"
"二步验证已重置"
:
"二步验证已重置"
,
"允许通过 Passkey 登录 & 认证"
:
"允许通过 Passkey 登录 & 认证"
}
web/src/services/secureVerification.js
View file @
8eace6bb
...
...
@@ -18,14 +18,15 @@ For commercial licensing, please contact support@quantumnous.com
*/
import
{
API
,
showError
}
from
'../helpers'
;
import
{
prepareCredentialRequestOptions
,
buildAssertionResult
,
isPasskeySupported
import
{
prepareCredentialRequestOptions
,
buildAssertionResult
,
isPasskeySupported
}
from
'../helpers/passkey'
;
/**
* 通用安全验证服务
* 验证状态完全由后端 Session 控制,前端不存储任何状态
*/
export
class
SecureVerificationService
{
/**
...
...
@@ -81,36 +82,41 @@ export class SecureVerificationService {
/**
* 执行2FA验证
* @param {string} code - 验证码
* @param {Function} apiCall - API调用函数,接收 {method: '2fa', code} 参数
* @returns {Promise<any>} API响应结果
* @returns {Promise<void>}
*/
static
async
verify2FA
(
code
,
apiCall
)
{
static
async
verify2FA
(
code
)
{
if
(
!
code
?.
trim
())
{
throw
new
Error
(
'请输入验证码或备用码'
);
}
return
await
apiCall
({
// 调用通用验证 API,验证成功后后端会设置 session
const
verifyResponse
=
await
API
.
post
(
'/api/verify'
,
{
method
:
'2fa'
,
code
:
code
.
trim
()
});
if
(
!
verifyResponse
.
data
?.
success
)
{
throw
new
Error
(
verifyResponse
.
data
?.
message
||
'验证失败'
);
}
// 验证成功,session 已在后端设置
}
/**
* 执行Passkey验证
* @param {Function} apiCall - API调用函数,接收 {method: 'passkey'} 参数
* @returns {Promise<any>} API响应结果
* @returns {Promise<void>}
*/
static
async
verifyPasskey
(
apiCall
)
{
static
async
verifyPasskey
()
{
try
{
// 开始Passkey验证
const
beginResponse
=
await
API
.
post
(
'/api/user/passkey/verify/begin'
);
if
(
!
beginResponse
.
success
)
{
throw
new
Error
(
beginResponse
.
message
);
if
(
!
beginResponse
.
data
?.
success
)
{
throw
new
Error
(
beginResponse
.
data
?.
message
||
'开始验证失败'
);
}
// 准备WebAuthn选项
const
publicKey
=
prepareCredentialRequestOptions
(
beginResponse
.
data
);
const
publicKey
=
prepareCredentialRequestOptions
(
beginResponse
.
data
.
data
.
options
);
// 执行WebAuthn验证
const
credential
=
await
navigator
.
credentials
.
get
({
publicKey
});
if
(
!
credential
)
{
...
...
@@ -119,17 +125,23 @@ export class SecureVerificationService {
// 构建验证结果
const
assertionResult
=
buildAssertionResult
(
credential
);
// 完成验证
const
finishResponse
=
await
API
.
post
(
'/api/user/passkey/verify/finish'
,
assertionResult
);
if
(
!
finishResponse
.
success
)
{
throw
new
Error
(
finishResponse
.
message
);
if
(
!
finishResponse
.
data
?.
success
)
{
throw
new
Error
(
finishResponse
.
data
?.
message
||
'验证失败'
);
}
// 调用
业务API
return
await
apiCall
(
{
// 调用
通用验证 API 设置 session(Passkey 验证已完成)
const
verifyResponse
=
await
API
.
post
(
'/api/verify'
,
{
method
:
'passkey'
});
if
(
!
verifyResponse
.
data
?.
success
)
{
throw
new
Error
(
verifyResponse
.
data
?.
message
||
'验证失败'
);
}
// 验证成功,session 已在后端设置
}
catch
(
error
)
{
if
(
error
.
name
===
'NotAllowedError'
)
{
throw
new
Error
(
'Passkey 验证被取消或超时'
);
...
...
@@ -144,17 +156,15 @@ export class SecureVerificationService {
/**
* 通用验证方法,根据验证类型执行相应的验证流程
* @param {string} method - 验证方式: '2fa' | 'passkey'
* @param {Object} params - 参数对象
* @param {string} params.code - 2FA验证码(当method为'2fa'时必需)
* @param {Function} params.apiCall - API调用函数
* @returns {Promise<any>} API响应结果
* @param {string} code - 2FA验证码(当method为'2fa'时必需)
* @returns {Promise<void>}
*/
static
async
verify
(
method
,
{
code
,
apiCall
}
)
{
static
async
verify
(
method
,
code
=
''
)
{
switch
(
method
)
{
case
'2fa'
:
return
await
this
.
verify2FA
(
code
,
apiCall
);
return
await
this
.
verify2FA
(
code
);
case
'passkey'
:
return
await
this
.
verifyPasskey
(
apiCall
);
return
await
this
.
verifyPasskey
();
default
:
throw
new
Error
(
`不支持的验证方式:
${
method
}
`
);
}
...
...
@@ -169,8 +179,10 @@ export const createApiCalls = {
* 创建查看渠道密钥的API调用
* @param {number} channelId - 渠道ID
*/
viewChannelKey
:
(
channelId
)
=>
async
(
verificationData
)
=>
{
return
await
API
.
post
(
`/api/channel/
${
channelId
}
/key`
,
verificationData
);
viewChannelKey
:
(
channelId
)
=>
async
()
=>
{
// 新系统中,验证已通过中间件处理,直接调用 API 即可
const
response
=
await
API
.
post
(
`/api/channel/
${
channelId
}
/key`
,
{});
return
response
.
data
;
},
/**
...
...
@@ -179,20 +191,27 @@ export const createApiCalls = {
* @param {string} method - HTTP方法,默认为 'POST'
* @param {Object} extraData - 额外的请求数据
*/
custom
:
(
url
,
method
=
'POST'
,
extraData
=
{})
=>
async
(
verificationData
)
=>
{
const
data
=
{
...
extraData
,
...
verificationData
};
custom
:
(
url
,
method
=
'POST'
,
extraData
=
{})
=>
async
()
=>
{
// 新系统中,验证已通过中间件处理
const
data
=
extraData
;
let
response
;
switch
(
method
.
toUpperCase
())
{
case
'GET'
:
return
await
API
.
get
(
url
,
{
params
:
data
});
response
=
await
API
.
get
(
url
,
{
params
:
data
});
break
;
case
'POST'
:
return
await
API
.
post
(
url
,
data
);
response
=
await
API
.
post
(
url
,
data
);
break
;
case
'PUT'
:
return
await
API
.
put
(
url
,
data
);
response
=
await
API
.
put
(
url
,
data
);
break
;
case
'DELETE'
:
return
await
API
.
delete
(
url
,
{
data
});
response
=
await
API
.
delete
(
url
,
{
data
});
break
;
default
:
throw
new
Error
(
`不支持的HTTP方法:
${
method
}
`
);
}
return
response
.
data
;
}
};
\ No newline at end of file
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment