Skip to content
GitLab
Menu
Projects
Groups
Snippets
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in / Register
Toggle navigation
Menu
Open sidebar
陈曦
sub2api
Commits
ee3f158f
Commit
ee3f158f
authored
Apr 21, 2026
by
IanShaw027
Browse files
fix(settings): restore wechat and payment config persistence
parent
d08757ce
Changes
19
Hide whitespace changes
Inline
Side-by-side
backend/internal/handler/admin/setting_handler.go
View file @
ee3f158f
...
...
@@ -122,6 +122,13 @@ func (h *SettingHandler) GetSettings(c *gin.Context) {
LinuxDoConnectClientID
:
settings
.
LinuxDoConnectClientID
,
LinuxDoConnectClientSecretConfigured
:
settings
.
LinuxDoConnectClientSecretConfigured
,
LinuxDoConnectRedirectURL
:
settings
.
LinuxDoConnectRedirectURL
,
WeChatConnectEnabled
:
settings
.
WeChatConnectEnabled
,
WeChatConnectAppID
:
settings
.
WeChatConnectAppID
,
WeChatConnectAppSecretConfigured
:
settings
.
WeChatConnectAppSecretConfigured
,
WeChatConnectMode
:
settings
.
WeChatConnectMode
,
WeChatConnectScopes
:
settings
.
WeChatConnectScopes
,
WeChatConnectRedirectURL
:
settings
.
WeChatConnectRedirectURL
,
WeChatConnectFrontendRedirectURL
:
settings
.
WeChatConnectFrontendRedirectURL
,
OIDCConnectEnabled
:
settings
.
OIDCConnectEnabled
,
OIDCConnectProviderName
:
settings
.
OIDCConnectProviderName
,
OIDCConnectClientID
:
settings
.
OIDCConnectClientID
,
...
...
@@ -246,6 +253,15 @@ type UpdateSettingsRequest struct {
LinuxDoConnectClientSecret
string
`json:"linuxdo_connect_client_secret"`
LinuxDoConnectRedirectURL
string
`json:"linuxdo_connect_redirect_url"`
// WeChat Connect OAuth 登录
WeChatConnectEnabled
bool
`json:"wechat_connect_enabled"`
WeChatConnectAppID
string
`json:"wechat_connect_app_id"`
WeChatConnectAppSecret
string
`json:"wechat_connect_app_secret"`
WeChatConnectMode
string
`json:"wechat_connect_mode"`
WeChatConnectScopes
string
`json:"wechat_connect_scopes"`
WeChatConnectRedirectURL
string
`json:"wechat_connect_redirect_url"`
WeChatConnectFrontendRedirectURL
string
`json:"wechat_connect_frontend_redirect_url"`
// Generic OIDC OAuth 登录
OIDCConnectEnabled
bool
`json:"oidc_connect_enabled"`
OIDCConnectProviderName
string
`json:"oidc_connect_provider_name"`
...
...
@@ -509,6 +525,54 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
}
}
if
req
.
WeChatConnectEnabled
{
req
.
WeChatConnectAppID
=
strings
.
TrimSpace
(
req
.
WeChatConnectAppID
)
req
.
WeChatConnectAppSecret
=
strings
.
TrimSpace
(
req
.
WeChatConnectAppSecret
)
req
.
WeChatConnectMode
=
strings
.
ToLower
(
strings
.
TrimSpace
(
req
.
WeChatConnectMode
))
req
.
WeChatConnectScopes
=
strings
.
TrimSpace
(
req
.
WeChatConnectScopes
)
req
.
WeChatConnectRedirectURL
=
strings
.
TrimSpace
(
req
.
WeChatConnectRedirectURL
)
req
.
WeChatConnectFrontendRedirectURL
=
strings
.
TrimSpace
(
req
.
WeChatConnectFrontendRedirectURL
)
if
req
.
WeChatConnectAppID
==
""
{
response
.
BadRequest
(
c
,
"WeChat App ID is required when enabled"
)
return
}
if
req
.
WeChatConnectAppSecret
==
""
{
if
previousSettings
.
WeChatConnectAppSecret
==
""
{
response
.
BadRequest
(
c
,
"WeChat App Secret is required when enabled"
)
return
}
req
.
WeChatConnectAppSecret
=
previousSettings
.
WeChatConnectAppSecret
}
if
req
.
WeChatConnectMode
==
""
{
req
.
WeChatConnectMode
=
"open"
}
switch
req
.
WeChatConnectMode
{
case
"open"
,
"mp"
:
default
:
response
.
BadRequest
(
c
,
"WeChat mode must be open or mp"
)
return
}
if
req
.
WeChatConnectScopes
==
""
{
req
.
WeChatConnectScopes
=
service
.
DefaultWeChatConnectScopesForMode
(
req
.
WeChatConnectMode
)
}
if
req
.
WeChatConnectRedirectURL
==
""
{
response
.
BadRequest
(
c
,
"WeChat Redirect URL is required when enabled"
)
return
}
if
err
:=
config
.
ValidateAbsoluteHTTPURL
(
req
.
WeChatConnectRedirectURL
);
err
!=
nil
{
response
.
BadRequest
(
c
,
"WeChat Redirect URL must be an absolute http(s) URL"
)
return
}
if
req
.
WeChatConnectFrontendRedirectURL
==
""
{
req
.
WeChatConnectFrontendRedirectURL
=
"/auth/wechat/callback"
}
if
err
:=
config
.
ValidateFrontendRedirectURL
(
req
.
WeChatConnectFrontendRedirectURL
);
err
!=
nil
{
response
.
BadRequest
(
c
,
"WeChat Frontend Redirect URL is invalid"
)
return
}
}
// Generic OIDC 参数验证
if
req
.
OIDCConnectEnabled
{
req
.
OIDCConnectProviderName
=
strings
.
TrimSpace
(
req
.
OIDCConnectProviderName
)
...
...
@@ -857,6 +921,13 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
LinuxDoConnectClientID
:
req
.
LinuxDoConnectClientID
,
LinuxDoConnectClientSecret
:
req
.
LinuxDoConnectClientSecret
,
LinuxDoConnectRedirectURL
:
req
.
LinuxDoConnectRedirectURL
,
WeChatConnectEnabled
:
req
.
WeChatConnectEnabled
,
WeChatConnectAppID
:
req
.
WeChatConnectAppID
,
WeChatConnectAppSecret
:
req
.
WeChatConnectAppSecret
,
WeChatConnectMode
:
req
.
WeChatConnectMode
,
WeChatConnectScopes
:
req
.
WeChatConnectScopes
,
WeChatConnectRedirectURL
:
req
.
WeChatConnectRedirectURL
,
WeChatConnectFrontendRedirectURL
:
req
.
WeChatConnectFrontendRedirectURL
,
OIDCConnectEnabled
:
req
.
OIDCConnectEnabled
,
OIDCConnectProviderName
:
req
.
OIDCConnectProviderName
,
OIDCConnectClientID
:
req
.
OIDCConnectClientID
,
...
...
@@ -1136,6 +1207,13 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
LinuxDoConnectClientID
:
updatedSettings
.
LinuxDoConnectClientID
,
LinuxDoConnectClientSecretConfigured
:
updatedSettings
.
LinuxDoConnectClientSecretConfigured
,
LinuxDoConnectRedirectURL
:
updatedSettings
.
LinuxDoConnectRedirectURL
,
WeChatConnectEnabled
:
updatedSettings
.
WeChatConnectEnabled
,
WeChatConnectAppID
:
updatedSettings
.
WeChatConnectAppID
,
WeChatConnectAppSecretConfigured
:
updatedSettings
.
WeChatConnectAppSecretConfigured
,
WeChatConnectMode
:
updatedSettings
.
WeChatConnectMode
,
WeChatConnectScopes
:
updatedSettings
.
WeChatConnectScopes
,
WeChatConnectRedirectURL
:
updatedSettings
.
WeChatConnectRedirectURL
,
WeChatConnectFrontendRedirectURL
:
updatedSettings
.
WeChatConnectFrontendRedirectURL
,
OIDCConnectEnabled
:
updatedSettings
.
OIDCConnectEnabled
,
OIDCConnectProviderName
:
updatedSettings
.
OIDCConnectProviderName
,
OIDCConnectClientID
:
updatedSettings
.
OIDCConnectClientID
,
...
...
@@ -1329,6 +1407,27 @@ func diffSettings(before *service.SystemSettings, after *service.SystemSettings,
if
before
.
LinuxDoConnectRedirectURL
!=
after
.
LinuxDoConnectRedirectURL
{
changed
=
append
(
changed
,
"linuxdo_connect_redirect_url"
)
}
if
before
.
WeChatConnectEnabled
!=
after
.
WeChatConnectEnabled
{
changed
=
append
(
changed
,
"wechat_connect_enabled"
)
}
if
before
.
WeChatConnectAppID
!=
after
.
WeChatConnectAppID
{
changed
=
append
(
changed
,
"wechat_connect_app_id"
)
}
if
req
.
WeChatConnectAppSecret
!=
""
{
changed
=
append
(
changed
,
"wechat_connect_app_secret"
)
}
if
before
.
WeChatConnectMode
!=
after
.
WeChatConnectMode
{
changed
=
append
(
changed
,
"wechat_connect_mode"
)
}
if
before
.
WeChatConnectScopes
!=
after
.
WeChatConnectScopes
{
changed
=
append
(
changed
,
"wechat_connect_scopes"
)
}
if
before
.
WeChatConnectRedirectURL
!=
after
.
WeChatConnectRedirectURL
{
changed
=
append
(
changed
,
"wechat_connect_redirect_url"
)
}
if
before
.
WeChatConnectFrontendRedirectURL
!=
after
.
WeChatConnectFrontendRedirectURL
{
changed
=
append
(
changed
,
"wechat_connect_frontend_redirect_url"
)
}
if
before
.
OIDCConnectEnabled
!=
after
.
OIDCConnectEnabled
{
changed
=
append
(
changed
,
"oidc_connect_enabled"
)
}
...
...
backend/internal/handler/auth_wechat_oauth.go
View file @
ee3f158f
...
...
@@ -8,7 +8,6 @@ import (
"io"
"net/http"
"net/url"
"os"
"strconv"
"strings"
"time"
...
...
@@ -149,7 +148,7 @@ func (h *AuthHandler) WeChatOAuthStart(c *gin.Context) {
// WeChatOAuthCallback exchanges the code with WeChat, resolves openid/unionid,
// and stores the result in the unified pending-auth flow.
func
(
h
*
AuthHandler
)
WeChatOAuthCallback
(
c
*
gin
.
Context
)
{
frontendCallback
:=
wechatOAuthFrontendCallback
()
frontendCallback
:=
h
.
wechatOAuthFrontendCallback
(
c
.
Request
.
Context
()
)
if
providerErr
:=
strings
.
TrimSpace
(
c
.
Query
(
"error"
));
providerErr
!=
""
{
redirectOAuthError
(
c
,
frontendCallback
,
"provider_error"
,
providerErr
,
c
.
Query
(
"error_description"
))
...
...
@@ -859,6 +858,10 @@ func (h *AuthHandler) getWeChatOAuthConfig(ctx context.Context, rawMode string,
return
wechatOAuthConfig
{},
err
}
if
h
==
nil
||
h
.
settingSvc
==
nil
{
return
wechatOAuthConfig
{},
infraerrors
.
ServiceUnavailable
(
"CONFIG_NOT_READY"
,
"wechat oauth settings service not ready"
)
}
apiBaseURL
:=
""
if
h
!=
nil
&&
h
.
settingSvc
!=
nil
{
settings
,
err
:=
h
.
settingSvc
.
GetAllSettings
(
ctx
)
...
...
@@ -867,27 +870,28 @@ func (h *AuthHandler) getWeChatOAuthConfig(ctx context.Context, rawMode string,
}
}
effective
,
err
:=
h
.
settingSvc
.
GetWeChatConnectOAuthConfig
(
ctx
)
if
err
!=
nil
{
return
wechatOAuthConfig
{},
err
}
if
effective
.
Mode
!=
mode
{
return
wechatOAuthConfig
{},
infraerrors
.
NotFound
(
"OAUTH_DISABLED"
,
"wechat oauth is disabled"
)
}
cfg
:=
wechatOAuthConfig
{
mode
:
mode
,
redirectURI
:
resolveWeChatOAuthAbsoluteURL
(
apiBaseURL
,
c
,
"/api/v1/auth/oauth/wechat/callback"
),
frontendCallback
:
wechatOAuthFrontendCallback
(),
appID
:
strings
.
TrimSpace
(
effective
.
AppID
),
appSecret
:
strings
.
TrimSpace
(
effective
.
AppSecret
),
redirectURI
:
firstNonEmpty
(
strings
.
TrimSpace
(
effective
.
RedirectURL
),
resolveWeChatOAuthAbsoluteURL
(
apiBaseURL
,
c
,
"/api/v1/auth/oauth/wechat/callback"
)),
frontendCallback
:
firstNonEmpty
(
strings
.
TrimSpace
(
effective
.
FrontendRedirectURL
),
wechatOAuthDefaultFrontendCB
),
scope
:
firstNonEmpty
(
strings
.
TrimSpace
(
effective
.
Scopes
),
service
.
DefaultWeChatConnectScopesForMode
(
mode
)),
}
switch
mode
{
case
"mp"
:
cfg
.
appID
=
strings
.
TrimSpace
(
os
.
Getenv
(
"WECHAT_OAUTH_MP_APP_ID"
))
cfg
.
appSecret
=
strings
.
TrimSpace
(
os
.
Getenv
(
"WECHAT_OAUTH_MP_APP_SECRET"
))
cfg
.
authorizeURL
=
"https://open.weixin.qq.com/connect/oauth2/authorize"
cfg
.
scope
=
"snsapi_userinfo"
default
:
cfg
.
appID
=
strings
.
TrimSpace
(
os
.
Getenv
(
"WECHAT_OAUTH_OPEN_APP_ID"
))
cfg
.
appSecret
=
strings
.
TrimSpace
(
os
.
Getenv
(
"WECHAT_OAUTH_OPEN_APP_SECRET"
))
cfg
.
authorizeURL
=
"https://open.weixin.qq.com/connect/qrconnect"
cfg
.
scope
=
"snsapi_login"
}
if
cfg
.
appID
==
""
||
cfg
.
appSecret
==
""
{
return
wechatOAuthConfig
{},
infraerrors
.
NotFound
(
"OAUTH_DISABLED"
,
"wechat oauth is disabled"
)
}
if
strings
.
TrimSpace
(
cfg
.
redirectURI
)
==
""
{
return
wechatOAuthConfig
{},
infraerrors
.
InternalServer
(
"OAUTH_CONFIG_INVALID"
,
"wechat oauth redirect url not configured"
)
...
...
@@ -896,8 +900,14 @@ func (h *AuthHandler) getWeChatOAuthConfig(ctx context.Context, rawMode string,
return
cfg
,
nil
}
func
wechatOAuthFrontendCallback
()
string
{
return
firstNonEmpty
(
strings
.
TrimSpace
(
os
.
Getenv
(
"WECHAT_OAUTH_FRONTEND_REDIRECT_URL"
)),
wechatOAuthDefaultFrontendCB
)
func
(
h
*
AuthHandler
)
wechatOAuthFrontendCallback
(
ctx
context
.
Context
)
string
{
if
h
!=
nil
&&
h
.
settingSvc
!=
nil
{
cfg
,
err
:=
h
.
settingSvc
.
GetWeChatConnectOAuthConfig
(
ctx
)
if
err
==
nil
&&
strings
.
TrimSpace
(
cfg
.
FrontendRedirectURL
)
!=
""
{
return
strings
.
TrimSpace
(
cfg
.
FrontendRedirectURL
)
}
}
return
wechatOAuthDefaultFrontendCB
}
func
resolveWeChatOAuthMode
(
rawMode
string
,
c
*
gin
.
Context
)
(
string
,
error
)
{
...
...
backend/internal/handler/auth_wechat_oauth_test.go
View file @
ee3f158f
...
...
@@ -33,16 +33,22 @@ import (
)
func
TestWeChatOAuthStartRedirectsAndSetsPendingCookies
(
t
*
testing
.
T
)
{
t
.
Setenv
(
"WECHAT_OAUTH_OPEN_APP_ID"
,
"wx-open-app"
)
t
.
Setenv
(
"WECHAT_OAUTH_OPEN_APP_SECRET"
,
"wx-open-secret"
)
gin
.
SetMode
(
gin
.
TestMode
)
handler
,
client
:=
newWeChatOAuthTestHandlerWithSettings
(
t
,
false
,
map
[
string
]
string
{
service
.
SettingKeyWeChatConnectEnabled
:
"true"
,
service
.
SettingKeyWeChatConnectAppID
:
"wx-open-app"
,
service
.
SettingKeyWeChatConnectAppSecret
:
"wx-open-secret"
,
service
.
SettingKeyWeChatConnectMode
:
"open"
,
service
.
SettingKeyWeChatConnectScopes
:
"snsapi_login"
,
service
.
SettingKeyWeChatConnectRedirectURL
:
"https://api.example.com/api/v1/auth/oauth/wechat/callback"
,
service
.
SettingKeyWeChatConnectFrontendRedirectURL
:
"/auth/wechat/callback"
,
})
defer
client
.
Close
()
recorder
:=
httptest
.
NewRecorder
()
c
,
_
:=
gin
.
CreateTestContext
(
recorder
)
c
.
Request
=
httptest
.
NewRequest
(
http
.
MethodGet
,
"/api/v1/auth/oauth/wechat/start?mode=open&redirect=/billing"
,
nil
)
c
.
Request
.
Host
=
"api.example.com"
handler
:=
&
AuthHandler
{}
handler
.
WeChatOAuthStart
(
c
)
require
.
Equal
(
t
,
http
.
StatusFound
,
recorder
.
Code
)
...
...
@@ -60,10 +66,6 @@ func TestWeChatOAuthStartRedirectsAndSetsPendingCookies(t *testing.T) {
}
func
TestWeChatOAuthCallbackCreatesPendingSessionForUnifiedFlow
(
t
*
testing
.
T
)
{
t
.
Setenv
(
"WECHAT_OAUTH_OPEN_APP_ID"
,
"wx-open-app"
)
t
.
Setenv
(
"WECHAT_OAUTH_OPEN_APP_SECRET"
,
"wx-open-secret"
)
t
.
Setenv
(
"WECHAT_OAUTH_FRONTEND_REDIRECT_URL"
,
"/auth/wechat/callback"
)
originalAccessTokenURL
:=
wechatOAuthAccessTokenURL
originalUserInfoURL
:=
wechatOAuthUserInfoURL
t
.
Cleanup
(
func
()
{
...
...
@@ -124,10 +126,6 @@ func TestWeChatOAuthCallbackCreatesPendingSessionForUnifiedFlow(t *testing.T) {
}
func
TestWeChatOAuthCallbackRejectsMissingUnionID
(
t
*
testing
.
T
)
{
t
.
Setenv
(
"WECHAT_OAUTH_OPEN_APP_ID"
,
"wx-open-app"
)
t
.
Setenv
(
"WECHAT_OAUTH_OPEN_APP_SECRET"
,
"wx-open-secret"
)
t
.
Setenv
(
"WECHAT_OAUTH_FRONTEND_REDIRECT_URL"
,
"https://app.example.com/auth/wechat/callback"
)
originalAccessTokenURL
:=
wechatOAuthAccessTokenURL
originalUserInfoURL
:=
wechatOAuthUserInfoURL
t
.
Cleanup
(
func
()
{
...
...
@@ -151,7 +149,7 @@ func TestWeChatOAuthCallbackRejectsMissingUnionID(t *testing.T) {
wechatOAuthAccessTokenURL
=
upstream
.
URL
+
"/sns/oauth2/access_token"
wechatOAuthUserInfoURL
=
upstream
.
URL
+
"/sns/userinfo"
handler
,
client
:=
newWeChatOAuthTestHandler
(
t
,
false
)
handler
,
client
:=
newWeChatOAuthTestHandler
WithSettings
(
t
,
false
,
wechatOAuthTestSettings
(
"open"
,
"wx-open-app"
,
"wx-open-secret"
,
"https://app.example.com/auth/wechat/callback"
)
)
defer
client
.
Close
()
recorder
:=
httptest
.
NewRecorder
()
...
...
@@ -177,9 +175,6 @@ func TestWeChatOAuthCallbackRejectsMissingUnionID(t *testing.T) {
}
func
TestWeChatPaymentOAuthCallbackRedirectsWithOpaqueResumeToken
(
t
*
testing
.
T
)
{
t
.
Setenv
(
"WECHAT_OAUTH_MP_APP_ID"
,
"wx-mp-app"
)
t
.
Setenv
(
"WECHAT_OAUTH_MP_APP_SECRET"
,
"wx-mp-secret"
)
originalAccessTokenURL
:=
wechatOAuthAccessTokenURL
t
.
Cleanup
(
func
()
{
wechatOAuthAccessTokenURL
=
originalAccessTokenURL
...
...
@@ -196,7 +191,7 @@ func TestWeChatPaymentOAuthCallbackRedirectsWithOpaqueResumeToken(t *testing.T)
defer
upstream
.
Close
()
wechatOAuthAccessTokenURL
=
upstream
.
URL
+
"/sns/oauth2/access_token"
handler
,
client
:=
newWeChatOAuthTestHandler
(
t
,
false
)
handler
,
client
:=
newWeChatOAuthTestHandler
WithSettings
(
t
,
false
,
wechatOAuthTestSettings
(
"mp"
,
"wx-mp-app"
,
"wx-mp-secret"
,
"/auth/wechat/callback"
)
)
defer
client
.
Close
()
handler
.
cfg
.
Totp
.
EncryptionKey
=
"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
...
...
@@ -240,7 +235,6 @@ func TestWeChatOAuthCallbackBindUsesUnionCanonicalIdentityAcrossChannels(t *test
testCases
:=
[]
struct
{
name
string
mode
string
appIDEnv
string
appID
string
appSecret
string
openID
string
...
...
@@ -248,7 +242,6 @@ func TestWeChatOAuthCallbackBindUsesUnionCanonicalIdentityAcrossChannels(t *test
{
name
:
"open"
,
mode
:
"open"
,
appIDEnv
:
"WECHAT_OAUTH_OPEN_APP_ID"
,
appID
:
"wx-open-app"
,
appSecret
:
"wx-open-secret"
,
openID
:
"openid-open-123"
,
...
...
@@ -256,7 +249,6 @@ func TestWeChatOAuthCallbackBindUsesUnionCanonicalIdentityAcrossChannels(t *test
{
name
:
"mp"
,
mode
:
"mp"
,
appIDEnv
:
"WECHAT_OAUTH_MP_APP_ID"
,
appID
:
"wx-mp-app"
,
appSecret
:
"wx-mp-secret"
,
openID
:
"openid-mp-123"
,
...
...
@@ -265,15 +257,6 @@ func TestWeChatOAuthCallbackBindUsesUnionCanonicalIdentityAcrossChannels(t *test
for
_
,
tc
:=
range
testCases
{
t
.
Run
(
tc
.
name
,
func
(
t
*
testing
.
T
)
{
t
.
Setenv
(
tc
.
appIDEnv
,
tc
.
appID
)
switch
tc
.
mode
{
case
"open"
:
t
.
Setenv
(
"WECHAT_OAUTH_OPEN_APP_SECRET"
,
tc
.
appSecret
)
case
"mp"
:
t
.
Setenv
(
"WECHAT_OAUTH_MP_APP_SECRET"
,
tc
.
appSecret
)
}
t
.
Setenv
(
"WECHAT_OAUTH_FRONTEND_REDIRECT_URL"
,
"/auth/wechat/callback"
)
originalAccessTokenURL
:=
wechatOAuthAccessTokenURL
originalUserInfoURL
:=
wechatOAuthUserInfoURL
t
.
Cleanup
(
func
()
{
...
...
@@ -297,7 +280,7 @@ func TestWeChatOAuthCallbackBindUsesUnionCanonicalIdentityAcrossChannels(t *test
wechatOAuthAccessTokenURL
=
upstream
.
URL
+
"/sns/oauth2/access_token"
wechatOAuthUserInfoURL
=
upstream
.
URL
+
"/sns/userinfo"
handler
,
client
:=
newWeChatOAuthTestHandler
(
t
,
false
)
handler
,
client
:=
newWeChatOAuthTestHandler
WithSettings
(
t
,
false
,
wechatOAuthTestSettings
(
tc
.
mode
,
tc
.
appID
,
tc
.
appSecret
,
"/auth/wechat/callback"
)
)
defer
client
.
Close
()
currentUser
,
err
:=
client
.
User
.
Create
()
.
...
...
@@ -354,10 +337,6 @@ func TestWeChatOAuthCallbackBindUsesUnionCanonicalIdentityAcrossChannels(t *test
}
func
TestWeChatOAuthCallbackBindRejectsCanonicalOwnershipConflict
(
t
*
testing
.
T
)
{
t
.
Setenv
(
"WECHAT_OAUTH_OPEN_APP_ID"
,
"wx-open-app"
)
t
.
Setenv
(
"WECHAT_OAUTH_OPEN_APP_SECRET"
,
"wx-open-secret"
)
t
.
Setenv
(
"WECHAT_OAUTH_FRONTEND_REDIRECT_URL"
,
"/auth/wechat/callback"
)
originalAccessTokenURL
:=
wechatOAuthAccessTokenURL
originalUserInfoURL
:=
wechatOAuthUserInfoURL
t
.
Cleanup
(
func
()
{
...
...
@@ -436,10 +415,6 @@ func TestWeChatOAuthCallbackBindRejectsCanonicalOwnershipConflict(t *testing.T)
}
func
TestWeChatOAuthCallbackBindRejectsChannelOwnershipConflict
(
t
*
testing
.
T
)
{
t
.
Setenv
(
"WECHAT_OAUTH_OPEN_APP_ID"
,
"wx-open-app"
)
t
.
Setenv
(
"WECHAT_OAUTH_OPEN_APP_SECRET"
,
"wx-open-secret"
)
t
.
Setenv
(
"WECHAT_OAUTH_FRONTEND_REDIRECT_URL"
,
"/auth/wechat/callback"
)
originalAccessTokenURL
:=
wechatOAuthAccessTokenURL
originalUserInfoURL
:=
wechatOAuthUserInfoURL
t
.
Cleanup
(
func
()
{
...
...
@@ -529,10 +504,6 @@ func TestWeChatOAuthCallbackBindRejectsChannelOwnershipConflict(t *testing.T) {
}
func
TestWeChatOAuthCallbackBindRejectsLegacyProviderKeyOwnershipConflict
(
t
*
testing
.
T
)
{
t
.
Setenv
(
"WECHAT_OAUTH_OPEN_APP_ID"
,
"wx-open-app"
)
t
.
Setenv
(
"WECHAT_OAUTH_OPEN_APP_SECRET"
,
"wx-open-secret"
)
t
.
Setenv
(
"WECHAT_OAUTH_FRONTEND_REDIRECT_URL"
,
"/auth/wechat/callback"
)
originalAccessTokenURL
:=
wechatOAuthAccessTokenURL
originalUserInfoURL
:=
wechatOAuthUserInfoURL
t
.
Cleanup
(
func
()
{
...
...
@@ -611,10 +582,6 @@ func TestWeChatOAuthCallbackBindRejectsLegacyProviderKeyOwnershipConflict(t *tes
}
func
TestCompleteWeChatOAuthRegistrationAfterInvitationPendingSession
(
t
*
testing
.
T
)
{
t
.
Setenv
(
"WECHAT_OAUTH_OPEN_APP_ID"
,
"wx-open-app"
)
t
.
Setenv
(
"WECHAT_OAUTH_OPEN_APP_SECRET"
,
"wx-open-secret"
)
t
.
Setenv
(
"WECHAT_OAUTH_FRONTEND_REDIRECT_URL"
,
"/auth/wechat/callback"
)
originalAccessTokenURL
:=
wechatOAuthAccessTokenURL
originalUserInfoURL
:=
wechatOAuthUserInfoURL
t
.
Cleanup
(
func
()
{
...
...
@@ -737,10 +704,6 @@ func TestCompleteWeChatOAuthRegistrationAfterInvitationPendingSession(t *testing
}
func
TestWeChatOAuthCallbackRepairsLegacyOpenIDOnlyIdentity
(
t
*
testing
.
T
)
{
t
.
Setenv
(
"WECHAT_OAUTH_OPEN_APP_ID"
,
"wx-open-app"
)
t
.
Setenv
(
"WECHAT_OAUTH_OPEN_APP_SECRET"
,
"wx-open-secret"
)
t
.
Setenv
(
"WECHAT_OAUTH_FRONTEND_REDIRECT_URL"
,
"/auth/wechat/callback"
)
originalAccessTokenURL
:=
wechatOAuthAccessTokenURL
originalUserInfoURL
:=
wechatOAuthUserInfoURL
t
.
Cleanup
(
func
()
{
...
...
@@ -900,10 +863,6 @@ func TestCompleteWeChatOAuthRegistrationRejectsAdoptExistingUserSession(t *testi
}
func
TestWeChatOAuthCallbackRepairsLegacyProviderKeyCanonicalIdentity
(
t
*
testing
.
T
)
{
t
.
Setenv
(
"WECHAT_OAUTH_OPEN_APP_ID"
,
"wx-open-app"
)
t
.
Setenv
(
"WECHAT_OAUTH_OPEN_APP_SECRET"
,
"wx-open-secret"
)
t
.
Setenv
(
"WECHAT_OAUTH_FRONTEND_REDIRECT_URL"
,
"/auth/wechat/callback"
)
originalAccessTokenURL
:=
wechatOAuthAccessTokenURL
originalUserInfoURL
:=
wechatOAuthUserInfoURL
t
.
Cleanup
(
func
()
{
...
...
@@ -1010,6 +969,22 @@ func TestWeChatOAuthCallbackRepairsLegacyProviderKeyCanonicalIdentity(t *testing
}
func
newWeChatOAuthTestHandler
(
t
*
testing
.
T
,
invitationEnabled
bool
)
(
*
AuthHandler
,
*
dbent
.
Client
)
{
return
newWeChatOAuthTestHandlerWithSettings
(
t
,
invitationEnabled
,
nil
)
}
func
wechatOAuthTestSettings
(
mode
,
appID
,
secret
,
frontendRedirect
string
)
map
[
string
]
string
{
return
map
[
string
]
string
{
service
.
SettingKeyWeChatConnectEnabled
:
"true"
,
service
.
SettingKeyWeChatConnectAppID
:
appID
,
service
.
SettingKeyWeChatConnectAppSecret
:
secret
,
service
.
SettingKeyWeChatConnectMode
:
mode
,
service
.
SettingKeyWeChatConnectScopes
:
service
.
DefaultWeChatConnectScopesForMode
(
mode
),
service
.
SettingKeyWeChatConnectRedirectURL
:
"https://api.example.com/api/v1/auth/oauth/wechat/callback"
,
service
.
SettingKeyWeChatConnectFrontendRedirectURL
:
frontendRedirect
,
}
}
func
newWeChatOAuthTestHandlerWithSettings
(
t
*
testing
.
T
,
invitationEnabled
bool
,
extraSettings
map
[
string
]
string
)
(
*
AuthHandler
,
*
dbent
.
Client
)
{
t
.
Helper
()
db
,
err
:=
sql
.
Open
(
"sqlite"
,
"file:auth_wechat_oauth?mode=memory&cache=shared"
)
...
...
@@ -1036,12 +1011,17 @@ func newWeChatOAuthTestHandler(t *testing.T, invitationEnabled bool) (*AuthHandl
UserConcurrency
:
1
,
},
}
settingSvc
:=
service
.
NewSettingService
(
&
wechatOAuthSettingRepoStub
{
values
:
map
[
string
]
string
{
service
.
SettingKeyRegistrationEnabled
:
"true"
,
service
.
SettingKeyInvitationCodeEnabled
:
boolSettingValue
(
invitationEnabled
),
},
},
cfg
)
values
:=
map
[
string
]
string
{
service
.
SettingKeyRegistrationEnabled
:
"true"
,
service
.
SettingKeyInvitationCodeEnabled
:
boolSettingValue
(
invitationEnabled
),
}
for
key
,
value
:=
range
wechatOAuthTestSettings
(
"open"
,
"wx-open-app"
,
"wx-open-secret"
,
"/auth/wechat/callback"
)
{
values
[
key
]
=
value
}
for
key
,
value
:=
range
extraSettings
{
values
[
key
]
=
value
}
settingSvc
:=
service
.
NewSettingService
(
&
wechatOAuthSettingRepoStub
{
values
:
values
},
cfg
)
authSvc
:=
service
.
NewAuthService
(
client
,
...
...
backend/internal/handler/dto/settings.go
View file @
ee3f158f
...
...
@@ -51,6 +51,14 @@ type SystemSettings struct {
LinuxDoConnectClientSecretConfigured
bool
`json:"linuxdo_connect_client_secret_configured"`
LinuxDoConnectRedirectURL
string
`json:"linuxdo_connect_redirect_url"`
WeChatConnectEnabled
bool
`json:"wechat_connect_enabled"`
WeChatConnectAppID
string
`json:"wechat_connect_app_id"`
WeChatConnectAppSecretConfigured
bool
`json:"wechat_connect_app_secret_configured"`
WeChatConnectMode
string
`json:"wechat_connect_mode"`
WeChatConnectScopes
string
`json:"wechat_connect_scopes"`
WeChatConnectRedirectURL
string
`json:"wechat_connect_redirect_url"`
WeChatConnectFrontendRedirectURL
string
`json:"wechat_connect_frontend_redirect_url"`
OIDCConnectEnabled
bool
`json:"oidc_connect_enabled"`
OIDCConnectProviderName
string
`json:"oidc_connect_provider_name"`
OIDCConnectClientID
string
`json:"oidc_connect_client_id"`
...
...
backend/internal/handler/setting_handler_public_test.go
View file @
ee3f158f
...
...
@@ -84,12 +84,17 @@ func TestSettingHandler_GetPublicSettings_ExposesForceEmailOnThirdPartySignup(t
func
TestSettingHandler_GetPublicSettings_ExposesWeChatOAuthModeCapabilities
(
t
*
testing
.
T
)
{
gin
.
SetMode
(
gin
.
TestMode
)
t
.
Setenv
(
"WECHAT_OAUTH_OPEN_APP_ID"
,
"wx-open-app"
)
t
.
Setenv
(
"WECHAT_OAUTH_OPEN_APP_SECRET"
,
"wx-open-secret"
)
t
.
Setenv
(
"WECHAT_OAUTH_MP_APP_ID"
,
""
)
t
.
Setenv
(
"WECHAT_OAUTH_MP_APP_SECRET"
,
""
)
h
:=
NewSettingHandler
(
service
.
NewSettingService
(
&
settingHandlerPublicRepoStub
{},
&
config
.
Config
{}),
"test-version"
)
h
:=
NewSettingHandler
(
service
.
NewSettingService
(
&
settingHandlerPublicRepoStub
{
values
:
map
[
string
]
string
{
service
.
SettingKeyWeChatConnectEnabled
:
"true"
,
service
.
SettingKeyWeChatConnectAppID
:
"wx-mp-app"
,
service
.
SettingKeyWeChatConnectAppSecret
:
"wx-mp-secret"
,
service
.
SettingKeyWeChatConnectMode
:
"mp"
,
service
.
SettingKeyWeChatConnectScopes
:
"snsapi_base"
,
service
.
SettingKeyWeChatConnectRedirectURL
:
"https://api.example.com/api/v1/auth/oauth/wechat/callback"
,
service
.
SettingKeyWeChatConnectFrontendRedirectURL
:
"/auth/wechat/callback"
,
},
},
&
config
.
Config
{}),
"test-version"
)
recorder
:=
httptest
.
NewRecorder
()
c
,
_
:=
gin
.
CreateTestContext
(
recorder
)
...
...
@@ -110,6 +115,6 @@ func TestSettingHandler_GetPublicSettings_ExposesWeChatOAuthModeCapabilities(t *
require
.
NoError
(
t
,
json
.
Unmarshal
(
recorder
.
Body
.
Bytes
(),
&
resp
))
require
.
Equal
(
t
,
0
,
resp
.
Code
)
require
.
True
(
t
,
resp
.
Data
.
WeChatOAuthEnabled
)
require
.
Tru
e
(
t
,
resp
.
Data
.
WeChatOAuthOpenEnabled
)
require
.
Fals
e
(
t
,
resp
.
Data
.
WeChatOAuthMPEnabled
)
require
.
Fals
e
(
t
,
resp
.
Data
.
WeChatOAuthOpenEnabled
)
require
.
Tru
e
(
t
,
resp
.
Data
.
WeChatOAuthMPEnabled
)
}
backend/internal/service/domain_constants.go
View file @
ee3f158f
...
...
@@ -111,6 +111,15 @@ const (
SettingKeyLinuxDoConnectClientSecret
=
"linuxdo_connect_client_secret"
SettingKeyLinuxDoConnectRedirectURL
=
"linuxdo_connect_redirect_url"
// WeChat Connect OAuth 登录设置
SettingKeyWeChatConnectEnabled
=
"wechat_connect_enabled"
SettingKeyWeChatConnectAppID
=
"wechat_connect_app_id"
SettingKeyWeChatConnectAppSecret
=
"wechat_connect_app_secret"
SettingKeyWeChatConnectMode
=
"wechat_connect_mode"
SettingKeyWeChatConnectScopes
=
"wechat_connect_scopes"
SettingKeyWeChatConnectRedirectURL
=
"wechat_connect_redirect_url"
SettingKeyWeChatConnectFrontendRedirectURL
=
"wechat_connect_frontend_redirect_url"
// Generic OIDC OAuth 登录设置
SettingKeyOIDCConnectEnabled
=
"oidc_connect_enabled"
SettingKeyOIDCConnectProviderName
=
"oidc_connect_provider_name"
...
...
backend/internal/service/payment_config_service.go
View file @
ee3f158f
...
...
@@ -93,6 +93,11 @@ type UpdatePaymentConfigRequest struct {
CancelRateLimitWindow
*
int
`json:"cancel_rate_limit_window"`
CancelRateLimitUnit
*
string
`json:"cancel_rate_limit_unit"`
CancelRateLimitMode
*
string
`json:"cancel_rate_limit_window_mode"`
VisibleMethodAlipaySource
*
string
`json:"payment_visible_method_alipay_source"`
VisibleMethodWxpaySource
*
string
`json:"payment_visible_method_wxpay_source"`
VisibleMethodAlipayEnabled
*
bool
`json:"payment_visible_method_alipay_enabled"`
VisibleMethodWxpayEnabled
*
bool
`json:"payment_visible_method_wxpay_enabled"`
}
// MethodLimits holds per-payment-type limits.
...
...
@@ -319,6 +324,10 @@ func (s *PaymentConfigService) UpdatePaymentConfig(ctx context.Context, req Upda
SettingCancelWindowSize
:
formatPositiveInt
(
req
.
CancelRateLimitWindow
),
SettingCancelWindowUnit
:
derefStr
(
req
.
CancelRateLimitUnit
),
SettingCancelWindowMode
:
derefStr
(
req
.
CancelRateLimitMode
),
SettingPaymentVisibleMethodAlipaySource
:
derefStr
(
req
.
VisibleMethodAlipaySource
),
SettingPaymentVisibleMethodWxpaySource
:
derefStr
(
req
.
VisibleMethodWxpaySource
),
SettingPaymentVisibleMethodAlipayEnabled
:
formatBoolOrEmpty
(
req
.
VisibleMethodAlipayEnabled
),
SettingPaymentVisibleMethodWxpayEnabled
:
formatBoolOrEmpty
(
req
.
VisibleMethodWxpayEnabled
),
}
if
req
.
EnabledTypes
!=
nil
{
m
[
SettingEnabledPaymentTypes
]
=
strings
.
Join
(
req
.
EnabledTypes
,
","
)
...
...
backend/internal/service/payment_config_service_test.go
View file @
ee3f158f
...
...
@@ -366,7 +366,8 @@ func newPaymentConfigServiceTestClient(t *testing.T) *dbent.Client {
}
type
paymentConfigSettingRepoStub
struct
{
values
map
[
string
]
string
values
map
[
string
]
string
updates
map
[
string
]
string
}
func
(
s
*
paymentConfigSettingRepoStub
)
Get
(
context
.
Context
,
string
)
(
*
Setting
,
error
)
{
...
...
@@ -383,10 +384,52 @@ func (s *paymentConfigSettingRepoStub) GetMultiple(_ context.Context, keys []str
}
return
out
,
nil
}
func
(
s
*
paymentConfigSettingRepoStub
)
SetMultiple
(
context
.
Context
,
map
[
string
]
string
)
error
{
func
(
s
*
paymentConfigSettingRepoStub
)
SetMultiple
(
_
context
.
Context
,
values
map
[
string
]
string
)
error
{
s
.
updates
=
make
(
map
[
string
]
string
,
len
(
values
))
for
key
,
value
:=
range
values
{
s
.
updates
[
key
]
=
value
if
s
.
values
==
nil
{
s
.
values
=
map
[
string
]
string
{}
}
s
.
values
[
key
]
=
value
}
return
nil
}
func
(
s
*
paymentConfigSettingRepoStub
)
GetAll
(
context
.
Context
)
(
map
[
string
]
string
,
error
)
{
return
s
.
values
,
nil
}
func
(
s
*
paymentConfigSettingRepoStub
)
Delete
(
context
.
Context
,
string
)
error
{
return
nil
}
func
TestUpdatePaymentConfig_PersistsVisibleMethodRouting
(
t
*
testing
.
T
)
{
repo
:=
&
paymentConfigSettingRepoStub
{
values
:
map
[
string
]
string
{}}
svc
:=
&
PaymentConfigService
{
settingRepo
:
repo
}
alipayEnabled
:=
true
wxpayEnabled
:=
false
err
:=
svc
.
UpdatePaymentConfig
(
context
.
Background
(),
UpdatePaymentConfigRequest
{
VisibleMethodAlipayEnabled
:
&
alipayEnabled
,
VisibleMethodAlipaySource
:
paymentConfigStrPtr
(
VisibleMethodSourceEasyPayAlipay
),
VisibleMethodWxpayEnabled
:
&
wxpayEnabled
,
VisibleMethodWxpaySource
:
paymentConfigStrPtr
(
VisibleMethodSourceOfficialWechat
),
})
if
err
!=
nil
{
t
.
Fatalf
(
"UpdatePaymentConfig returned error: %v"
,
err
)
}
if
repo
.
values
[
SettingPaymentVisibleMethodAlipayEnabled
]
!=
"true"
{
t
.
Fatalf
(
"alipay enabled = %q, want true"
,
repo
.
values
[
SettingPaymentVisibleMethodAlipayEnabled
])
}
if
repo
.
values
[
SettingPaymentVisibleMethodAlipaySource
]
!=
VisibleMethodSourceEasyPayAlipay
{
t
.
Fatalf
(
"alipay source = %q, want %q"
,
repo
.
values
[
SettingPaymentVisibleMethodAlipaySource
],
VisibleMethodSourceEasyPayAlipay
)
}
if
repo
.
values
[
SettingPaymentVisibleMethodWxpayEnabled
]
!=
"false"
{
t
.
Fatalf
(
"wxpay enabled = %q, want false"
,
repo
.
values
[
SettingPaymentVisibleMethodWxpayEnabled
])
}
if
repo
.
values
[
SettingPaymentVisibleMethodWxpaySource
]
!=
VisibleMethodSourceOfficialWechat
{
t
.
Fatalf
(
"wxpay source = %q, want %q"
,
repo
.
values
[
SettingPaymentVisibleMethodWxpaySource
],
VisibleMethodSourceOfficialWechat
)
}
}
func
paymentConfigStrPtr
(
value
string
)
*
string
{
return
&
value
}
backend/internal/service/payment_order.go
View file @
ee3f158f
...
...
@@ -6,7 +6,6 @@ import (
"log/slog"
"math"
"net/url"
"os"
"strconv"
"strings"
"time"
...
...
@@ -512,16 +511,21 @@ func requiresWeChatJSAPICompatibleSelection(req CreateOrderRequest, sel *payment
return
req
.
IsWeChatBrowser
||
strings
.
TrimSpace
(
req
.
OpenID
)
!=
""
}
func
(
s
*
PaymentService
)
getWeChatPaymentOAuthCredential
(
context
.
Context
)
(
string
,
string
,
error
)
{
appID
:=
strings
.
TrimSpace
(
os
.
Getenv
(
"WECHAT_OAUTH_MP_APP_ID"
))
appSecret
:=
strings
.
TrimSpace
(
os
.
Getenv
(
"WECHAT_OAUTH_MP_APP_SECRET"
))
if
appID
==
""
||
appSecret
==
""
{
func
(
s
*
PaymentService
)
getWeChatPaymentOAuthCredential
(
ctx
context
.
Context
)
(
string
,
string
,
error
)
{
if
s
==
nil
||
s
.
configService
==
nil
||
s
.
configService
.
settingRepo
==
nil
{
return
""
,
""
,
infraerrors
.
ServiceUnavailable
(
"WECHAT_PAYMENT_MP_NOT_CONFIGURED"
,
"wechat in-app payment requires a complete WeChat MP OAuth credential"
,
)
}
cfg
,
err
:=
(
&
SettingService
{
settingRepo
:
s
.
configService
.
settingRepo
})
.
GetWeChatConnectOAuthConfig
(
ctx
)
if
err
!=
nil
||
cfg
.
Mode
!=
"mp"
||
strings
.
TrimSpace
(
cfg
.
AppID
)
==
""
||
strings
.
TrimSpace
(
cfg
.
AppSecret
)
==
""
{
return
""
,
""
,
infraerrors
.
ServiceUnavailable
(
"WECHAT_PAYMENT_MP_NOT_CONFIGURED"
,
"wechat in-app payment requires a complete WeChat MP OAuth credential"
,
)
}
return
a
ppID
,
a
ppSecret
,
nil
return
strings
.
TrimSpace
(
cfg
.
A
ppID
)
,
strings
.
TrimSpace
(
cfg
.
A
ppSecret
)
,
nil
}
func
classifyCreatePaymentError
(
req
CreateOrderRequest
,
providerKey
string
,
err
error
)
error
{
...
...
backend/internal/service/payment_order_jsapi_test.go
View file @
ee3f158f
...
...
@@ -60,10 +60,17 @@ func TestSelectCreateOrderInstancePrefersJSAPICompatibleWxpayInstance(t *testing
}
configService
:=
&
PaymentConfigService
{
entClient
:
client
,
entClient
:
client
,
settingRepo
:
&
paymentConfigSettingRepoStub
{
values
:
map
[
string
]
string
{
SettingPaymentVisibleMethodWxpayEnabled
:
"true"
,
SettingPaymentVisibleMethodWxpaySource
:
VisibleMethodSourceOfficialWechat
,
SettingPaymentVisibleMethodWxpayEnabled
:
"true"
,
SettingPaymentVisibleMethodWxpaySource
:
VisibleMethodSourceOfficialWechat
,
SettingKeyWeChatConnectEnabled
:
"true"
,
SettingKeyWeChatConnectAppID
:
"wx-mp-app"
,
SettingKeyWeChatConnectAppSecret
:
"wechat-secret"
,
SettingKeyWeChatConnectMode
:
"mp"
,
SettingKeyWeChatConnectScopes
:
"snsapi_base"
,
SettingKeyWeChatConnectRedirectURL
:
"https://api.example.com/api/v1/auth/oauth/wechat/callback"
,
SettingKeyWeChatConnectFrontendRedirectURL
:
"/auth/wechat/callback"
,
}},
encryptionKey
:
[]
byte
(
jsapiTestEncryptionKey
),
}
...
...
@@ -77,9 +84,6 @@ func TestSelectCreateOrderInstancePrefersJSAPICompatibleWxpayInstance(t *testing
configService
:
configService
,
}
t
.
Setenv
(
"WECHAT_OAUTH_MP_APP_ID"
,
"wx-mp-app"
)
t
.
Setenv
(
"WECHAT_OAUTH_MP_APP_SECRET"
,
"wechat-secret"
)
sel
,
err
:=
svc
.
selectCreateOrderInstance
(
ctx
,
CreateOrderRequest
{
PaymentType
:
payment
.
TypeWxpay
,
OpenID
:
"openid-123"
,
...
...
backend/internal/service/payment_order_result_test.go
View file @
ee3f158f
...
...
@@ -91,10 +91,15 @@ func TestBuildCreateOrderResponseCopiesJSAPIPayload(t *testing.T) {
}
func
TestMaybeBuildWeChatOAuthRequiredResponse
(
t
*
testing
.
T
)
{
t
.
Setenv
(
"WECHAT_OAUTH_MP_APP_ID"
,
"wx123456"
)
t
.
Setenv
(
"WECHAT_OAUTH_MP_APP_SECRET"
,
"wechat-secret"
)
svc
:=
&
PaymentService
{}
svc
:=
newWeChatPaymentOAuthTestService
(
map
[
string
]
string
{
SettingKeyWeChatConnectEnabled
:
"true"
,
SettingKeyWeChatConnectAppID
:
"wx123456"
,
SettingKeyWeChatConnectAppSecret
:
"wechat-secret"
,
SettingKeyWeChatConnectMode
:
"mp"
,
SettingKeyWeChatConnectScopes
:
"snsapi_base"
,
SettingKeyWeChatConnectRedirectURL
:
"https://api.example.com/api/v1/auth/oauth/wechat/callback"
,
SettingKeyWeChatConnectFrontendRedirectURL
:
"/auth/wechat/callback"
,
})
resp
,
err
:=
svc
.
maybeBuildWeChatOAuthRequiredResponse
(
context
.
Background
(),
CreateOrderRequest
{
Amount
:
12.5
,
...
...
@@ -132,7 +137,7 @@ func TestMaybeBuildWeChatOAuthRequiredResponse(t *testing.T) {
func
TestMaybeBuildWeChatOAuthRequiredResponseRequiresMPConfigInWeChat
(
t
*
testing
.
T
)
{
t
.
Parallel
()
svc
:=
&
Paymen
tService
{}
svc
:=
newWeChatPaymentOAuthTes
tService
(
nil
)
resp
,
err
:=
svc
.
maybeBuildWeChatOAuthRequiredResponse
(
context
.
Background
(),
CreateOrderRequest
{
Amount
:
12.5
,
...
...
@@ -155,10 +160,15 @@ func TestMaybeBuildWeChatOAuthRequiredResponseRequiresMPConfigInWeChat(t *testin
}
func
TestMaybeBuildWeChatOAuthRequiredResponseForSelectionSkipsEasyPayProvider
(
t
*
testing
.
T
)
{
t
.
Setenv
(
"WECHAT_OAUTH_MP_APP_ID"
,
"wx123456"
)
t
.
Setenv
(
"WECHAT_OAUTH_MP_APP_SECRET"
,
"wechat-secret"
)
svc
:=
&
PaymentService
{}
svc
:=
newWeChatPaymentOAuthTestService
(
map
[
string
]
string
{
SettingKeyWeChatConnectEnabled
:
"true"
,
SettingKeyWeChatConnectAppID
:
"wx123456"
,
SettingKeyWeChatConnectAppSecret
:
"wechat-secret"
,
SettingKeyWeChatConnectMode
:
"mp"
,
SettingKeyWeChatConnectScopes
:
"snsapi_base"
,
SettingKeyWeChatConnectRedirectURL
:
"https://api.example.com/api/v1/auth/oauth/wechat/callback"
,
SettingKeyWeChatConnectFrontendRedirectURL
:
"/auth/wechat/callback"
,
})
resp
,
err
:=
svc
.
maybeBuildWeChatOAuthRequiredResponseForSelection
(
context
.
Background
(),
CreateOrderRequest
{
Amount
:
12.5
,
...
...
@@ -175,3 +185,11 @@ func TestMaybeBuildWeChatOAuthRequiredResponseForSelectionSkipsEasyPayProvider(t
t
.
Fatalf
(
"expected nil response, got %+v"
,
resp
)
}
}
func
newWeChatPaymentOAuthTestService
(
values
map
[
string
]
string
)
*
PaymentService
{
return
&
PaymentService
{
configService
:
&
PaymentConfigService
{
settingRepo
:
&
paymentConfigSettingRepoStub
{
values
:
values
},
},
}
}
backend/internal/service/setting_service.go
View file @
ee3f158f
...
...
@@ -9,7 +9,6 @@ import (
"fmt"
"log/slog"
"net/url"
"os"
"sort"
"strconv"
"strings"
...
...
@@ -173,8 +172,43 @@ var (
const
(
defaultAuthSourceBalance
=
0
defaultAuthSourceConcurrency
=
5
defaultWeChatConnectMode
=
"open"
defaultWeChatConnectScopes
=
"snsapi_login"
defaultWeChatConnectFrontend
=
"/auth/wechat/callback"
)
func
normalizeWeChatConnectModeSetting
(
raw
string
)
string
{
switch
strings
.
ToLower
(
strings
.
TrimSpace
(
raw
))
{
case
"mp"
:
return
"mp"
default
:
return
"open"
}
}
func
defaultWeChatConnectScopeForMode
(
mode
string
)
string
{
if
normalizeWeChatConnectModeSetting
(
mode
)
==
"mp"
{
return
"snsapi_userinfo"
}
return
defaultWeChatConnectScopes
}
func
normalizeWeChatConnectScopeSetting
(
raw
,
mode
string
)
string
{
switch
normalizeWeChatConnectModeSetting
(
mode
)
{
case
"mp"
:
switch
strings
.
TrimSpace
(
raw
)
{
case
"snsapi_base"
:
return
"snsapi_base"
case
"snsapi_userinfo"
:
return
"snsapi_userinfo"
default
:
return
defaultWeChatConnectScopeForMode
(
mode
)
}
default
:
return
defaultWeChatConnectScopes
}
}
// NewSettingService 创建系统设置服务实例
func
NewSettingService
(
settingRepo
SettingRepository
,
cfg
*
config
.
Config
)
*
SettingService
{
return
&
SettingService
{
...
...
@@ -240,6 +274,13 @@ func (s *SettingService) GetPublicSettings(ctx context.Context) (*PublicSettings
SettingKeyCustomMenuItems
,
SettingKeyCustomEndpoints
,
SettingKeyLinuxDoConnectEnabled
,
SettingKeyWeChatConnectEnabled
,
SettingKeyWeChatConnectAppID
,
SettingKeyWeChatConnectAppSecret
,
SettingKeyWeChatConnectMode
,
SettingKeyWeChatConnectScopes
,
SettingKeyWeChatConnectRedirectURL
,
SettingKeyWeChatConnectFrontendRedirectURL
,
SettingKeyBackendModeEnabled
,
SettingPaymentEnabled
,
SettingKeyOIDCConnectEnabled
,
...
...
@@ -274,9 +315,7 @@ func (s *SettingService) GetPublicSettings(ctx context.Context) (*PublicSettings
if
oidcProviderName
==
""
{
oidcProviderName
=
"OIDC"
}
weChatOpenEnabled
:=
isWeChatOAuthOpenConfigured
()
weChatMPEnabled
:=
isWeChatOAuthMPConfigured
()
weChatEnabled
:=
weChatOpenEnabled
||
weChatMPEnabled
weChatEnabled
,
weChatOpenEnabled
,
weChatMPEnabled
:=
s
.
weChatOAuthCapabilitiesFromSettings
(
settings
)
// Password reset requires email verification to be enabled
emailVerifyEnabled
:=
settings
[
SettingKeyEmailVerifyEnabled
]
==
"true"
...
...
@@ -431,6 +470,56 @@ func (s *SettingService) GetPublicSettingsForInjection(ctx context.Context) (any
},
nil
}
func
DefaultWeChatConnectScopesForMode
(
mode
string
)
string
{
return
defaultWeChatConnectScopeForMode
(
mode
)
}
func
(
s
*
SettingService
)
parseWeChatConnectOAuthConfig
(
settings
map
[
string
]
string
)
(
WeChatConnectOAuthConfig
,
error
)
{
cfg
:=
WeChatConnectOAuthConfig
{
Enabled
:
settings
[
SettingKeyWeChatConnectEnabled
]
==
"true"
,
AppID
:
strings
.
TrimSpace
(
settings
[
SettingKeyWeChatConnectAppID
]),
AppSecret
:
strings
.
TrimSpace
(
settings
[
SettingKeyWeChatConnectAppSecret
]),
Mode
:
normalizeWeChatConnectModeSetting
(
settings
[
SettingKeyWeChatConnectMode
]),
Scopes
:
normalizeWeChatConnectScopeSetting
(
settings
[
SettingKeyWeChatConnectScopes
],
settings
[
SettingKeyWeChatConnectMode
]),
RedirectURL
:
strings
.
TrimSpace
(
settings
[
SettingKeyWeChatConnectRedirectURL
]),
FrontendRedirectURL
:
strings
.
TrimSpace
(
settings
[
SettingKeyWeChatConnectFrontendRedirectURL
]),
}
if
cfg
.
FrontendRedirectURL
==
""
{
cfg
.
FrontendRedirectURL
=
defaultWeChatConnectFrontend
}
if
!
cfg
.
Enabled
{
return
WeChatConnectOAuthConfig
{},
infraerrors
.
NotFound
(
"OAUTH_DISABLED"
,
"wechat oauth is disabled"
)
}
if
cfg
.
AppID
==
""
{
return
WeChatConnectOAuthConfig
{},
infraerrors
.
InternalServer
(
"OAUTH_CONFIG_INVALID"
,
"wechat oauth app id not configured"
)
}
if
cfg
.
AppSecret
==
""
{
return
WeChatConnectOAuthConfig
{},
infraerrors
.
InternalServer
(
"OAUTH_CONFIG_INVALID"
,
"wechat oauth app secret not configured"
)
}
if
cfg
.
RedirectURL
==
""
{
return
WeChatConnectOAuthConfig
{},
infraerrors
.
InternalServer
(
"OAUTH_CONFIG_INVALID"
,
"wechat oauth redirect url not configured"
)
}
if
cfg
.
FrontendRedirectURL
==
""
{
return
WeChatConnectOAuthConfig
{},
infraerrors
.
InternalServer
(
"OAUTH_CONFIG_INVALID"
,
"wechat oauth frontend redirect url not configured"
)
}
if
err
:=
config
.
ValidateAbsoluteHTTPURL
(
cfg
.
RedirectURL
);
err
!=
nil
{
return
WeChatConnectOAuthConfig
{},
infraerrors
.
InternalServer
(
"OAUTH_CONFIG_INVALID"
,
"wechat oauth redirect url invalid"
)
}
if
err
:=
config
.
ValidateFrontendRedirectURL
(
cfg
.
FrontendRedirectURL
);
err
!=
nil
{
return
WeChatConnectOAuthConfig
{},
infraerrors
.
InternalServer
(
"OAUTH_CONFIG_INVALID"
,
"wechat oauth frontend redirect url invalid"
)
}
return
cfg
,
nil
}
func
(
s
*
SettingService
)
weChatOAuthCapabilitiesFromSettings
(
settings
map
[
string
]
string
)
(
bool
,
bool
,
bool
)
{
cfg
,
err
:=
s
.
parseWeChatConnectOAuthConfig
(
settings
)
if
err
!=
nil
{
return
false
,
false
,
false
}
return
true
,
cfg
.
Mode
==
"open"
,
cfg
.
Mode
==
"mp"
}
// filterUserVisibleMenuItems filters out admin-only menu items from a raw JSON
// array string, returning only items with visibility != "admin".
func
filterUserVisibleMenuItems
(
raw
string
)
json
.
RawMessage
{
...
...
@@ -467,20 +556,6 @@ func filterUserVisibleMenuItems(raw string) json.RawMessage {
return
result
}
func
isWeChatOAuthConfigured
()
bool
{
return
isWeChatOAuthOpenConfigured
()
||
isWeChatOAuthMPConfigured
()
}
func
isWeChatOAuthOpenConfigured
()
bool
{
return
strings
.
TrimSpace
(
os
.
Getenv
(
"WECHAT_OAUTH_OPEN_APP_ID"
))
!=
""
&&
strings
.
TrimSpace
(
os
.
Getenv
(
"WECHAT_OAUTH_OPEN_APP_SECRET"
))
!=
""
}
func
isWeChatOAuthMPConfigured
()
bool
{
return
strings
.
TrimSpace
(
os
.
Getenv
(
"WECHAT_OAUTH_MP_APP_ID"
))
!=
""
&&
strings
.
TrimSpace
(
os
.
Getenv
(
"WECHAT_OAUTH_MP_APP_SECRET"
))
!=
""
}
// safeRawJSONArray returns raw as json.RawMessage if it's valid JSON, otherwise "[]".
func
safeRawJSONArray
(
raw
string
)
json
.
RawMessage
{
raw
=
strings
.
TrimSpace
(
raw
)
...
...
@@ -625,6 +700,15 @@ func (s *SettingService) buildSystemSettingsUpdates(ctx context.Context, setting
}
settings
.
PaymentVisibleMethodAlipaySource
=
alipaySource
settings
.
PaymentVisibleMethodWxpaySource
=
wxpaySource
settings
.
WeChatConnectAppID
=
strings
.
TrimSpace
(
settings
.
WeChatConnectAppID
)
settings
.
WeChatConnectAppSecret
=
strings
.
TrimSpace
(
settings
.
WeChatConnectAppSecret
)
settings
.
WeChatConnectMode
=
normalizeWeChatConnectModeSetting
(
settings
.
WeChatConnectMode
)
settings
.
WeChatConnectScopes
=
normalizeWeChatConnectScopeSetting
(
settings
.
WeChatConnectScopes
,
settings
.
WeChatConnectMode
)
settings
.
WeChatConnectRedirectURL
=
strings
.
TrimSpace
(
settings
.
WeChatConnectRedirectURL
)
settings
.
WeChatConnectFrontendRedirectURL
=
strings
.
TrimSpace
(
settings
.
WeChatConnectFrontendRedirectURL
)
if
settings
.
WeChatConnectFrontendRedirectURL
==
""
{
settings
.
WeChatConnectFrontendRedirectURL
=
defaultWeChatConnectFrontend
}
updates
:=
make
(
map
[
string
]
string
)
...
...
@@ -694,6 +778,17 @@ func (s *SettingService) buildSystemSettingsUpdates(ctx context.Context, setting
updates
[
SettingKeyOIDCConnectClientSecret
]
=
settings
.
OIDCConnectClientSecret
}
// WeChat Connect OAuth 登录
updates
[
SettingKeyWeChatConnectEnabled
]
=
strconv
.
FormatBool
(
settings
.
WeChatConnectEnabled
)
updates
[
SettingKeyWeChatConnectAppID
]
=
settings
.
WeChatConnectAppID
updates
[
SettingKeyWeChatConnectMode
]
=
settings
.
WeChatConnectMode
updates
[
SettingKeyWeChatConnectScopes
]
=
settings
.
WeChatConnectScopes
updates
[
SettingKeyWeChatConnectRedirectURL
]
=
settings
.
WeChatConnectRedirectURL
updates
[
SettingKeyWeChatConnectFrontendRedirectURL
]
=
settings
.
WeChatConnectFrontendRedirectURL
if
settings
.
WeChatConnectAppSecret
!=
""
{
updates
[
SettingKeyWeChatConnectAppSecret
]
=
settings
.
WeChatConnectAppSecret
}
// OEM设置
updates
[
SettingKeySiteName
]
=
settings
.
SiteName
updates
[
SettingKeySiteLogo
]
=
settings
.
SiteLogo
...
...
@@ -1200,6 +1295,10 @@ func (s *SettingService) InitializeDefaultSettings(ctx context.Context) error {
SettingKeyTablePageSizeOptions
:
"[10,20,50,100]"
,
SettingKeyCustomMenuItems
:
"[]"
,
SettingKeyCustomEndpoints
:
"[]"
,
SettingKeyWeChatConnectEnabled
:
"false"
,
SettingKeyWeChatConnectMode
:
"open"
,
SettingKeyWeChatConnectScopes
:
"snsapi_login"
,
SettingKeyWeChatConnectFrontendRedirectURL
:
defaultWeChatConnectFrontend
,
SettingKeyOIDCConnectEnabled
:
"false"
,
SettingKeyOIDCConnectProviderName
:
"OIDC"
,
SettingKeyDefaultConcurrency
:
strconv
.
Itoa
(
s
.
cfg
.
Default
.
UserConcurrency
),
...
...
@@ -1491,6 +1590,19 @@ func (s *SettingService) parseSettings(settings map[string]string) *SystemSettin
}
result
.
OIDCConnectClientSecretConfigured
=
result
.
OIDCConnectClientSecret
!=
""
// WeChat Connect 设置:完全以 DB 系统设置为准。
result
.
WeChatConnectEnabled
=
settings
[
SettingKeyWeChatConnectEnabled
]
==
"true"
result
.
WeChatConnectAppID
=
strings
.
TrimSpace
(
settings
[
SettingKeyWeChatConnectAppID
])
result
.
WeChatConnectAppSecret
=
strings
.
TrimSpace
(
settings
[
SettingKeyWeChatConnectAppSecret
])
result
.
WeChatConnectAppSecretConfigured
=
result
.
WeChatConnectAppSecret
!=
""
result
.
WeChatConnectMode
=
normalizeWeChatConnectModeSetting
(
settings
[
SettingKeyWeChatConnectMode
])
result
.
WeChatConnectScopes
=
normalizeWeChatConnectScopeSetting
(
settings
[
SettingKeyWeChatConnectScopes
],
settings
[
SettingKeyWeChatConnectMode
])
result
.
WeChatConnectRedirectURL
=
strings
.
TrimSpace
(
settings
[
SettingKeyWeChatConnectRedirectURL
])
result
.
WeChatConnectFrontendRedirectURL
=
strings
.
TrimSpace
(
settings
[
SettingKeyWeChatConnectFrontendRedirectURL
])
if
result
.
WeChatConnectFrontendRedirectURL
==
""
{
result
.
WeChatConnectFrontendRedirectURL
=
defaultWeChatConnectFrontend
}
// Model fallback settings
result
.
EnableModelFallback
=
settings
[
SettingKeyEnableModelFallback
]
==
"true"
result
.
FallbackModelAnthropic
=
s
.
getStringOrDefault
(
settings
,
SettingKeyFallbackModelAnthropic
,
"claude-3-5-sonnet-20241022"
)
...
...
@@ -1972,6 +2084,26 @@ func (s *SettingService) GetLinuxDoConnectOAuthConfig(ctx context.Context) (conf
return
effective
,
nil
}
// GetWeChatConnectOAuthConfig 返回用于登录的最终生效 WeChat Connect 配置。
//
// WeChat Connect 已回归 DB 系统设置模型,不再回退到 config/env。
func
(
s
*
SettingService
)
GetWeChatConnectOAuthConfig
(
ctx
context
.
Context
)
(
WeChatConnectOAuthConfig
,
error
)
{
keys
:=
[]
string
{
SettingKeyWeChatConnectEnabled
,
SettingKeyWeChatConnectAppID
,
SettingKeyWeChatConnectAppSecret
,
SettingKeyWeChatConnectMode
,
SettingKeyWeChatConnectScopes
,
SettingKeyWeChatConnectRedirectURL
,
SettingKeyWeChatConnectFrontendRedirectURL
,
}
settings
,
err
:=
s
.
settingRepo
.
GetMultiple
(
ctx
,
keys
)
if
err
!=
nil
{
return
WeChatConnectOAuthConfig
{},
fmt
.
Errorf
(
"get wechat connect settings: %w"
,
err
)
}
return
s
.
parseWeChatConnectOAuthConfig
(
settings
)
}
// GetOverloadCooldownSettings 获取529过载冷却配置
func
(
s
*
SettingService
)
GetOverloadCooldownSettings
(
ctx
context
.
Context
)
(
*
OverloadCooldownSettings
,
error
)
{
value
,
err
:=
s
.
settingRepo
.
GetValue
(
ctx
,
SettingKeyOverloadCooldownSettings
)
...
...
backend/internal/service/setting_service_public_test.go
View file @
ee3f158f
...
...
@@ -92,16 +92,21 @@ func TestSettingService_GetPublicSettings_ExposesForceEmailOnThirdPartySignup(t
}
func
TestSettingService_GetPublicSettings_ExposesWeChatOAuthModeCapabilities
(
t
*
testing
.
T
)
{
t
.
Setenv
(
"WECHAT_OAUTH_OPEN_APP_ID"
,
"wx-open-app"
)
t
.
Setenv
(
"WECHAT_OAUTH_OPEN_APP_SECRET"
,
"wx-open-secret"
)
t
.
Setenv
(
"WECHAT_OAUTH_MP_APP_ID"
,
""
)
t
.
Setenv
(
"WECHAT_OAUTH_MP_APP_SECRET"
,
""
)
svc
:=
NewSettingService
(
&
settingPublicRepoStub
{},
&
config
.
Config
{})
svc
:=
NewSettingService
(
&
settingPublicRepoStub
{
values
:
map
[
string
]
string
{
SettingKeyWeChatConnectEnabled
:
"true"
,
SettingKeyWeChatConnectAppID
:
"wx-mp-app"
,
SettingKeyWeChatConnectAppSecret
:
"wx-mp-secret"
,
SettingKeyWeChatConnectMode
:
"mp"
,
SettingKeyWeChatConnectScopes
:
"snsapi_base"
,
SettingKeyWeChatConnectRedirectURL
:
"https://api.example.com/api/v1/auth/oauth/wechat/callback"
,
SettingKeyWeChatConnectFrontendRedirectURL
:
"/auth/wechat/callback"
,
},
},
&
config
.
Config
{})
settings
,
err
:=
svc
.
GetPublicSettings
(
context
.
Background
())
require
.
NoError
(
t
,
err
)
require
.
True
(
t
,
settings
.
WeChatOAuthEnabled
)
require
.
Tru
e
(
t
,
settings
.
WeChatOAuthOpenEnabled
)
require
.
Fals
e
(
t
,
settings
.
WeChatOAuthMPEnabled
)
require
.
Fals
e
(
t
,
settings
.
WeChatOAuthOpenEnabled
)
require
.
Tru
e
(
t
,
settings
.
WeChatOAuthMPEnabled
)
}
backend/internal/service/setting_service_wechat_config_test.go
0 → 100644
View file @
ee3f158f
//go:build unit
package
service
import
(
"context"
"testing"
"github.com/Wei-Shaw/sub2api/internal/config"
"github.com/stretchr/testify/require"
)
type
settingWeChatRepoStub
struct
{
values
map
[
string
]
string
}
func
(
s
*
settingWeChatRepoStub
)
Get
(
context
.
Context
,
string
)
(
*
Setting
,
error
)
{
panic
(
"unexpected Get call"
)
}
func
(
s
*
settingWeChatRepoStub
)
GetValue
(
_
context
.
Context
,
key
string
)
(
string
,
error
)
{
if
value
,
ok
:=
s
.
values
[
key
];
ok
{
return
value
,
nil
}
return
""
,
ErrSettingNotFound
}
func
(
s
*
settingWeChatRepoStub
)
Set
(
context
.
Context
,
string
,
string
)
error
{
panic
(
"unexpected Set call"
)
}
func
(
s
*
settingWeChatRepoStub
)
GetMultiple
(
_
context
.
Context
,
keys
[]
string
)
(
map
[
string
]
string
,
error
)
{
out
:=
make
(
map
[
string
]
string
,
len
(
keys
))
for
_
,
key
:=
range
keys
{
if
value
,
ok
:=
s
.
values
[
key
];
ok
{
out
[
key
]
=
value
}
}
return
out
,
nil
}
func
(
s
*
settingWeChatRepoStub
)
SetMultiple
(
context
.
Context
,
map
[
string
]
string
)
error
{
panic
(
"unexpected SetMultiple call"
)
}
func
(
s
*
settingWeChatRepoStub
)
GetAll
(
context
.
Context
)
(
map
[
string
]
string
,
error
)
{
panic
(
"unexpected GetAll call"
)
}
func
(
s
*
settingWeChatRepoStub
)
Delete
(
context
.
Context
,
string
)
error
{
panic
(
"unexpected Delete call"
)
}
func
TestSettingService_GetWeChatConnectOAuthConfig_UsesDatabaseOverrides
(
t
*
testing
.
T
)
{
repo
:=
&
settingWeChatRepoStub
{
values
:
map
[
string
]
string
{
SettingKeyWeChatConnectEnabled
:
"true"
,
SettingKeyWeChatConnectAppID
:
"wx-db-app"
,
SettingKeyWeChatConnectAppSecret
:
"wx-db-secret"
,
SettingKeyWeChatConnectMode
:
"mp"
,
SettingKeyWeChatConnectScopes
:
"snsapi_base"
,
SettingKeyWeChatConnectRedirectURL
:
"https://api.example.com/api/v1/auth/oauth/wechat/callback"
,
SettingKeyWeChatConnectFrontendRedirectURL
:
"/auth/wechat/callback"
,
},
}
svc
:=
NewSettingService
(
repo
,
&
config
.
Config
{})
got
,
err
:=
svc
.
GetWeChatConnectOAuthConfig
(
context
.
Background
())
require
.
NoError
(
t
,
err
)
require
.
True
(
t
,
got
.
Enabled
)
require
.
Equal
(
t
,
"wx-db-app"
,
got
.
AppID
)
require
.
Equal
(
t
,
"wx-db-secret"
,
got
.
AppSecret
)
require
.
Equal
(
t
,
"mp"
,
got
.
Mode
)
require
.
Equal
(
t
,
"snsapi_base"
,
got
.
Scopes
)
require
.
Equal
(
t
,
"https://api.example.com/api/v1/auth/oauth/wechat/callback"
,
got
.
RedirectURL
)
require
.
Equal
(
t
,
"/auth/wechat/callback"
,
got
.
FrontendRedirectURL
)
}
backend/internal/service/settings_view.go
View file @
ee3f158f
...
...
@@ -31,6 +31,16 @@ type SystemSettings struct {
LinuxDoConnectClientSecretConfigured
bool
LinuxDoConnectRedirectURL
string
// WeChat Connect OAuth 登录
WeChatConnectEnabled
bool
WeChatConnectAppID
string
WeChatConnectAppSecret
string
WeChatConnectAppSecretConfigured
bool
WeChatConnectMode
string
WeChatConnectScopes
string
WeChatConnectRedirectURL
string
WeChatConnectFrontendRedirectURL
string
// Generic OIDC OAuth 登录
OIDCConnectEnabled
bool
OIDCConnectProviderName
string
...
...
@@ -177,6 +187,16 @@ type PublicSettings struct {
BalanceLowNotifyRechargeURL
string
}
type
WeChatConnectOAuthConfig
struct
{
Enabled
bool
AppID
string
AppSecret
string
Mode
string
Scopes
string
RedirectURL
string
FrontendRedirectURL
string
}
// StreamTimeoutSettings 流超时处理配置(仅控制超时后的处理方式,超时判定由网关配置控制)
type
StreamTimeoutSettings
struct
{
// Enabled 是否启用流超时处理
...
...
frontend/src/api/__tests__/settings.wechatConnect.spec.ts
0 → 100644
View file @
ee3f158f
import
{
describe
,
expect
,
it
}
from
"
vitest
"
;
import
{
defaultWeChatConnectScopesForMode
,
normalizeWeChatConnectMode
,
}
from
"
@/api/admin/settings
"
;
describe
(
"
admin settings wechat connect helpers
"
,
()
=>
{
it
(
"
normalizes legacy or noisy mode values to the backend contract
"
,
()
=>
{
expect
(
normalizeWeChatConnectMode
(
"
OPEN
"
)).
toBe
(
"
open
"
);
expect
(
normalizeWeChatConnectMode
(
"
open_platform
"
)).
toBe
(
"
open
"
);
expect
(
normalizeWeChatConnectMode
(
"
mp
"
)).
toBe
(
"
mp
"
);
expect
(
normalizeWeChatConnectMode
(
"
official_account
"
)).
toBe
(
"
mp
"
);
expect
(
normalizeWeChatConnectMode
(
"
unknown
"
)).
toBe
(
"
open
"
);
});
it
(
"
maps each mode to the backend default scopes
"
,
()
=>
{
expect
(
defaultWeChatConnectScopesForMode
(
"
open
"
)).
toBe
(
"
snsapi_login
"
);
expect
(
defaultWeChatConnectScopesForMode
(
"
mp
"
)).
toBe
(
"
snsapi_userinfo
"
);
});
});
frontend/src/api/admin/settings.ts
View file @
ee3f158f
...
...
@@ -3,155 +3,240 @@
* Handles system settings management for administrators
*/
import
{
apiClient
}
from
'
../client
'
import
type
{
CustomMenuItem
,
CustomEndpoint
,
NotifyEmailEntry
}
from
'
@/types
'
import
{
apiClient
}
from
"
../client
"
;
import
type
{
CustomMenuItem
,
CustomEndpoint
,
NotifyEmailEntry
}
from
"
@/types
"
;
export
interface
DefaultSubscriptionSetting
{
group_id
:
number
validity_days
:
number
group_id
:
number
;
validity_days
:
number
;
}
export
type
AuthSourceType
=
'
email
'
|
'
linuxdo
'
|
'
oidc
'
|
'
wechat
'
export
type
AuthSourceType
=
"
email
"
|
"
linuxdo
"
|
"
oidc
"
|
"
wechat
"
;
export
interface
AuthSourceDefaultsValue
{
balance
:
number
concurrency
:
number
subscriptions
:
DefaultSubscriptionSetting
[]
grant_on_signup
:
boolean
grant_on_first_bind
:
boolean
}
export
type
AuthSourceDefaultsState
=
Record
<
AuthSourceType
,
AuthSourceDefaultsValue
>
export
type
PaymentVisibleMethod
=
'
alipay
'
|
'
wxpay
'
balance
:
number
;
concurrency
:
number
;
subscriptions
:
DefaultSubscriptionSetting
[];
grant_on_signup
:
boolean
;
grant_on_first_bind
:
boolean
;
}
export
type
AuthSourceDefaultsState
=
Record
<
AuthSourceType
,
AuthSourceDefaultsValue
>
;
export
type
PaymentVisibleMethod
=
"
alipay
"
|
"
wxpay
"
;
export
type
PaymentVisibleMethodSource
=
|
''
|
'
official_alipay
'
|
'
easypay_alipay
'
|
'
official_wxpay
'
|
'
easypay_wxpay
'
|
""
|
"
official_alipay
"
|
"
easypay_alipay
"
|
"
official_wxpay
"
|
"
easypay_wxpay
"
;
export
type
WeChatConnectMode
=
"
open
"
|
"
mp
"
;
export
interface
PaymentVisibleMethodSourceOption
{
value
:
PaymentVisibleMethodSource
labelZh
:
string
labelEn
:
string
value
:
PaymentVisibleMethodSource
;
labelZh
:
string
;
labelEn
:
string
;
}
export
interface
WeChatConnectModeOption
{
value
:
WeChatConnectMode
;
labelZh
:
string
;
labelEn
:
string
;
}
const
AUTH_SOURCE_TYPES
:
AuthSourceType
[]
=
[
'
email
'
,
'
linuxdo
'
,
'
oidc
'
,
'
wechat
'
]
const
AUTH_SOURCE_DEFAULT_BALANCE
=
0
const
AUTH_SOURCE_DEFAULT_CONCURRENCY
=
5
const
AUTH_SOURCE_TYPES
:
AuthSourceType
[]
=
[
"
email
"
,
"
linuxdo
"
,
"
oidc
"
,
"
wechat
"
,
];
const
AUTH_SOURCE_DEFAULT_BALANCE
=
0
;
const
AUTH_SOURCE_DEFAULT_CONCURRENCY
=
5
;
const
PAYMENT_VISIBLE_METHOD_SOURCE_OPTIONS
:
Record
<
PaymentVisibleMethod
,
PaymentVisibleMethodSourceOption
[]
>
=
{
alipay
:
[
{
value
:
''
,
labelZh
:
'
未配置
'
,
labelEn
:
'
Not configured
'
},
{
value
:
'
official_alipay
'
,
labelZh
:
'
支付宝官方
'
,
labelEn
:
'
Official Alipay
'
},
{
value
:
'
easypay_alipay
'
,
labelZh
:
'
易支付支付宝
'
,
labelEn
:
'
EasyPay Alipay
'
},
{
value
:
""
,
labelZh
:
"
未配置
"
,
labelEn
:
"
Not configured
"
},
{
value
:
"
official_alipay
"
,
labelZh
:
"
支付宝官方
"
,
labelEn
:
"
Official Alipay
"
,
},
{
value
:
"
easypay_alipay
"
,
labelZh
:
"
易支付支付宝
"
,
labelEn
:
"
EasyPay Alipay
"
,
},
],
wxpay
:
[
{
value
:
''
,
labelZh
:
'
未配置
'
,
labelEn
:
'
Not configured
'
},
{
value
:
'
official_wxpay
'
,
labelZh
:
'
微信官方
'
,
labelEn
:
'
Official WeChat Pay
'
},
{
value
:
'
easypay_wxpay
'
,
labelZh
:
'
易支付微信
'
,
labelEn
:
'
EasyPay WeChat Pay
'
},
{
value
:
""
,
labelZh
:
"
未配置
"
,
labelEn
:
"
Not configured
"
},
{
value
:
"
official_wxpay
"
,
labelZh
:
"
微信官方
"
,
labelEn
:
"
Official WeChat Pay
"
,
},
{
value
:
"
easypay_wxpay
"
,
labelZh
:
"
易支付微信
"
,
labelEn
:
"
EasyPay WeChat Pay
"
,
},
],
}
}
;
const
PAYMENT_VISIBLE_METHOD_SOURCE_ALIASES
:
Record
<
PaymentVisibleMethod
,
Record
<
string
,
PaymentVisibleMethodSource
>
>
=
{
alipay
:
{
official_alipay
:
'
official_alipay
'
,
alipay
:
'
official_alipay
'
,
alipay_direct
:
'
official_alipay
'
,
official
:
'
official_alipay
'
,
easypay_alipay
:
'
easypay_alipay
'
,
easypay
:
'
easypay_alipay
'
,
official_alipay
:
"
official_alipay
"
,
alipay
:
"
official_alipay
"
,
alipay_direct
:
"
official_alipay
"
,
official
:
"
official_alipay
"
,
easypay_alipay
:
"
easypay_alipay
"
,
easypay
:
"
easypay_alipay
"
,
},
wxpay
:
{
official_wxpay
:
'
official_wxpay
'
,
wxpay
:
'
official_wxpay
'
,
wxpay_direct
:
'
official_wxpay
'
,
wechat
:
'
official_wxpay
'
,
official
:
'
official_wxpay
'
,
easypay_wxpay
:
'
easypay_wxpay
'
,
easypay
:
'
easypay_wxpay
'
,
official_wxpay
:
"
official_wxpay
"
,
wxpay
:
"
official_wxpay
"
,
wxpay_direct
:
"
official_wxpay
"
,
wechat
:
"
official_wxpay
"
,
official
:
"
official_wxpay
"
,
easypay_wxpay
:
"
easypay_wxpay
"
,
easypay
:
"
easypay_wxpay
"
,
},
}
};
const
WECHAT_CONNECT_MODE_OPTIONS
:
WeChatConnectModeOption
[]
=
[
{
value
:
"
open
"
,
labelZh
:
"
微信开放平台
"
,
labelEn
:
"
WeChat Open Platform
"
},
{
value
:
"
mp
"
,
labelZh
:
"
微信公众号 / 小程序
"
,
labelEn
:
"
WeChat Official Account / Mini Program
"
,
},
];
const
WECHAT_CONNECT_MODE_ALIASES
:
Record
<
string
,
WeChatConnectMode
>
=
{
open
:
"
open
"
,
open_platform
:
"
open
"
,
official
:
"
open
"
,
wx_open
:
"
open
"
,
mp
:
"
mp
"
,
official_account
:
"
mp
"
,
wechat_mp
:
"
mp
"
,
mini_program
:
"
mp
"
,
};
export
function
normalizeDefaultSubscriptionSettings
(
subscriptions
:
DefaultSubscriptionSetting
[]
|
null
|
undefined
subscriptions
:
DefaultSubscriptionSetting
[]
|
null
|
undefined
,
):
DefaultSubscriptionSetting
[]
{
if
(
!
Array
.
isArray
(
subscriptions
))
return
[]
if
(
!
Array
.
isArray
(
subscriptions
))
return
[]
;
return
subscriptions
.
filter
((
item
)
=>
item
.
group_id
>
0
&&
item
.
validity_days
>
0
)
.
map
((
item
)
=>
({
group_id
:
Math
.
floor
(
item
.
group_id
),
validity_days
:
Math
.
min
(
36500
,
Math
.
max
(
1
,
Math
.
floor
(
item
.
validity_days
)))
}))
validity_days
:
Math
.
min
(
36500
,
Math
.
max
(
1
,
Math
.
floor
(
item
.
validity_days
)),
),
}));
}
export
function
buildAuthSourceDefaultsState
(
settings
:
Partial
<
SystemSettings
>
settings
:
Partial
<
SystemSettings
>
,
):
AuthSourceDefaultsState
{
const
raw
=
settings
as
Record
<
string
,
unknown
>
const
raw
=
settings
as
Record
<
string
,
unknown
>
;
return
AUTH_SOURCE_TYPES
.
reduce
((
acc
,
source
)
=>
{
const
subscriptions
=
raw
[
`auth_source_default_
${
source
}
_subscriptions`
]
const
subscriptions
=
raw
[
`auth_source_default_
${
source
}
_subscriptions`
]
;
acc
[
source
]
=
{
balance
:
Number
(
raw
[
`auth_source_default_
${
source
}
_balance`
]
??
AUTH_SOURCE_DEFAULT_BALANCE
),
balance
:
Number
(
raw
[
`auth_source_default_
${
source
}
_balance`
]
??
AUTH_SOURCE_DEFAULT_BALANCE
,
),
concurrency
:
Math
.
max
(
1
,
Number
(
raw
[
`auth_source_default_
${
source
}
_concurrency`
]
??
AUTH_SOURCE_DEFAULT_CONCURRENCY
)
Number
(
raw
[
`auth_source_default_
${
source
}
_concurrency`
]
??
AUTH_SOURCE_DEFAULT_CONCURRENCY
,
),
),
subscriptions
:
normalizeDefaultSubscriptionSettings
(
Array
.
isArray
(
subscriptions
)
?
(
subscriptions
as
DefaultSubscriptionSetting
[])
:
[]
Array
.
isArray
(
subscriptions
)
?
(
subscriptions
as
DefaultSubscriptionSetting
[])
:
[],
),
grant_on_signup
:
raw
[
`auth_source_default_
${
source
}
_grant_on_signup`
]
!==
false
,
grant_on_first_bind
:
raw
[
`auth_source_default_
${
source
}
_grant_on_first_bind`
]
===
true
,
}
return
acc
},
{}
as
AuthSourceDefaultsState
)
grant_on_signup
:
raw
[
`auth_source_default_
${
source
}
_grant_on_signup`
]
!==
false
,
grant_on_first_bind
:
raw
[
`auth_source_default_
${
source
}
_grant_on_first_bind`
]
===
true
,
};
return
acc
;
},
{}
as
AuthSourceDefaultsState
);
}
export
function
appendAuthSourceDefaultsToUpdateRequest
(
payload
:
UpdateSettingsRequest
,
authSourceDefaults
:
AuthSourceDefaultsState
authSourceDefaults
:
AuthSourceDefaultsState
,
):
UpdateSettingsRequest
{
const
target
=
payload
as
Record
<
string
,
unknown
>
const
target
=
payload
as
Record
<
string
,
unknown
>
;
for
(
const
source
of
AUTH_SOURCE_TYPES
)
{
const
current
=
authSourceDefaults
[
source
]
target
[
`auth_source_default_
${
source
}
_balance`
]
=
Number
(
current
.
balance
)
||
0
const
current
=
authSourceDefaults
[
source
];
target
[
`auth_source_default_
${
source
}
_balance`
]
=
Number
(
current
.
balance
)
||
0
;
target
[
`auth_source_default_
${
source
}
_concurrency`
]
=
Math
.
max
(
1
,
Math
.
floor
(
Number
(
current
.
concurrency
)
||
AUTH_SOURCE_DEFAULT_CONCURRENCY
)
)
target
[
`auth_source_default_
${
source
}
_subscriptions`
]
=
normalizeDefaultSubscriptionSettings
(
current
.
subscriptions
)
target
[
`auth_source_default_
${
source
}
_grant_on_signup`
]
=
current
.
grant_on_signup
target
[
`auth_source_default_
${
source
}
_grant_on_first_bind`
]
=
current
.
grant_on_first_bind
Math
.
floor
(
Number
(
current
.
concurrency
)
||
AUTH_SOURCE_DEFAULT_CONCURRENCY
,
),
);
target
[
`auth_source_default_
${
source
}
_subscriptions`
]
=
normalizeDefaultSubscriptionSettings
(
current
.
subscriptions
);
target
[
`auth_source_default_
${
source
}
_grant_on_signup`
]
=
current
.
grant_on_signup
;
target
[
`auth_source_default_
${
source
}
_grant_on_first_bind`
]
=
current
.
grant_on_first_bind
;
}
return
payload
return
payload
;
}
export
function
getPaymentVisibleMethodSourceOptions
(
method
:
PaymentVisibleMethod
method
:
PaymentVisibleMethod
,
):
PaymentVisibleMethodSourceOption
[]
{
return
PAYMENT_VISIBLE_METHOD_SOURCE_OPTIONS
[
method
]
return
PAYMENT_VISIBLE_METHOD_SOURCE_OPTIONS
[
method
]
;
}
export
function
normalizePaymentVisibleMethodSource
(
method
:
PaymentVisibleMethod
,
source
:
unknown
source
:
unknown
,
):
PaymentVisibleMethodSource
{
if
(
typeof
source
!==
'
string
'
)
return
''
if
(
typeof
source
!==
"
string
"
)
return
""
;
const
normalized
=
source
.
trim
().
toLowerCase
()
if
(
!
normalized
)
return
''
const
normalized
=
source
.
trim
().
toLowerCase
()
;
if
(
!
normalized
)
return
""
;
return
PAYMENT_VISIBLE_METHOD_SOURCE_ALIASES
[
method
][
normalized
]
??
''
return
PAYMENT_VISIBLE_METHOD_SOURCE_ALIASES
[
method
][
normalized
]
??
""
;
}
export
function
getWeChatConnectModeOptions
():
WeChatConnectModeOption
[]
{
return
WECHAT_CONNECT_MODE_OPTIONS
;
}
export
function
normalizeWeChatConnectMode
(
source
:
unknown
):
WeChatConnectMode
{
if
(
typeof
source
!==
"
string
"
)
return
"
open
"
;
const
normalized
=
source
.
trim
().
toLowerCase
();
if
(
!
normalized
)
return
"
open
"
;
return
WECHAT_CONNECT_MODE_ALIASES
[
normalized
]
??
"
open
"
;
}
export
function
defaultWeChatConnectScopesForMode
(
mode
:
unknown
):
string
{
return
normalizeWeChatConnectMode
(
mode
)
===
"
mp
"
?
"
snsapi_userinfo
"
:
"
snsapi_login
"
;
}
/**
...
...
@@ -159,293 +244,309 @@ export function normalizePaymentVisibleMethodSource(
*/
export
interface
SystemSettings
{
// Registration settings
registration_enabled
:
boolean
email_verify_enabled
:
boolean
registration_email_suffix_whitelist
:
string
[]
promo_code_enabled
:
boolean
password_reset_enabled
:
boolean
frontend_url
:
string
invitation_code_enabled
:
boolean
totp_enabled
:
boolean
// TOTP 双因素认证
totp_encryption_key_configured
:
boolean
// TOTP 加密密钥是否已配置
registration_enabled
:
boolean
;
email_verify_enabled
:
boolean
;
registration_email_suffix_whitelist
:
string
[]
;
promo_code_enabled
:
boolean
;
password_reset_enabled
:
boolean
;
frontend_url
:
string
;
invitation_code_enabled
:
boolean
;
totp_enabled
:
boolean
;
// TOTP 双因素认证
totp_encryption_key_configured
:
boolean
;
// TOTP 加密密钥是否已配置
// Default settings
default_balance
:
number
default_concurrency
:
number
default_subscriptions
:
DefaultSubscriptionSetting
[]
auth_source_default_email_balance
?:
number
auth_source_default_email_concurrency
?:
number
auth_source_default_email_subscriptions
?:
DefaultSubscriptionSetting
[]
auth_source_default_email_grant_on_signup
?:
boolean
auth_source_default_email_grant_on_first_bind
?:
boolean
auth_source_default_linuxdo_balance
?:
number
auth_source_default_linuxdo_concurrency
?:
number
auth_source_default_linuxdo_subscriptions
?:
DefaultSubscriptionSetting
[]
auth_source_default_linuxdo_grant_on_signup
?:
boolean
auth_source_default_linuxdo_grant_on_first_bind
?:
boolean
auth_source_default_oidc_balance
?:
number
auth_source_default_oidc_concurrency
?:
number
auth_source_default_oidc_subscriptions
?:
DefaultSubscriptionSetting
[]
auth_source_default_oidc_grant_on_signup
?:
boolean
auth_source_default_oidc_grant_on_first_bind
?:
boolean
auth_source_default_wechat_balance
?:
number
auth_source_default_wechat_concurrency
?:
number
auth_source_default_wechat_subscriptions
?:
DefaultSubscriptionSetting
[]
auth_source_default_wechat_grant_on_signup
?:
boolean
auth_source_default_wechat_grant_on_first_bind
?:
boolean
force_email_on_third_party_signup
?:
boolean
default_balance
:
number
;
default_concurrency
:
number
;
default_subscriptions
:
DefaultSubscriptionSetting
[]
;
auth_source_default_email_balance
?:
number
;
auth_source_default_email_concurrency
?:
number
;
auth_source_default_email_subscriptions
?:
DefaultSubscriptionSetting
[]
;
auth_source_default_email_grant_on_signup
?:
boolean
;
auth_source_default_email_grant_on_first_bind
?:
boolean
;
auth_source_default_linuxdo_balance
?:
number
;
auth_source_default_linuxdo_concurrency
?:
number
;
auth_source_default_linuxdo_subscriptions
?:
DefaultSubscriptionSetting
[]
;
auth_source_default_linuxdo_grant_on_signup
?:
boolean
;
auth_source_default_linuxdo_grant_on_first_bind
?:
boolean
;
auth_source_default_oidc_balance
?:
number
;
auth_source_default_oidc_concurrency
?:
number
;
auth_source_default_oidc_subscriptions
?:
DefaultSubscriptionSetting
[]
;
auth_source_default_oidc_grant_on_signup
?:
boolean
;
auth_source_default_oidc_grant_on_first_bind
?:
boolean
;
auth_source_default_wechat_balance
?:
number
;
auth_source_default_wechat_concurrency
?:
number
;
auth_source_default_wechat_subscriptions
?:
DefaultSubscriptionSetting
[]
;
auth_source_default_wechat_grant_on_signup
?:
boolean
;
auth_source_default_wechat_grant_on_first_bind
?:
boolean
;
force_email_on_third_party_signup
?:
boolean
;
// OEM settings
site_name
:
string
site_logo
:
string
site_subtitle
:
string
api_base_url
:
string
contact_info
:
string
doc_url
:
string
home_content
:
string
hide_ccs_import_button
:
boolean
table_default_page_size
:
number
table_page_size_options
:
number
[]
backend_mode_enabled
:
boolean
custom_menu_items
:
CustomMenuItem
[]
custom_endpoints
:
CustomEndpoint
[]
site_name
:
string
;
site_logo
:
string
;
site_subtitle
:
string
;
api_base_url
:
string
;
contact_info
:
string
;
doc_url
:
string
;
home_content
:
string
;
hide_ccs_import_button
:
boolean
;
table_default_page_size
:
number
;
table_page_size_options
:
number
[]
;
backend_mode_enabled
:
boolean
;
custom_menu_items
:
CustomMenuItem
[]
;
custom_endpoints
:
CustomEndpoint
[]
;
// SMTP settings
smtp_host
:
string
smtp_port
:
number
smtp_username
:
string
smtp_password_configured
:
boolean
smtp_from_email
:
string
smtp_from_name
:
string
smtp_use_tls
:
boolean
smtp_host
:
string
;
smtp_port
:
number
;
smtp_username
:
string
;
smtp_password_configured
:
boolean
;
smtp_from_email
:
string
;
smtp_from_name
:
string
;
smtp_use_tls
:
boolean
;
// Cloudflare Turnstile settings
turnstile_enabled
:
boolean
turnstile_site_key
:
string
turnstile_secret_key_configured
:
boolean
turnstile_enabled
:
boolean
;
turnstile_site_key
:
string
;
turnstile_secret_key_configured
:
boolean
;
// LinuxDo Connect OAuth settings
linuxdo_connect_enabled
:
boolean
linuxdo_connect_client_id
:
string
linuxdo_connect_client_secret_configured
:
boolean
linuxdo_connect_redirect_url
:
string
linuxdo_connect_enabled
:
boolean
;
linuxdo_connect_client_id
:
string
;
linuxdo_connect_client_secret_configured
:
boolean
;
linuxdo_connect_redirect_url
:
string
;
// WeChat Connect OAuth settings
wechat_connect_enabled
:
boolean
;
wechat_connect_app_id
:
string
;
wechat_connect_app_secret_configured
:
boolean
;
wechat_connect_mode
:
string
;
wechat_connect_scopes
:
string
;
wechat_connect_redirect_url
:
string
;
wechat_connect_frontend_redirect_url
:
string
;
// Generic OIDC OAuth settings
oidc_connect_enabled
:
boolean
oidc_connect_provider_name
:
string
oidc_connect_client_id
:
string
oidc_connect_client_secret_configured
:
boolean
oidc_connect_issuer_url
:
string
oidc_connect_discovery_url
:
string
oidc_connect_authorize_url
:
string
oidc_connect_token_url
:
string
oidc_connect_userinfo_url
:
string
oidc_connect_jwks_url
:
string
oidc_connect_scopes
:
string
oidc_connect_redirect_url
:
string
oidc_connect_frontend_redirect_url
:
string
oidc_connect_token_auth_method
:
string
oidc_connect_use_pkce
:
boolean
oidc_connect_validate_id_token
:
boolean
oidc_connect_allowed_signing_algs
:
string
oidc_connect_clock_skew_seconds
:
number
oidc_connect_require_email_verified
:
boolean
oidc_connect_userinfo_email_path
:
string
oidc_connect_userinfo_id_path
:
string
oidc_connect_userinfo_username_path
:
string
oidc_connect_enabled
:
boolean
;
oidc_connect_provider_name
:
string
;
oidc_connect_client_id
:
string
;
oidc_connect_client_secret_configured
:
boolean
;
oidc_connect_issuer_url
:
string
;
oidc_connect_discovery_url
:
string
;
oidc_connect_authorize_url
:
string
;
oidc_connect_token_url
:
string
;
oidc_connect_userinfo_url
:
string
;
oidc_connect_jwks_url
:
string
;
oidc_connect_scopes
:
string
;
oidc_connect_redirect_url
:
string
;
oidc_connect_frontend_redirect_url
:
string
;
oidc_connect_token_auth_method
:
string
;
oidc_connect_use_pkce
:
boolean
;
oidc_connect_validate_id_token
:
boolean
;
oidc_connect_allowed_signing_algs
:
string
;
oidc_connect_clock_skew_seconds
:
number
;
oidc_connect_require_email_verified
:
boolean
;
oidc_connect_userinfo_email_path
:
string
;
oidc_connect_userinfo_id_path
:
string
;
oidc_connect_userinfo_username_path
:
string
;
// Model fallback configuration
enable_model_fallback
:
boolean
fallback_model_anthropic
:
string
fallback_model_openai
:
string
fallback_model_gemini
:
string
fallback_model_antigravity
:
string
enable_model_fallback
:
boolean
;
fallback_model_anthropic
:
string
;
fallback_model_openai
:
string
;
fallback_model_gemini
:
string
;
fallback_model_antigravity
:
string
;
// Identity patch configuration (Claude -> Gemini)
enable_identity_patch
:
boolean
identity_patch_prompt
:
string
enable_identity_patch
:
boolean
;
identity_patch_prompt
:
string
;
// Ops Monitoring (vNext)
ops_monitoring_enabled
:
boolean
ops_realtime_monitoring_enabled
:
boolean
ops_query_mode_default
:
'
auto
'
|
'
raw
'
|
'
preagg
'
|
string
ops_metrics_interval_seconds
:
number
ops_monitoring_enabled
:
boolean
;
ops_realtime_monitoring_enabled
:
boolean
;
ops_query_mode_default
:
"
auto
"
|
"
raw
"
|
"
preagg
"
|
string
;
ops_metrics_interval_seconds
:
number
;
// Claude Code version check
min_claude_code_version
:
string
max_claude_code_version
:
string
min_claude_code_version
:
string
;
max_claude_code_version
:
string
;
// 分组隔离
allow_ungrouped_key_scheduling
:
boolean
allow_ungrouped_key_scheduling
:
boolean
;
// Gateway forwarding behavior
enable_fingerprint_unification
:
boolean
enable_metadata_passthrough
:
boolean
enable_cch_signing
:
boolean
web_search_emulation_enabled
?:
boolean
enable_fingerprint_unification
:
boolean
;
enable_metadata_passthrough
:
boolean
;
enable_cch_signing
:
boolean
;
web_search_emulation_enabled
?:
boolean
;
// Payment configuration
payment_enabled
:
boolean
payment_min_amount
:
number
payment_max_amount
:
number
payment_daily_limit
:
number
payment_order_timeout_minutes
:
number
payment_max_pending_orders
:
number
payment_enabled_types
:
string
[]
payment_balance_disabled
:
boolean
payment_balance_recharge_multiplier
:
number
payment_recharge_fee_rate
:
number
payment_load_balance_strategy
:
string
payment_product_name_prefix
:
string
payment_product_name_suffix
:
string
payment_help_image_url
:
string
payment_help_text
:
string
payment_cancel_rate_limit_enabled
:
boolean
payment_cancel_rate_limit_max
:
number
payment_cancel_rate_limit_window
:
number
payment_cancel_rate_limit_unit
:
string
payment_cancel_rate_limit_window_mode
:
string
payment_visible_method_alipay_source
?:
string
payment_visible_method_wxpay_source
?:
string
payment_visible_method_alipay_enabled
?:
boolean
payment_visible_method_wxpay_enabled
?:
boolean
openai_advanced_scheduler_enabled
?:
boolean
payment_enabled
:
boolean
;
payment_min_amount
:
number
;
payment_max_amount
:
number
;
payment_daily_limit
:
number
;
payment_order_timeout_minutes
:
number
;
payment_max_pending_orders
:
number
;
payment_enabled_types
:
string
[]
;
payment_balance_disabled
:
boolean
;
payment_balance_recharge_multiplier
:
number
;
payment_recharge_fee_rate
:
number
;
payment_load_balance_strategy
:
string
;
payment_product_name_prefix
:
string
;
payment_product_name_suffix
:
string
;
payment_help_image_url
:
string
;
payment_help_text
:
string
;
payment_cancel_rate_limit_enabled
:
boolean
;
payment_cancel_rate_limit_max
:
number
;
payment_cancel_rate_limit_window
:
number
;
payment_cancel_rate_limit_unit
:
string
;
payment_cancel_rate_limit_window_mode
:
string
;
payment_visible_method_alipay_source
?:
string
;
payment_visible_method_wxpay_source
?:
string
;
payment_visible_method_alipay_enabled
?:
boolean
;
payment_visible_method_wxpay_enabled
?:
boolean
;
openai_advanced_scheduler_enabled
?:
boolean
;
// Balance & quota notification
balance_low_notify_enabled
:
boolean
balance_low_notify_threshold
:
number
balance_low_notify_recharge_url
:
string
account_quota_notify_enabled
:
boolean
account_quota_notify_emails
:
NotifyEmailEntry
[]
balance_low_notify_enabled
:
boolean
;
balance_low_notify_threshold
:
number
;
balance_low_notify_recharge_url
:
string
;
account_quota_notify_enabled
:
boolean
;
account_quota_notify_emails
:
NotifyEmailEntry
[]
;
}
export
interface
UpdateSettingsRequest
{
registration_enabled
?:
boolean
email_verify_enabled
?:
boolean
registration_email_suffix_whitelist
?:
string
[]
promo_code_enabled
?:
boolean
password_reset_enabled
?:
boolean
frontend_url
?:
string
invitation_code_enabled
?:
boolean
totp_enabled
?:
boolean
// TOTP 双因素认证
default_balance
?:
number
default_concurrency
?:
number
default_subscriptions
?:
DefaultSubscriptionSetting
[]
auth_source_default_email_balance
?:
number
auth_source_default_email_concurrency
?:
number
auth_source_default_email_subscriptions
?:
DefaultSubscriptionSetting
[]
auth_source_default_email_grant_on_signup
?:
boolean
auth_source_default_email_grant_on_first_bind
?:
boolean
auth_source_default_linuxdo_balance
?:
number
auth_source_default_linuxdo_concurrency
?:
number
auth_source_default_linuxdo_subscriptions
?:
DefaultSubscriptionSetting
[]
auth_source_default_linuxdo_grant_on_signup
?:
boolean
auth_source_default_linuxdo_grant_on_first_bind
?:
boolean
auth_source_default_oidc_balance
?:
number
auth_source_default_oidc_concurrency
?:
number
auth_source_default_oidc_subscriptions
?:
DefaultSubscriptionSetting
[]
auth_source_default_oidc_grant_on_signup
?:
boolean
auth_source_default_oidc_grant_on_first_bind
?:
boolean
auth_source_default_wechat_balance
?:
number
auth_source_default_wechat_concurrency
?:
number
auth_source_default_wechat_subscriptions
?:
DefaultSubscriptionSetting
[]
auth_source_default_wechat_grant_on_signup
?:
boolean
auth_source_default_wechat_grant_on_first_bind
?:
boolean
force_email_on_third_party_signup
?:
boolean
site_name
?:
string
site_logo
?:
string
site_subtitle
?:
string
api_base_url
?:
string
contact_info
?:
string
doc_url
?:
string
home_content
?:
string
hide_ccs_import_button
?:
boolean
table_default_page_size
?:
number
table_page_size_options
?:
number
[]
backend_mode_enabled
?:
boolean
custom_menu_items
?:
CustomMenuItem
[]
custom_endpoints
?:
CustomEndpoint
[]
smtp_host
?:
string
smtp_port
?:
number
smtp_username
?:
string
smtp_password
?:
string
smtp_from_email
?:
string
smtp_from_name
?:
string
smtp_use_tls
?:
boolean
turnstile_enabled
?:
boolean
turnstile_site_key
?:
string
turnstile_secret_key
?:
string
linuxdo_connect_enabled
?:
boolean
linuxdo_connect_client_id
?:
string
linuxdo_connect_client_secret
?:
string
linuxdo_connect_redirect_url
?:
string
oidc_connect_enabled
?:
boolean
oidc_connect_provider_name
?:
string
oidc_connect_client_id
?:
string
oidc_connect_client_secret
?:
string
oidc_connect_issuer_url
?:
string
oidc_connect_discovery_url
?:
string
oidc_connect_authorize_url
?:
string
oidc_connect_token_url
?:
string
oidc_connect_userinfo_url
?:
string
oidc_connect_jwks_url
?:
string
oidc_connect_scopes
?:
string
oidc_connect_redirect_url
?:
string
oidc_connect_frontend_redirect_url
?:
string
oidc_connect_token_auth_method
?:
string
oidc_connect_use_pkce
?:
boolean
oidc_connect_validate_id_token
?:
boolean
oidc_connect_allowed_signing_algs
?:
string
oidc_connect_clock_skew_seconds
?:
number
oidc_connect_require_email_verified
?:
boolean
oidc_connect_userinfo_email_path
?:
string
oidc_connect_userinfo_id_path
?:
string
oidc_connect_userinfo_username_path
?:
string
enable_model_fallback
?:
boolean
fallback_model_anthropic
?:
string
fallback_model_openai
?:
string
fallback_model_gemini
?:
string
fallback_model_antigravity
?:
string
enable_identity_patch
?:
boolean
identity_patch_prompt
?:
string
ops_monitoring_enabled
?:
boolean
ops_realtime_monitoring_enabled
?:
boolean
ops_query_mode_default
?:
'
auto
'
|
'
raw
'
|
'
preagg
'
|
string
ops_metrics_interval_seconds
?:
number
min_claude_code_version
?:
string
max_claude_code_version
?:
string
allow_ungrouped_key_scheduling
?:
boolean
enable_fingerprint_unification
?:
boolean
enable_metadata_passthrough
?:
boolean
enable_cch_signing
?:
boolean
registration_enabled
?:
boolean
;
email_verify_enabled
?:
boolean
;
registration_email_suffix_whitelist
?:
string
[];
promo_code_enabled
?:
boolean
;
password_reset_enabled
?:
boolean
;
frontend_url
?:
string
;
invitation_code_enabled
?:
boolean
;
totp_enabled
?:
boolean
;
// TOTP 双因素认证
default_balance
?:
number
;
default_concurrency
?:
number
;
default_subscriptions
?:
DefaultSubscriptionSetting
[];
auth_source_default_email_balance
?:
number
;
auth_source_default_email_concurrency
?:
number
;
auth_source_default_email_subscriptions
?:
DefaultSubscriptionSetting
[];
auth_source_default_email_grant_on_signup
?:
boolean
;
auth_source_default_email_grant_on_first_bind
?:
boolean
;
auth_source_default_linuxdo_balance
?:
number
;
auth_source_default_linuxdo_concurrency
?:
number
;
auth_source_default_linuxdo_subscriptions
?:
DefaultSubscriptionSetting
[];
auth_source_default_linuxdo_grant_on_signup
?:
boolean
;
auth_source_default_linuxdo_grant_on_first_bind
?:
boolean
;
auth_source_default_oidc_balance
?:
number
;
auth_source_default_oidc_concurrency
?:
number
;
auth_source_default_oidc_subscriptions
?:
DefaultSubscriptionSetting
[];
auth_source_default_oidc_grant_on_signup
?:
boolean
;
auth_source_default_oidc_grant_on_first_bind
?:
boolean
;
auth_source_default_wechat_balance
?:
number
;
auth_source_default_wechat_concurrency
?:
number
;
auth_source_default_wechat_subscriptions
?:
DefaultSubscriptionSetting
[];
auth_source_default_wechat_grant_on_signup
?:
boolean
;
auth_source_default_wechat_grant_on_first_bind
?:
boolean
;
force_email_on_third_party_signup
?:
boolean
;
site_name
?:
string
;
site_logo
?:
string
;
site_subtitle
?:
string
;
api_base_url
?:
string
;
contact_info
?:
string
;
doc_url
?:
string
;
home_content
?:
string
;
hide_ccs_import_button
?:
boolean
;
table_default_page_size
?:
number
;
table_page_size_options
?:
number
[];
backend_mode_enabled
?:
boolean
;
custom_menu_items
?:
CustomMenuItem
[];
custom_endpoints
?:
CustomEndpoint
[];
smtp_host
?:
string
;
smtp_port
?:
number
;
smtp_username
?:
string
;
smtp_password
?:
string
;
smtp_from_email
?:
string
;
smtp_from_name
?:
string
;
smtp_use_tls
?:
boolean
;
turnstile_enabled
?:
boolean
;
turnstile_site_key
?:
string
;
turnstile_secret_key
?:
string
;
linuxdo_connect_enabled
?:
boolean
;
linuxdo_connect_client_id
?:
string
;
linuxdo_connect_client_secret
?:
string
;
linuxdo_connect_redirect_url
?:
string
;
wechat_connect_enabled
?:
boolean
;
wechat_connect_app_id
?:
string
;
wechat_connect_app_secret
?:
string
;
wechat_connect_mode
?:
string
;
wechat_connect_scopes
?:
string
;
wechat_connect_redirect_url
?:
string
;
wechat_connect_frontend_redirect_url
?:
string
;
oidc_connect_enabled
?:
boolean
;
oidc_connect_provider_name
?:
string
;
oidc_connect_client_id
?:
string
;
oidc_connect_client_secret
?:
string
;
oidc_connect_issuer_url
?:
string
;
oidc_connect_discovery_url
?:
string
;
oidc_connect_authorize_url
?:
string
;
oidc_connect_token_url
?:
string
;
oidc_connect_userinfo_url
?:
string
;
oidc_connect_jwks_url
?:
string
;
oidc_connect_scopes
?:
string
;
oidc_connect_redirect_url
?:
string
;
oidc_connect_frontend_redirect_url
?:
string
;
oidc_connect_token_auth_method
?:
string
;
oidc_connect_use_pkce
?:
boolean
;
oidc_connect_validate_id_token
?:
boolean
;
oidc_connect_allowed_signing_algs
?:
string
;
oidc_connect_clock_skew_seconds
?:
number
;
oidc_connect_require_email_verified
?:
boolean
;
oidc_connect_userinfo_email_path
?:
string
;
oidc_connect_userinfo_id_path
?:
string
;
oidc_connect_userinfo_username_path
?:
string
;
enable_model_fallback
?:
boolean
;
fallback_model_anthropic
?:
string
;
fallback_model_openai
?:
string
;
fallback_model_gemini
?:
string
;
fallback_model_antigravity
?:
string
;
enable_identity_patch
?:
boolean
;
identity_patch_prompt
?:
string
;
ops_monitoring_enabled
?:
boolean
;
ops_realtime_monitoring_enabled
?:
boolean
;
ops_query_mode_default
?:
"
auto
"
|
"
raw
"
|
"
preagg
"
|
string
;
ops_metrics_interval_seconds
?:
number
;
min_claude_code_version
?:
string
;
max_claude_code_version
?:
string
;
allow_ungrouped_key_scheduling
?:
boolean
;
enable_fingerprint_unification
?:
boolean
;
enable_metadata_passthrough
?:
boolean
;
enable_cch_signing
?:
boolean
;
// Payment configuration
payment_enabled
?:
boolean
payment_min_amount
?:
number
payment_max_amount
?:
number
payment_daily_limit
?:
number
payment_order_timeout_minutes
?:
number
payment_max_pending_orders
?:
number
payment_enabled_types
?:
string
[]
payment_balance_disabled
?:
boolean
payment_balance_recharge_multiplier
?:
number
payment_recharge_fee_rate
?:
number
payment_load_balance_strategy
?:
string
payment_product_name_prefix
?:
string
payment_product_name_suffix
?:
string
payment_help_image_url
?:
string
payment_help_text
?:
string
payment_cancel_rate_limit_enabled
?:
boolean
payment_cancel_rate_limit_max
?:
number
payment_cancel_rate_limit_window
?:
number
payment_cancel_rate_limit_unit
?:
string
payment_cancel_rate_limit_window_mode
?:
string
payment_visible_method_alipay_source
?:
string
payment_visible_method_wxpay_source
?:
string
payment_visible_method_alipay_enabled
?:
boolean
payment_visible_method_wxpay_enabled
?:
boolean
openai_advanced_scheduler_enabled
?:
boolean
payment_enabled
?:
boolean
;
payment_min_amount
?:
number
;
payment_max_amount
?:
number
;
payment_daily_limit
?:
number
;
payment_order_timeout_minutes
?:
number
;
payment_max_pending_orders
?:
number
;
payment_enabled_types
?:
string
[]
;
payment_balance_disabled
?:
boolean
;
payment_balance_recharge_multiplier
?:
number
;
payment_recharge_fee_rate
?:
number
;
payment_load_balance_strategy
?:
string
;
payment_product_name_prefix
?:
string
;
payment_product_name_suffix
?:
string
;
payment_help_image_url
?:
string
;
payment_help_text
?:
string
;
payment_cancel_rate_limit_enabled
?:
boolean
;
payment_cancel_rate_limit_max
?:
number
;
payment_cancel_rate_limit_window
?:
number
;
payment_cancel_rate_limit_unit
?:
string
;
payment_cancel_rate_limit_window_mode
?:
string
;
payment_visible_method_alipay_source
?:
string
;
payment_visible_method_wxpay_source
?:
string
;
payment_visible_method_alipay_enabled
?:
boolean
;
payment_visible_method_wxpay_enabled
?:
boolean
;
openai_advanced_scheduler_enabled
?:
boolean
;
// Balance & quota notification
balance_low_notify_enabled
?:
boolean
balance_low_notify_threshold
?:
number
balance_low_notify_recharge_url
?:
string
account_quota_notify_enabled
?:
boolean
account_quota_notify_emails
?:
NotifyEmailEntry
[]
balance_low_notify_enabled
?:
boolean
;
balance_low_notify_threshold
?:
number
;
balance_low_notify_recharge_url
?:
string
;
account_quota_notify_enabled
?:
boolean
;
account_quota_notify_emails
?:
NotifyEmailEntry
[]
;
}
/**
...
...
@@ -453,8 +554,8 @@ export interface UpdateSettingsRequest {
* @returns System settings
*/
export
async
function
getSettings
():
Promise
<
SystemSettings
>
{
const
{
data
}
=
await
apiClient
.
get
<
SystemSettings
>
(
'
/admin/settings
'
)
return
data
const
{
data
}
=
await
apiClient
.
get
<
SystemSettings
>
(
"
/admin/settings
"
);
return
data
;
}
/**
...
...
@@ -462,20 +563,25 @@ export async function getSettings(): Promise<SystemSettings> {
* @param settings - Partial settings to update
* @returns Updated settings
*/
export
async
function
updateSettings
(
settings
:
UpdateSettingsRequest
):
Promise
<
SystemSettings
>
{
const
{
data
}
=
await
apiClient
.
put
<
SystemSettings
>
(
'
/admin/settings
'
,
settings
)
return
data
export
async
function
updateSettings
(
settings
:
UpdateSettingsRequest
,
):
Promise
<
SystemSettings
>
{
const
{
data
}
=
await
apiClient
.
put
<
SystemSettings
>
(
"
/admin/settings
"
,
settings
,
);
return
data
;
}
/**
* Test SMTP connection request
*/
export
interface
TestSmtpRequest
{
smtp_host
:
string
smtp_port
:
number
smtp_username
:
string
smtp_password
:
string
smtp_use_tls
:
boolean
smtp_host
:
string
;
smtp_port
:
number
;
smtp_username
:
string
;
smtp_password
:
string
;
smtp_use_tls
:
boolean
;
}
/**
...
...
@@ -483,23 +589,28 @@ export interface TestSmtpRequest {
* @param config - SMTP configuration to test
* @returns Test result message
*/
export
async
function
testSmtpConnection
(
config
:
TestSmtpRequest
):
Promise
<
{
message
:
string
}
>
{
const
{
data
}
=
await
apiClient
.
post
<
{
message
:
string
}
>
(
'
/admin/settings/test-smtp
'
,
config
)
return
data
export
async
function
testSmtpConnection
(
config
:
TestSmtpRequest
,
):
Promise
<
{
message
:
string
}
>
{
const
{
data
}
=
await
apiClient
.
post
<
{
message
:
string
}
>
(
"
/admin/settings/test-smtp
"
,
config
,
);
return
data
;
}
/**
* Send test email request
*/
export
interface
SendTestEmailRequest
{
email
:
string
smtp_host
:
string
smtp_port
:
number
smtp_username
:
string
smtp_password
:
string
smtp_from_email
:
string
smtp_from_name
:
string
smtp_use_tls
:
boolean
email
:
string
;
smtp_host
:
string
;
smtp_port
:
number
;
smtp_username
:
string
;
smtp_password
:
string
;
smtp_from_email
:
string
;
smtp_from_name
:
string
;
smtp_use_tls
:
boolean
;
}
/**
...
...
@@ -507,20 +618,22 @@ export interface SendTestEmailRequest {
* @param request - Email address and SMTP config
* @returns Test result message
*/
export
async
function
sendTestEmail
(
request
:
SendTestEmailRequest
):
Promise
<
{
message
:
string
}
>
{
export
async
function
sendTestEmail
(
request
:
SendTestEmailRequest
,
):
Promise
<
{
message
:
string
}
>
{
const
{
data
}
=
await
apiClient
.
post
<
{
message
:
string
}
>
(
'
/admin/settings/send-test-email
'
,
request
)
return
data
"
/admin/settings/send-test-email
"
,
request
,
)
;
return
data
;
}
/**
* Admin API Key status response
*/
export
interface
AdminApiKeyStatus
{
exists
:
boolean
masked_key
:
string
exists
:
boolean
;
masked_key
:
string
;
}
/**
...
...
@@ -528,8 +641,10 @@ export interface AdminApiKeyStatus {
* @returns Status indicating if key exists and masked version
*/
export
async
function
getAdminApiKey
():
Promise
<
AdminApiKeyStatus
>
{
const
{
data
}
=
await
apiClient
.
get
<
AdminApiKeyStatus
>
(
'
/admin/settings/admin-api-key
'
)
return
data
const
{
data
}
=
await
apiClient
.
get
<
AdminApiKeyStatus
>
(
"
/admin/settings/admin-api-key
"
,
);
return
data
;
}
/**
...
...
@@ -537,8 +652,10 @@ export async function getAdminApiKey(): Promise<AdminApiKeyStatus> {
* @returns The new full API key (only shown once)
*/
export
async
function
regenerateAdminApiKey
():
Promise
<
{
key
:
string
}
>
{
const
{
data
}
=
await
apiClient
.
post
<
{
key
:
string
}
>
(
'
/admin/settings/admin-api-key/regenerate
'
)
return
data
const
{
data
}
=
await
apiClient
.
post
<
{
key
:
string
}
>
(
"
/admin/settings/admin-api-key/regenerate
"
,
);
return
data
;
}
/**
...
...
@@ -546,8 +663,10 @@ export async function regenerateAdminApiKey(): Promise<{ key: string }> {
* @returns Success message
*/
export
async
function
deleteAdminApiKey
():
Promise
<
{
message
:
string
}
>
{
const
{
data
}
=
await
apiClient
.
delete
<
{
message
:
string
}
>
(
'
/admin/settings/admin-api-key
'
)
return
data
const
{
data
}
=
await
apiClient
.
delete
<
{
message
:
string
}
>
(
"
/admin/settings/admin-api-key
"
,
);
return
data
;
}
// ==================== Overload Cooldown Settings ====================
...
...
@@ -556,23 +675,25 @@ export async function deleteAdminApiKey(): Promise<{ message: string }> {
* Overload cooldown settings interface (529 handling)
*/
export
interface
OverloadCooldownSettings
{
enabled
:
boolean
cooldown_minutes
:
number
enabled
:
boolean
;
cooldown_minutes
:
number
;
}
export
async
function
getOverloadCooldownSettings
():
Promise
<
OverloadCooldownSettings
>
{
const
{
data
}
=
await
apiClient
.
get
<
OverloadCooldownSettings
>
(
'
/admin/settings/overload-cooldown
'
)
return
data
const
{
data
}
=
await
apiClient
.
get
<
OverloadCooldownSettings
>
(
"
/admin/settings/overload-cooldown
"
,
);
return
data
;
}
export
async
function
updateOverloadCooldownSettings
(
settings
:
OverloadCooldownSettings
settings
:
OverloadCooldownSettings
,
):
Promise
<
OverloadCooldownSettings
>
{
const
{
data
}
=
await
apiClient
.
put
<
OverloadCooldownSettings
>
(
'
/admin/settings/overload-cooldown
'
,
settings
)
return
data
"
/admin/settings/overload-cooldown
"
,
settings
,
)
;
return
data
;
}
// ==================== Stream Timeout Settings ====================
...
...
@@ -581,11 +702,11 @@ export async function updateOverloadCooldownSettings(
* Stream timeout settings interface
*/
export
interface
StreamTimeoutSettings
{
enabled
:
boolean
action
:
'
temp_unsched
'
|
'
error
'
|
'
none
'
temp_unsched_minutes
:
number
threshold_count
:
number
threshold_window_minutes
:
number
enabled
:
boolean
;
action
:
"
temp_unsched
"
|
"
error
"
|
"
none
"
;
temp_unsched_minutes
:
number
;
threshold_count
:
number
;
threshold_window_minutes
:
number
;
}
/**
...
...
@@ -593,8 +714,10 @@ export interface StreamTimeoutSettings {
* @returns Stream timeout settings
*/
export
async
function
getStreamTimeoutSettings
():
Promise
<
StreamTimeoutSettings
>
{
const
{
data
}
=
await
apiClient
.
get
<
StreamTimeoutSettings
>
(
'
/admin/settings/stream-timeout
'
)
return
data
const
{
data
}
=
await
apiClient
.
get
<
StreamTimeoutSettings
>
(
"
/admin/settings/stream-timeout
"
,
);
return
data
;
}
/**
...
...
@@ -603,13 +726,13 @@ export async function getStreamTimeoutSettings(): Promise<StreamTimeoutSettings>
* @returns Updated settings
*/
export
async
function
updateStreamTimeoutSettings
(
settings
:
StreamTimeoutSettings
settings
:
StreamTimeoutSettings
,
):
Promise
<
StreamTimeoutSettings
>
{
const
{
data
}
=
await
apiClient
.
put
<
StreamTimeoutSettings
>
(
'
/admin/settings/stream-timeout
'
,
settings
)
return
data
"
/admin/settings/stream-timeout
"
,
settings
,
)
;
return
data
;
}
// ==================== Rectifier Settings ====================
...
...
@@ -618,11 +741,11 @@ export async function updateStreamTimeoutSettings(
* Rectifier settings interface
*/
export
interface
RectifierSettings
{
enabled
:
boolean
thinking_signature_enabled
:
boolean
thinking_budget_enabled
:
boolean
apikey_signature_enabled
:
boolean
apikey_signature_patterns
:
string
[]
enabled
:
boolean
;
thinking_signature_enabled
:
boolean
;
thinking_budget_enabled
:
boolean
;
apikey_signature_enabled
:
boolean
;
apikey_signature_patterns
:
string
[]
;
}
/**
...
...
@@ -630,8 +753,10 @@ export interface RectifierSettings {
* @returns Rectifier settings
*/
export
async
function
getRectifierSettings
():
Promise
<
RectifierSettings
>
{
const
{
data
}
=
await
apiClient
.
get
<
RectifierSettings
>
(
'
/admin/settings/rectifier
'
)
return
data
const
{
data
}
=
await
apiClient
.
get
<
RectifierSettings
>
(
"
/admin/settings/rectifier
"
,
);
return
data
;
}
/**
...
...
@@ -640,13 +765,13 @@ export async function getRectifierSettings(): Promise<RectifierSettings> {
* @returns Updated settings
*/
export
async
function
updateRectifierSettings
(
settings
:
RectifierSettings
settings
:
RectifierSettings
,
):
Promise
<
RectifierSettings
>
{
const
{
data
}
=
await
apiClient
.
put
<
RectifierSettings
>
(
'
/admin/settings/rectifier
'
,
settings
)
return
data
"
/admin/settings/rectifier
"
,
settings
,
)
;
return
data
;
}
// ==================== Beta Policy Settings ====================
...
...
@@ -655,20 +780,20 @@ export async function updateRectifierSettings(
* Beta policy rule interface
*/
export
interface
BetaPolicyRule
{
beta_token
:
string
action
:
'
pass
'
|
'
filter
'
|
'
block
'
scope
:
'
all
'
|
'
oauth
'
|
'
apikey
'
|
'
bedrock
'
error_message
?:
string
model_whitelist
?:
string
[]
fallback_action
?:
'
pass
'
|
'
filter
'
|
'
block
'
fallback_error_message
?:
string
beta_token
:
string
;
action
:
"
pass
"
|
"
filter
"
|
"
block
"
;
scope
:
"
all
"
|
"
oauth
"
|
"
apikey
"
|
"
bedrock
"
;
error_message
?:
string
;
model_whitelist
?:
string
[]
;
fallback_action
?:
"
pass
"
|
"
filter
"
|
"
block
"
;
fallback_error_message
?:
string
;
}
/**
* Beta policy settings interface
*/
export
interface
BetaPolicySettings
{
rules
:
BetaPolicyRule
[]
rules
:
BetaPolicyRule
[]
;
}
/**
...
...
@@ -676,8 +801,10 @@ export interface BetaPolicySettings {
* @returns Beta policy settings
*/
export
async
function
getBetaPolicySettings
():
Promise
<
BetaPolicySettings
>
{
const
{
data
}
=
await
apiClient
.
get
<
BetaPolicySettings
>
(
'
/admin/settings/beta-policy
'
)
return
data
const
{
data
}
=
await
apiClient
.
get
<
BetaPolicySettings
>
(
"
/admin/settings/beta-policy
"
,
);
return
data
;
}
/**
...
...
@@ -686,70 +813,73 @@ export async function getBetaPolicySettings(): Promise<BetaPolicySettings> {
* @returns Updated settings
*/
export
async
function
updateBetaPolicySettings
(
settings
:
BetaPolicySettings
settings
:
BetaPolicySettings
,
):
Promise
<
BetaPolicySettings
>
{
const
{
data
}
=
await
apiClient
.
put
<
BetaPolicySettings
>
(
'
/admin/settings/beta-policy
'
,
settings
)
return
data
"
/admin/settings/beta-policy
"
,
settings
,
)
;
return
data
;
}
// --- Web Search Emulation Config ---
export
interface
WebSearchProviderConfig
{
type
:
'
brave
'
|
'
tavily
'
api_key
:
string
api_key_configured
:
boolean
quota_limit
:
number
|
null
subscribed_at
:
number
|
null
quota_used
?:
number
proxy_id
:
number
|
null
expires_at
:
number
|
null
type
:
"
brave
"
|
"
tavily
"
;
api_key
:
string
;
api_key_configured
:
boolean
;
quota_limit
:
number
|
null
;
subscribed_at
:
number
|
null
;
quota_used
?:
number
;
proxy_id
:
number
|
null
;
expires_at
:
number
|
null
;
}
export
interface
WebSearchEmulationConfig
{
enabled
:
boolean
providers
:
WebSearchProviderConfig
[]
enabled
:
boolean
;
providers
:
WebSearchProviderConfig
[]
;
}
export
interface
WebSearchTestResult
{
provider
:
string
results
:
{
url
:
string
;
title
:
string
;
snippet
:
string
;
page_age
?:
string
}[]
query
:
string
provider
:
string
;
results
:
{
url
:
string
;
title
:
string
;
snippet
:
string
;
page_age
?:
string
}[]
;
query
:
string
;
}
export
async
function
getWebSearchEmulationConfig
():
Promise
<
WebSearchEmulationConfig
>
{
const
{
data
}
=
await
apiClient
.
get
<
WebSearchEmulationConfig
>
(
'
/admin/settings/web-search-emulation
'
)
return
data
"
/admin/settings/web-search-emulation
"
,
)
;
return
data
;
}
export
async
function
updateWebSearchEmulationConfig
(
config
:
WebSearchEmulationConfig
config
:
WebSearchEmulationConfig
,
):
Promise
<
WebSearchEmulationConfig
>
{
const
{
data
}
=
await
apiClient
.
put
<
WebSearchEmulationConfig
>
(
'
/admin/settings/web-search-emulation
'
,
config
)
return
data
"
/admin/settings/web-search-emulation
"
,
config
,
)
;
return
data
;
}
export
async
function
testWebSearchEmulation
(
query
:
string
query
:
string
,
):
Promise
<
WebSearchTestResult
>
{
const
{
data
}
=
await
apiClient
.
post
<
WebSearchTestResult
>
(
'
/admin/settings/web-search-emulation/test
'
,
{
query
}
)
return
data
"
/admin/settings/web-search-emulation/test
"
,
{
query
}
,
)
;
return
data
;
}
export
async
function
resetWebSearchUsage
(
payload
:
{
provider_type
:
string
}
):
Promise
<
void
>
{
await
apiClient
.
post
(
'
/admin/settings/web-search-emulation/reset-usage
'
,
payload
)
export
async
function
resetWebSearchUsage
(
payload
:
{
provider_type
:
string
;
}):
Promise
<
void
>
{
await
apiClient
.
post
(
"
/admin/settings/web-search-emulation/reset-usage
"
,
payload
,
);
}
export
const
settingsAPI
=
{
...
...
@@ -771,7 +901,7 @@ export const settingsAPI = {
getWebSearchEmulationConfig
,
updateWebSearchEmulationConfig
,
testWebSearchEmulation
,
resetWebSearchUsage
}
resetWebSearchUsage
,
}
;
export
default
settingsAPI
export
default
settingsAPI
;
frontend/src/views/admin/SettingsView.vue
View file @
ee3f158f
This source diff could not be displayed because it is too large. You can
view the blob
instead.
frontend/src/views/admin/__tests__/SettingsView.spec.ts
View file @
ee3f158f
import
{
beforeEach
,
describe
,
expect
,
it
,
vi
}
from
'
vitest
'
import
{
defineComponent
,
h
,
ref
}
from
'
vue
'
import
{
flushPromises
,
mount
}
from
'
@vue/test-utils
'
import
{
beforeEach
,
describe
,
expect
,
it
,
vi
}
from
"
vitest
"
;
import
{
defineComponent
,
h
,
ref
}
from
"
vue
"
;
import
{
flushPromises
,
mount
}
from
"
@vue/test-utils
"
;
import
SettingsView
from
'
../SettingsView.vue
'
import
SettingsView
from
"
../SettingsView.vue
"
;
const
{
getSettings
,
...
...
@@ -38,9 +38,9 @@ const {
adminSettingsFetch
:
vi
.
fn
(),
showError
:
vi
.
fn
(),
showSuccess
:
vi
.
fn
(),
}))
}))
;
vi
.
mock
(
'
@/api
'
,
()
=>
({
vi
.
mock
(
"
@/api
"
,
()
=>
({
adminAPI
:
{
settings
:
{
getSettings
,
...
...
@@ -63,9 +63,9 @@ vi.mock('@/api', () => ({
getProviders
,
},
},
}))
}))
;
vi
.
mock
(
'
@/stores
'
,
()
=>
({
vi
.
mock
(
"
@/stores
"
,
()
=>
({
useAppStore
:
()
=>
({
showError
,
showSuccess
,
...
...
@@ -73,36 +73,36 @@ vi.mock('@/stores', () => ({
showInfo
:
vi
.
fn
(),
fetchPublicSettings
,
}),
}))
}))
;
vi
.
mock
(
'
@/stores/adminSettings
'
,
()
=>
({
vi
.
mock
(
"
@/stores/adminSettings
"
,
()
=>
({
useAdminSettingsStore
:
()
=>
({
fetch
:
adminSettingsFetch
,
}),
}))
}))
;
vi
.
mock
(
'
@/composables/useClipboard
'
,
()
=>
({
vi
.
mock
(
"
@/composables/useClipboard
"
,
()
=>
({
useClipboard
:
()
=>
({
copyToClipboard
:
vi
.
fn
(),
}),
}))
}))
;
vi
.
mock
(
'
@/utils/apiError
'
,
()
=>
({
extractApiErrorMessage
:
()
=>
'
error
'
,
}))
vi
.
mock
(
"
@/utils/apiError
"
,
()
=>
({
extractApiErrorMessage
:
()
=>
"
error
"
,
}))
;
vi
.
mock
(
'
vue-i18n
'
,
async
()
=>
{
const
actual
=
await
vi
.
importActual
<
typeof
import
(
'
vue-i18n
'
)
>
(
'
vue-i18n
'
)
vi
.
mock
(
"
vue-i18n
"
,
async
()
=>
{
const
actual
=
await
vi
.
importActual
<
typeof
import
(
"
vue-i18n
"
)
>
(
"
vue-i18n
"
);
return
{
...
actual
,
useI18n
:
()
=>
({
t
:
(
key
:
string
)
=>
key
,
locale
:
ref
(
'
zh-CN
'
),
locale
:
ref
(
"
zh-CN
"
),
}),
}
})
}
;
})
;
const
AppLayoutStub
=
{
template
:
'
<div><slot /></div>
'
}
const
AppLayoutStub
=
{
template
:
"
<div><slot /></div>
"
}
;
const
ToggleStub
=
defineComponent
({
props
:
{
modelValue
:
{
...
...
@@ -110,25 +110,25 @@ const ToggleStub = defineComponent({
default
:
false
,
},
},
emits
:
[
'
update:modelValue
'
],
emits
:
[
"
update:modelValue
"
],
setup
(
props
,
{
emit
})
{
return
()
=>
h
(
'
input
'
,
{
class
:
'
toggle-stub
'
,
type
:
'
checkbox
'
,
h
(
"
input
"
,
{
class
:
"
toggle-stub
"
,
type
:
"
checkbox
"
,
checked
:
props
.
modelValue
,
onChange
:
(
event
:
Event
)
=>
{
emit
(
'
update:modelValue
'
,
(
event
.
target
as
HTMLInputElement
).
checked
)
emit
(
"
update:modelValue
"
,
(
event
.
target
as
HTMLInputElement
).
checked
)
;
},
})
})
;
},
})
})
;
const
SelectStub
=
defineComponent
({
props
:
{
modelValue
:
{
type
:
[
String
,
Number
,
Boolean
,
null
],
default
:
''
,
default
:
""
,
},
options
:
{
type
:
Array
,
...
...
@@ -136,42 +136,43 @@ const SelectStub = defineComponent({
},
placeholder
:
{
type
:
String
,
default
:
''
,
default
:
""
,
},
},
emits
:
[
'
update:modelValue
'
,
'
change
'
],
emits
:
[
"
update:modelValue
"
,
"
change
"
],
setup
(
props
,
{
emit
})
{
const
onChange
=
(
event
:
Event
)
=>
{
const
target
=
event
.
target
as
HTMLSelectElement
emit
(
'
update:modelValue
'
,
target
.
value
)
const
option
=
(
props
.
options
as
Array
<
Record
<
string
,
unknown
>>
).
find
(
(
item
)
=>
String
(
item
.
value
??
''
)
===
target
.
value
)
??
null
emit
(
'
change
'
,
target
.
value
,
option
)
}
const
target
=
event
.
target
as
HTMLSelectElement
;
emit
(
"
update:modelValue
"
,
target
.
value
);
const
option
=
(
props
.
options
as
Array
<
Record
<
string
,
unknown
>>
).
find
(
(
item
)
=>
String
(
item
.
value
??
""
)
===
target
.
value
,
)
??
null
;
emit
(
"
change
"
,
target
.
value
,
option
);
};
return
()
=>
h
(
'
select
'
,
"
select
"
,
{
class
:
'
select-stub
'
,
value
:
props
.
modelValue
??
''
,
'
data-placeholder
'
:
props
.
placeholder
,
class
:
"
select-stub
"
,
value
:
props
.
modelValue
??
""
,
"
data-placeholder
"
:
props
.
placeholder
,
onChange
,
},
(
props
.
options
as
Array
<
Record
<
string
,
unknown
>>
).
map
((
option
)
=>
h
(
'
option
'
,
"
option
"
,
{
key
:
`
${
String
(
option
.
value
??
''
)}
:
${
String
(
option
.
label
??
''
)}
`
,
key
:
`
${
String
(
option
.
value
??
""
)}
:
${
String
(
option
.
label
??
""
)}
`
,
value
:
option
.
value
as
string
,
},
String
(
option
.
label
??
''
)
)
)
)
String
(
option
.
label
??
""
),
)
,
)
,
)
;
},
})
})
;
const
baseSettingsResponse
=
{
registration_enabled
:
true
,
...
...
@@ -185,69 +186,77 @@ const baseSettingsResponse = {
default_balance
:
0
,
default_concurrency
:
1
,
default_subscriptions
:
[],
site_name
:
'
Sub2API
'
,
site_logo
:
''
,
site_subtitle
:
''
,
api_base_url
:
''
,
contact_info
:
''
,
doc_url
:
''
,
home_content
:
''
,
site_name
:
"
Sub2API
"
,
site_logo
:
""
,
site_subtitle
:
""
,
api_base_url
:
""
,
contact_info
:
""
,
doc_url
:
""
,
home_content
:
""
,
hide_ccs_import_button
:
false
,
table_default_page_size
:
20
,
table_page_size_options
:
[
10
,
20
,
50
,
100
],
backend_mode_enabled
:
false
,
custom_menu_items
:
[],
custom_endpoints
:
[],
frontend_url
:
''
,
smtp_host
:
''
,
frontend_url
:
""
,
smtp_host
:
""
,
smtp_port
:
587
,
smtp_username
:
''
,
smtp_username
:
""
,
smtp_password_configured
:
false
,
smtp_from_email
:
''
,
smtp_from_name
:
''
,
smtp_from_email
:
""
,
smtp_from_name
:
""
,
smtp_use_tls
:
true
,
turnstile_enabled
:
false
,
turnstile_site_key
:
''
,
turnstile_site_key
:
""
,
turnstile_secret_key_configured
:
false
,
linuxdo_connect_enabled
:
false
,
linuxdo_connect_client_id
:
''
,
linuxdo_connect_client_id
:
""
,
linuxdo_connect_client_secret_configured
:
false
,
linuxdo_connect_redirect_url
:
''
,
linuxdo_connect_redirect_url
:
""
,
wechat_connect_enabled
:
true
,
wechat_connect_app_id
:
"
wx-app-id-123
"
,
wechat_connect_app_secret_configured
:
true
,
wechat_connect_mode
:
"
mp
"
,
wechat_connect_scopes
:
""
,
wechat_connect_redirect_url
:
"
https://admin.example.com/api/v1/auth/oauth/wechat/callback
"
,
wechat_connect_frontend_redirect_url
:
"
/auth/wechat/callback
"
,
oidc_connect_enabled
:
false
,
oidc_connect_provider_name
:
'
OIDC
'
,
oidc_connect_client_id
:
''
,
oidc_connect_provider_name
:
"
OIDC
"
,
oidc_connect_client_id
:
""
,
oidc_connect_client_secret_configured
:
false
,
oidc_connect_issuer_url
:
''
,
oidc_connect_discovery_url
:
''
,
oidc_connect_authorize_url
:
''
,
oidc_connect_token_url
:
''
,
oidc_connect_userinfo_url
:
''
,
oidc_connect_jwks_url
:
''
,
oidc_connect_scopes
:
'
openid email profile
'
,
oidc_connect_redirect_url
:
''
,
oidc_connect_frontend_redirect_url
:
'
/auth/oidc/callback
'
,
oidc_connect_token_auth_method
:
'
client_secret_post
'
,
oidc_connect_issuer_url
:
""
,
oidc_connect_discovery_url
:
""
,
oidc_connect_authorize_url
:
""
,
oidc_connect_token_url
:
""
,
oidc_connect_userinfo_url
:
""
,
oidc_connect_jwks_url
:
""
,
oidc_connect_scopes
:
"
openid email profile
"
,
oidc_connect_redirect_url
:
""
,
oidc_connect_frontend_redirect_url
:
"
/auth/oidc/callback
"
,
oidc_connect_token_auth_method
:
"
client_secret_post
"
,
oidc_connect_use_pkce
:
true
,
oidc_connect_validate_id_token
:
true
,
oidc_connect_allowed_signing_algs
:
'
RS256,ES256,PS256
'
,
oidc_connect_allowed_signing_algs
:
"
RS256,ES256,PS256
"
,
oidc_connect_clock_skew_seconds
:
120
,
oidc_connect_require_email_verified
:
false
,
oidc_connect_userinfo_email_path
:
''
,
oidc_connect_userinfo_id_path
:
''
,
oidc_connect_userinfo_username_path
:
''
,
oidc_connect_userinfo_email_path
:
""
,
oidc_connect_userinfo_id_path
:
""
,
oidc_connect_userinfo_username_path
:
""
,
enable_model_fallback
:
false
,
fallback_model_anthropic
:
''
,
fallback_model_openai
:
''
,
fallback_model_gemini
:
''
,
fallback_model_antigravity
:
''
,
fallback_model_anthropic
:
""
,
fallback_model_openai
:
""
,
fallback_model_gemini
:
""
,
fallback_model_antigravity
:
""
,
enable_identity_patch
:
false
,
identity_patch_prompt
:
''
,
identity_patch_prompt
:
""
,
ops_monitoring_enabled
:
false
,
ops_realtime_monitoring_enabled
:
false
,
ops_query_mode_default
:
'
auto
'
,
ops_query_mode_default
:
"
auto
"
,
ops_metrics_interval_seconds
:
60
,
min_claude_code_version
:
''
,
max_claude_code_version
:
''
,
min_claude_code_version
:
""
,
max_claude_code_version
:
""
,
allow_ungrouped_key_scheduling
:
false
,
enable_fingerprint_unification
:
true
,
enable_metadata_passthrough
:
false
,
...
...
@@ -262,27 +271,27 @@ const baseSettingsResponse = {
payment_balance_disabled
:
false
,
payment_balance_recharge_multiplier
:
1
,
payment_recharge_fee_rate
:
0
,
payment_load_balance_strategy
:
'
round-robin
'
,
payment_product_name_prefix
:
''
,
payment_product_name_suffix
:
''
,
payment_help_image_url
:
''
,
payment_help_text
:
''
,
payment_load_balance_strategy
:
"
round-robin
"
,
payment_product_name_prefix
:
""
,
payment_product_name_suffix
:
""
,
payment_help_image_url
:
""
,
payment_help_text
:
""
,
payment_cancel_rate_limit_enabled
:
false
,
payment_cancel_rate_limit_max
:
10
,
payment_cancel_rate_limit_window
:
1
,
payment_cancel_rate_limit_unit
:
'
day
'
,
payment_cancel_rate_limit_window_mode
:
'
rolling
'
,
payment_visible_method_alipay_source
:
'
alipay_direct
'
,
payment_visible_method_wxpay_source
:
'
invalid-source
'
,
payment_cancel_rate_limit_unit
:
"
day
"
,
payment_cancel_rate_limit_window_mode
:
"
rolling
"
,
payment_visible_method_alipay_source
:
"
alipay_direct
"
,
payment_visible_method_wxpay_source
:
"
invalid-source
"
,
payment_visible_method_alipay_enabled
:
true
,
payment_visible_method_wxpay_enabled
:
true
,
openai_advanced_scheduler_enabled
:
false
,
balance_low_notify_enabled
:
false
,
balance_low_notify_threshold
:
0
,
balance_low_notify_recharge_url
:
''
,
balance_low_notify_recharge_url
:
""
,
account_quota_notify_enabled
:
false
,
account_quota_notify_emails
:
[],
}
}
;
function
mountView
()
{
return
mount
(
SettingsView
,
{
...
...
@@ -302,184 +311,361 @@ function mountView() {
BackupSettings
:
true
,
},
},
})
})
;
}
async
function
openPaymentTab
(
wrapper
:
ReturnType
<
typeof
mountView
>
)
{
const
paymentTabButton
=
wrapper
.
findAll
(
'
button
'
)
.
find
((
node
)
=>
node
.
text
().
includes
(
'
admin.settings.tabs.payment
'
))
.
findAll
(
"
button
"
)
.
find
((
node
)
=>
node
.
text
().
includes
(
"
admin.settings.tabs.payment
"
));
expect
(
paymentTabButton
).
toBeDefined
();
await
paymentTabButton
?.
trigger
(
"
click
"
);
await
flushPromises
();
}
expect
(
paymentTabButton
).
toBeDefined
()
await
paymentTabButton
?.
trigger
(
'
click
'
)
await
flushPromises
()
async
function
openSecurityTab
(
wrapper
:
ReturnType
<
typeof
mountView
>
)
{
const
securityTabButton
=
wrapper
.
findAll
(
"
button
"
)
.
find
((
node
)
=>
node
.
text
().
includes
(
"
admin.settings.tabs.security
"
));
expect
(
securityTabButton
).
toBeDefined
();
await
securityTabButton
?.
trigger
(
"
click
"
);
await
flushPromises
();
}
describe
(
'
admin SettingsView payment visible method controls
'
,
()
=>
{
describe
(
"
admin SettingsView payment visible method controls
"
,
()
=>
{
beforeEach
(()
=>
{
getSettings
.
mockReset
()
updateSettings
.
mockReset
()
getWebSearchEmulationConfig
.
mockReset
()
updateWebSearchEmulationConfig
.
mockReset
()
getAdminApiKey
.
mockReset
()
getOverloadCooldownSettings
.
mockReset
()
getStreamTimeoutSettings
.
mockReset
()
getRectifierSettings
.
mockReset
()
getBetaPolicySettings
.
mockReset
()
getGroups
.
mockReset
()
listProxies
.
mockReset
()
getProviders
.
mockReset
()
fetchPublicSettings
.
mockReset
()
adminSettingsFetch
.
mockReset
()
showError
.
mockReset
()
showSuccess
.
mockReset
()
getSettings
.
mockResolvedValue
({
...
baseSettingsResponse
})
getSettings
.
mockReset
()
;
updateSettings
.
mockReset
()
;
getWebSearchEmulationConfig
.
mockReset
()
;
updateWebSearchEmulationConfig
.
mockReset
()
;
getAdminApiKey
.
mockReset
()
;
getOverloadCooldownSettings
.
mockReset
()
;
getStreamTimeoutSettings
.
mockReset
()
;
getRectifierSettings
.
mockReset
()
;
getBetaPolicySettings
.
mockReset
()
;
getGroups
.
mockReset
()
;
listProxies
.
mockReset
()
;
getProviders
.
mockReset
()
;
fetchPublicSettings
.
mockReset
()
;
adminSettingsFetch
.
mockReset
()
;
showError
.
mockReset
()
;
showSuccess
.
mockReset
()
;
getSettings
.
mockResolvedValue
({
...
baseSettingsResponse
})
;
updateSettings
.
mockImplementation
(
async
(
payload
)
=>
({
...
baseSettingsResponse
,
...
payload
,
}))
}))
;
getWebSearchEmulationConfig
.
mockResolvedValue
({
enabled
:
false
,
providers
:
[],
})
})
;
updateWebSearchEmulationConfig
.
mockResolvedValue
({
enabled
:
false
,
providers
:
[],
})
})
;
getAdminApiKey
.
mockResolvedValue
({
exists
:
false
,
masked_key
:
''
,
})
masked_key
:
""
,
})
;
getOverloadCooldownSettings
.
mockResolvedValue
({
enabled
:
true
,
cooldown_minutes
:
10
,
})
})
;
getStreamTimeoutSettings
.
mockResolvedValue
({
enabled
:
true
,
action
:
'
temp_unsched
'
,
action
:
"
temp_unsched
"
,
temp_unsched_minutes
:
5
,
threshold_count
:
3
,
threshold_window_minutes
:
10
,
})
})
;
getRectifierSettings
.
mockResolvedValue
({
enabled
:
true
,
thinking_signature_enabled
:
true
,
thinking_budget_enabled
:
true
,
apikey_signature_enabled
:
false
,
apikey_signature_patterns
:
[],
})
})
;
getBetaPolicySettings
.
mockResolvedValue
({
rules
:
[],
})
getGroups
.
mockResolvedValue
([])
})
;
getGroups
.
mockResolvedValue
([])
;
listProxies
.
mockResolvedValue
({
items
:
[],
})
})
;
getProviders
.
mockResolvedValue
({
data
:
[],
})
fetchPublicSettings
.
mockResolvedValue
(
undefined
)
adminSettingsFetch
.
mockResolvedValue
(
undefined
)
})
})
;
fetchPublicSettings
.
mockResolvedValue
(
undefined
)
;
adminSettingsFetch
.
mockResolvedValue
(
undefined
)
;
})
;
it
(
'
loads canonical source options and normalizes existing values
'
,
async
()
=>
{
const
wrapper
=
mountView
()
it
(
"
loads canonical source options and normalizes existing values
"
,
async
()
=>
{
const
wrapper
=
mountView
()
;
await
flushPromises
()
await
openPaymentTab
(
wrapper
)
await
flushPromises
()
;
await
openPaymentTab
(
wrapper
)
;
const
paymentSourceSelects
=
wrapper
.
findAll
(
'
select.select-stub
'
)
.
filter
((
node
)
=>
[
'
alipay
'
,
'
wxpay
'
].
includes
(
node
.
attributes
(
'
data-placeholder
'
)))
.
findAll
(
"
select.select-stub
"
)
.
filter
((
node
)
=>
[
"
alipay
"
,
"
wxpay
"
].
includes
(
node
.
attributes
(
"
data-placeholder
"
)),
);
expect
(
paymentSourceSelects
).
toHaveLength
(
2
)
expect
(
paymentSourceSelects
).
toHaveLength
(
2
)
;
const
alipaySelect
=
paymentSourceSelects
.
find
(
(
node
)
=>
node
.
attributes
(
'
data-placeholder
'
)
===
'
alipay
'
)
(
node
)
=>
node
.
attributes
(
"
data-placeholder
"
)
===
"
alipay
"
,
)
;
const
wxpaySelect
=
paymentSourceSelects
.
find
(
(
node
)
=>
node
.
attributes
(
'
data-placeholder
'
)
===
'
wxpay
'
)
expect
(
alipaySelect
?.
element
.
value
).
toBe
(
'
official_alipay
'
)
expect
(
alipaySelect
?.
findAll
(
'
option
'
).
map
((
option
)
=>
option
.
element
.
value
)).
toEqual
([
''
,
'
official_alipay
'
,
'
easypay_alipay
'
,
])
expect
(
wxpaySelect
?.
element
.
value
).
toBe
(
''
)
expect
(
wxpaySelect
?.
findAll
(
'
option
'
).
map
((
option
)
=>
option
.
element
.
value
)).
toEqual
([
''
,
'
official_wxpay
'
,
'
easypay_wxpay
'
,
])
})
it
(
'
saves canonical source keys selected from the dropdowns
'
,
async
()
=>
{
const
wrapper
=
mountView
()
await
flushPromises
()
await
openPaymentTab
(
wrapper
)
(
node
)
=>
node
.
attributes
(
"
data-placeholder
"
)
===
"
wxpay
"
,
);
expect
(
alipaySelect
?.
element
.
value
).
toBe
(
"
official_alipay
"
);
expect
(
alipaySelect
?.
findAll
(
"
option
"
).
map
((
option
)
=>
option
.
element
.
value
),
).
toEqual
([
""
,
"
official_alipay
"
,
"
easypay_alipay
"
]);
expect
(
wxpaySelect
?.
element
.
value
).
toBe
(
""
);
expect
(
wxpaySelect
?.
findAll
(
"
option
"
).
map
((
option
)
=>
option
.
element
.
value
),
).
toEqual
([
""
,
"
official_wxpay
"
,
"
easypay_wxpay
"
]);
});
it
(
"
saves canonical source keys selected from the dropdowns
"
,
async
()
=>
{
const
wrapper
=
mountView
();
await
flushPromises
();
await
openPaymentTab
(
wrapper
);
const
paymentSourceSelects
=
wrapper
.
findAll
(
'
select.select-stub
'
)
.
filter
((
node
)
=>
[
'
alipay
'
,
'
wxpay
'
].
includes
(
node
.
attributes
(
'
data-placeholder
'
)))
.
findAll
(
"
select.select-stub
"
)
.
filter
((
node
)
=>
[
"
alipay
"
,
"
wxpay
"
].
includes
(
node
.
attributes
(
"
data-placeholder
"
)),
);
const
alipaySelect
=
paymentSourceSelects
.
find
(
(
node
)
=>
node
.
attributes
(
'
data-placeholder
'
)
===
'
alipay
'
)
(
node
)
=>
node
.
attributes
(
"
data-placeholder
"
)
===
"
alipay
"
,
)
;
const
wxpaySelect
=
paymentSourceSelects
.
find
(
(
node
)
=>
node
.
attributes
(
'
data-placeholder
'
)
===
'
wxpay
'
)
(
node
)
=>
node
.
attributes
(
"
data-placeholder
"
)
===
"
wxpay
"
,
)
;
await
alipaySelect
?.
setValue
(
'
easypay_alipay
'
)
await
wxpaySelect
?.
setValue
(
'
official_wxpay
'
)
await
wrapper
.
find
(
'
form
'
).
trigger
(
'
submit.prevent
'
)
await
flushPromises
()
await
alipaySelect
?.
setValue
(
"
easypay_alipay
"
);
await
wxpaySelect
?.
setValue
(
"
official_wxpay
"
);
await
wrapper
.
find
(
"
form
"
).
trigger
(
"
submit.prevent
"
);
await
flushPromises
()
;
expect
(
updateSettings
).
toHaveBeenCalledTimes
(
1
)
expect
(
updateSettings
).
toHaveBeenCalledTimes
(
1
)
;
expect
(
updateSettings
).
toHaveBeenCalledWith
(
expect
.
objectContaining
({
payment_visible_method_alipay_source
:
'
easypay_alipay
'
,
payment_visible_method_wxpay_source
:
'
official_wxpay
'
,
payment_visible_method_alipay_source
:
"
easypay_alipay
"
,
payment_visible_method_wxpay_source
:
"
official_wxpay
"
,
payment_visible_method_alipay_enabled
:
true
,
payment_visible_method_wxpay_enabled
:
true
,
})
)
})
})
,
)
;
})
;
it
(
'
blocks saving when a visible payment method is enabled without a source
'
,
async
()
=>
{
const
wrapper
=
mountView
()
it
(
"
blocks saving when a visible payment method is enabled without a source
"
,
async
()
=>
{
const
wrapper
=
mountView
()
;
await
flushPromises
()
await
openPaymentTab
(
wrapper
)
await
flushPromises
()
;
await
openPaymentTab
(
wrapper
)
;
const
paymentSourceSelects
=
wrapper
.
findAll
(
'
select.select-stub
'
)
.
filter
((
node
)
=>
[
'
alipay
'
,
'
wxpay
'
].
includes
(
node
.
attributes
(
'
data-placeholder
'
)))
.
findAll
(
"
select.select-stub
"
)
.
filter
((
node
)
=>
[
"
alipay
"
,
"
wxpay
"
].
includes
(
node
.
attributes
(
"
data-placeholder
"
)),
);
const
alipaySelect
=
paymentSourceSelects
.
find
(
(
node
)
=>
node
.
attributes
(
'
data-placeholder
'
)
===
'
alipay
'
)
await
alipaySelect
?.
setValue
(
''
)
await
wrapper
.
find
(
'
form
'
).
trigger
(
'
submit.prevent
'
)
await
flushPromises
()
expect
(
updateSettings
).
not
.
toHaveBeenCalled
()
expect
(
showError
).
toHaveBeenCalled
()
expect
(
String
(
showError
.
mock
.
calls
.
at
(
-
1
)?.[
0
]
??
''
)).
toContain
(
'
支付来源
'
)
})
it
(
'
renders advanced scheduler copy as local experimental gateway policy
'
,
async
()
=>
{
const
wrapper
=
mountView
()
await
flushPromises
()
expect
(
wrapper
.
text
()).
toContain
(
'
OpenAI 实验调度策略
'
)
expect
(
wrapper
.
text
()).
toContain
(
'
默认关闭。开启后仅影响本网关在 OpenAI 账号间的实验性调度选择逻辑
'
)
expect
(
wrapper
.
text
()).
not
.
toContain
(
'
OpenAI 高级调度器
'
)
})
})
(
node
)
=>
node
.
attributes
(
"
data-placeholder
"
)
===
"
alipay
"
,
);
await
alipaySelect
?.
setValue
(
""
);
await
wrapper
.
find
(
"
form
"
).
trigger
(
"
submit.prevent
"
);
await
flushPromises
();
expect
(
updateSettings
).
not
.
toHaveBeenCalled
();
expect
(
showError
).
toHaveBeenCalled
();
expect
(
String
(
showError
.
mock
.
calls
.
at
(
-
1
)?.[
0
]
??
""
)).
toContain
(
"
支付来源
"
,
);
});
it
(
"
renders advanced scheduler copy as local experimental gateway policy
"
,
async
()
=>
{
const
wrapper
=
mountView
();
await
flushPromises
();
expect
(
wrapper
.
text
()).
toContain
(
"
OpenAI 实验调度策略
"
);
expect
(
wrapper
.
text
()).
toContain
(
"
默认关闭。开启后仅影响本网关在 OpenAI 账号间的实验性调度选择逻辑
"
,
);
expect
(
wrapper
.
text
()).
not
.
toContain
(
"
OpenAI 高级调度器
"
);
});
});
describe
(
"
admin SettingsView wechat connect controls
"
,
()
=>
{
beforeEach
(()
=>
{
getSettings
.
mockReset
();
updateSettings
.
mockReset
();
getWebSearchEmulationConfig
.
mockReset
();
updateWebSearchEmulationConfig
.
mockReset
();
getAdminApiKey
.
mockReset
();
getOverloadCooldownSettings
.
mockReset
();
getStreamTimeoutSettings
.
mockReset
();
getRectifierSettings
.
mockReset
();
getBetaPolicySettings
.
mockReset
();
getGroups
.
mockReset
();
listProxies
.
mockReset
();
getProviders
.
mockReset
();
fetchPublicSettings
.
mockReset
();
adminSettingsFetch
.
mockReset
();
showError
.
mockReset
();
showSuccess
.
mockReset
();
getSettings
.
mockResolvedValue
({
...
baseSettingsResponse
,
payment_visible_method_wxpay_source
:
"
official_wxpay
"
,
});
updateSettings
.
mockImplementation
(
async
(
payload
)
=>
({
...
baseSettingsResponse
,
payment_visible_method_wxpay_source
:
"
official_wxpay
"
,
...
payload
,
}));
getWebSearchEmulationConfig
.
mockResolvedValue
({
enabled
:
false
,
providers
:
[],
});
updateWebSearchEmulationConfig
.
mockResolvedValue
({
enabled
:
false
,
providers
:
[],
});
getAdminApiKey
.
mockResolvedValue
({
exists
:
false
,
masked_key
:
""
,
});
getOverloadCooldownSettings
.
mockResolvedValue
({
enabled
:
true
,
cooldown_minutes
:
10
,
});
getStreamTimeoutSettings
.
mockResolvedValue
({
enabled
:
true
,
action
:
"
temp_unsched
"
,
temp_unsched_minutes
:
5
,
threshold_count
:
3
,
threshold_window_minutes
:
10
,
});
getRectifierSettings
.
mockResolvedValue
({
enabled
:
true
,
thinking_signature_enabled
:
true
,
thinking_budget_enabled
:
true
,
apikey_signature_enabled
:
false
,
apikey_signature_patterns
:
[],
});
getBetaPolicySettings
.
mockResolvedValue
({
rules
:
[],
});
getGroups
.
mockResolvedValue
([]);
listProxies
.
mockResolvedValue
({
items
:
[],
});
getProviders
.
mockResolvedValue
({
data
:
[],
});
fetchPublicSettings
.
mockResolvedValue
(
undefined
);
adminSettingsFetch
.
mockResolvedValue
(
undefined
);
});
it
(
"
loads and echoes WeChat Connect fields from the backend payload
"
,
async
()
=>
{
const
wrapper
=
mountView
();
await
flushPromises
();
await
openSecurityTab
(
wrapper
);
expect
(
(
wrapper
.
get
(
'
[data-testid="wechat-connect-app-id"]
'
)
.
element
as
HTMLInputElement
).
value
,
).
toBe
(
"
wx-app-id-123
"
);
expect
(
(
wrapper
.
get
(
'
[data-testid="wechat-connect-mode"]
'
)
.
element
as
HTMLSelectElement
).
value
,
).
toBe
(
"
mp
"
);
expect
(
(
wrapper
.
get
(
'
[data-testid="wechat-connect-scopes"]
'
)
.
element
as
HTMLInputElement
).
value
,
).
toBe
(
"
snsapi_userinfo
"
);
expect
(
wrapper
.
get
(
'
[data-testid="wechat-connect-app-secret"]
'
)
.
attributes
(
"
placeholder
"
),
).
toContain
(
"
密钥已配置
"
);
expect
(
(
wrapper
.
get
(
'
[data-testid="wechat-connect-frontend-redirect-url"]
'
)
.
element
as
HTMLInputElement
).
value
,
).
toBe
(
"
/auth/wechat/callback
"
);
});
it
(
"
saves WeChat Connect fields using the backend contract and clears the secret after save
"
,
async
()
=>
{
const
wrapper
=
mountView
();
await
flushPromises
();
await
openSecurityTab
(
wrapper
);
await
wrapper
.
get
(
'
[data-testid="wechat-connect-app-id"]
'
)
.
setValue
(
"
wx-app-id-updated
"
);
await
wrapper
.
get
(
'
[data-testid="wechat-connect-app-secret"]
'
)
.
setValue
(
"
new-secret
"
);
await
wrapper
.
get
(
'
[data-testid="wechat-connect-mode"]
'
).
setValue
(
"
open
"
);
await
wrapper
.
get
(
'
[data-testid="wechat-connect-scopes"]
'
)
.
setValue
(
"
snsapi_base
"
);
await
wrapper
.
get
(
'
[data-testid="wechat-connect-redirect-url"]
'
)
.
setValue
(
"
https://admin.example.com/api/v1/auth/oauth/wechat/callback
"
);
await
wrapper
.
get
(
'
[data-testid="wechat-connect-frontend-redirect-url"]
'
)
.
setValue
(
"
/auth/wechat/callback
"
);
await
wrapper
.
find
(
"
form
"
).
trigger
(
"
submit.prevent
"
);
await
flushPromises
();
expect
(
updateSettings
).
toHaveBeenCalledTimes
(
1
);
expect
(
updateSettings
).
toHaveBeenCalledWith
(
expect
.
objectContaining
({
wechat_connect_enabled
:
true
,
wechat_connect_app_id
:
"
wx-app-id-updated
"
,
wechat_connect_app_secret
:
"
new-secret
"
,
wechat_connect_mode
:
"
open
"
,
wechat_connect_scopes
:
"
snsapi_base
"
,
wechat_connect_redirect_url
:
"
https://admin.example.com/api/v1/auth/oauth/wechat/callback
"
,
wechat_connect_frontend_redirect_url
:
"
/auth/wechat/callback
"
,
}),
);
expect
(
(
wrapper
.
get
(
'
[data-testid="wechat-connect-app-secret"]
'
)
.
element
as
HTMLInputElement
).
value
,
).
toBe
(
""
);
expect
(
wrapper
.
get
(
'
[data-testid="wechat-connect-app-secret"]
'
)
.
attributes
(
"
placeholder
"
),
).
toContain
(
"
密钥已配置
"
);
});
});
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