feat(settings): support dual-mode wechat oauth defaults

This commit is contained in:
IanShaw027
2026-04-21 20:36:10 +08:00
parent 17c6348b57
commit 2cebb0dc60
15 changed files with 490 additions and 214 deletions

View File

@@ -125,6 +125,8 @@ func (h *SettingHandler) GetSettings(c *gin.Context) {
WeChatConnectEnabled: settings.WeChatConnectEnabled, WeChatConnectEnabled: settings.WeChatConnectEnabled,
WeChatConnectAppID: settings.WeChatConnectAppID, WeChatConnectAppID: settings.WeChatConnectAppID,
WeChatConnectAppSecretConfigured: settings.WeChatConnectAppSecretConfigured, WeChatConnectAppSecretConfigured: settings.WeChatConnectAppSecretConfigured,
WeChatConnectOpenEnabled: settings.WeChatConnectOpenEnabled,
WeChatConnectMPEnabled: settings.WeChatConnectMPEnabled,
WeChatConnectMode: settings.WeChatConnectMode, WeChatConnectMode: settings.WeChatConnectMode,
WeChatConnectScopes: settings.WeChatConnectScopes, WeChatConnectScopes: settings.WeChatConnectScopes,
WeChatConnectRedirectURL: settings.WeChatConnectRedirectURL, WeChatConnectRedirectURL: settings.WeChatConnectRedirectURL,
@@ -257,6 +259,8 @@ type UpdateSettingsRequest struct {
WeChatConnectEnabled bool `json:"wechat_connect_enabled"` WeChatConnectEnabled bool `json:"wechat_connect_enabled"`
WeChatConnectAppID string `json:"wechat_connect_app_id"` WeChatConnectAppID string `json:"wechat_connect_app_id"`
WeChatConnectAppSecret string `json:"wechat_connect_app_secret"` WeChatConnectAppSecret string `json:"wechat_connect_app_secret"`
WeChatConnectOpenEnabled bool `json:"wechat_connect_open_enabled"`
WeChatConnectMPEnabled bool `json:"wechat_connect_mp_enabled"`
WeChatConnectMode string `json:"wechat_connect_mode"` WeChatConnectMode string `json:"wechat_connect_mode"`
WeChatConnectScopes string `json:"wechat_connect_scopes"` WeChatConnectScopes string `json:"wechat_connect_scopes"`
WeChatConnectRedirectURL string `json:"wechat_connect_redirect_url"` WeChatConnectRedirectURL string `json:"wechat_connect_redirect_url"`
@@ -544,17 +548,35 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
} }
req.WeChatConnectAppSecret = previousSettings.WeChatConnectAppSecret req.WeChatConnectAppSecret = previousSettings.WeChatConnectAppSecret
} }
if req.WeChatConnectMode == "" { if req.WeChatConnectMode != "" {
req.WeChatConnectMode = "open" switch req.WeChatConnectMode {
case "open", "mp":
default:
response.BadRequest(c, "WeChat mode must be open or mp")
return
}
} }
switch req.WeChatConnectMode { if !req.WeChatConnectOpenEnabled && !req.WeChatConnectMPEnabled {
case "open", "mp": switch req.WeChatConnectMode {
default: case "mp":
response.BadRequest(c, "WeChat mode must be open or mp") req.WeChatConnectMPEnabled = true
return default:
req.WeChatConnectOpenEnabled = true
}
}
if req.WeChatConnectMode == "" {
if req.WeChatConnectMPEnabled {
req.WeChatConnectMode = "mp"
} else {
req.WeChatConnectMode = "open"
}
} }
if req.WeChatConnectScopes == "" { if req.WeChatConnectScopes == "" {
req.WeChatConnectScopes = service.DefaultWeChatConnectScopesForMode(req.WeChatConnectMode) if req.WeChatConnectMPEnabled {
req.WeChatConnectScopes = service.DefaultWeChatConnectScopesForMode("mp")
} else {
req.WeChatConnectScopes = service.DefaultWeChatConnectScopesForMode(req.WeChatConnectMode)
}
} }
if req.WeChatConnectRedirectURL == "" { if req.WeChatConnectRedirectURL == "" {
response.BadRequest(c, "WeChat Redirect URL is required when enabled") response.BadRequest(c, "WeChat Redirect URL is required when enabled")
@@ -924,6 +946,8 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
WeChatConnectEnabled: req.WeChatConnectEnabled, WeChatConnectEnabled: req.WeChatConnectEnabled,
WeChatConnectAppID: req.WeChatConnectAppID, WeChatConnectAppID: req.WeChatConnectAppID,
WeChatConnectAppSecret: req.WeChatConnectAppSecret, WeChatConnectAppSecret: req.WeChatConnectAppSecret,
WeChatConnectOpenEnabled: req.WeChatConnectOpenEnabled,
WeChatConnectMPEnabled: req.WeChatConnectMPEnabled,
WeChatConnectMode: req.WeChatConnectMode, WeChatConnectMode: req.WeChatConnectMode,
WeChatConnectScopes: req.WeChatConnectScopes, WeChatConnectScopes: req.WeChatConnectScopes,
WeChatConnectRedirectURL: req.WeChatConnectRedirectURL, WeChatConnectRedirectURL: req.WeChatConnectRedirectURL,
@@ -1210,6 +1234,8 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
WeChatConnectEnabled: updatedSettings.WeChatConnectEnabled, WeChatConnectEnabled: updatedSettings.WeChatConnectEnabled,
WeChatConnectAppID: updatedSettings.WeChatConnectAppID, WeChatConnectAppID: updatedSettings.WeChatConnectAppID,
WeChatConnectAppSecretConfigured: updatedSettings.WeChatConnectAppSecretConfigured, WeChatConnectAppSecretConfigured: updatedSettings.WeChatConnectAppSecretConfigured,
WeChatConnectOpenEnabled: updatedSettings.WeChatConnectOpenEnabled,
WeChatConnectMPEnabled: updatedSettings.WeChatConnectMPEnabled,
WeChatConnectMode: updatedSettings.WeChatConnectMode, WeChatConnectMode: updatedSettings.WeChatConnectMode,
WeChatConnectScopes: updatedSettings.WeChatConnectScopes, WeChatConnectScopes: updatedSettings.WeChatConnectScopes,
WeChatConnectRedirectURL: updatedSettings.WeChatConnectRedirectURL, WeChatConnectRedirectURL: updatedSettings.WeChatConnectRedirectURL,
@@ -1416,6 +1442,12 @@ func diffSettings(before *service.SystemSettings, after *service.SystemSettings,
if req.WeChatConnectAppSecret != "" { if req.WeChatConnectAppSecret != "" {
changed = append(changed, "wechat_connect_app_secret") changed = append(changed, "wechat_connect_app_secret")
} }
if before.WeChatConnectOpenEnabled != after.WeChatConnectOpenEnabled {
changed = append(changed, "wechat_connect_open_enabled")
}
if before.WeChatConnectMPEnabled != after.WeChatConnectMPEnabled {
changed = append(changed, "wechat_connect_mp_enabled")
}
if before.WeChatConnectMode != after.WeChatConnectMode { if before.WeChatConnectMode != after.WeChatConnectMode {
changed = append(changed, "wechat_connect_mode") changed = append(changed, "wechat_connect_mode")
} }

View File

@@ -54,6 +54,8 @@ type SystemSettings struct {
WeChatConnectEnabled bool `json:"wechat_connect_enabled"` WeChatConnectEnabled bool `json:"wechat_connect_enabled"`
WeChatConnectAppID string `json:"wechat_connect_app_id"` WeChatConnectAppID string `json:"wechat_connect_app_id"`
WeChatConnectAppSecretConfigured bool `json:"wechat_connect_app_secret_configured"` WeChatConnectAppSecretConfigured bool `json:"wechat_connect_app_secret_configured"`
WeChatConnectOpenEnabled bool `json:"wechat_connect_open_enabled"`
WeChatConnectMPEnabled bool `json:"wechat_connect_mp_enabled"`
WeChatConnectMode string `json:"wechat_connect_mode"` WeChatConnectMode string `json:"wechat_connect_mode"`
WeChatConnectScopes string `json:"wechat_connect_scopes"` WeChatConnectScopes string `json:"wechat_connect_scopes"`
WeChatConnectRedirectURL string `json:"wechat_connect_redirect_url"` WeChatConnectRedirectURL string `json:"wechat_connect_redirect_url"`

View File

@@ -91,6 +91,8 @@ func TestSettingHandler_GetPublicSettings_ExposesWeChatOAuthModeCapabilities(t *
service.SettingKeyWeChatConnectAppSecret: "wx-mp-secret", service.SettingKeyWeChatConnectAppSecret: "wx-mp-secret",
service.SettingKeyWeChatConnectMode: "mp", service.SettingKeyWeChatConnectMode: "mp",
service.SettingKeyWeChatConnectScopes: "snsapi_base", service.SettingKeyWeChatConnectScopes: "snsapi_base",
service.SettingKeyWeChatConnectOpenEnabled: "true",
service.SettingKeyWeChatConnectMPEnabled: "true",
service.SettingKeyWeChatConnectRedirectURL: "https://api.example.com/api/v1/auth/oauth/wechat/callback", service.SettingKeyWeChatConnectRedirectURL: "https://api.example.com/api/v1/auth/oauth/wechat/callback",
service.SettingKeyWeChatConnectFrontendRedirectURL: "/auth/wechat/callback", service.SettingKeyWeChatConnectFrontendRedirectURL: "/auth/wechat/callback",
}, },
@@ -115,6 +117,6 @@ func TestSettingHandler_GetPublicSettings_ExposesWeChatOAuthModeCapabilities(t *
require.NoError(t, json.Unmarshal(recorder.Body.Bytes(), &resp)) require.NoError(t, json.Unmarshal(recorder.Body.Bytes(), &resp))
require.Equal(t, 0, resp.Code) require.Equal(t, 0, resp.Code)
require.True(t, resp.Data.WeChatOAuthEnabled) require.True(t, resp.Data.WeChatOAuthEnabled)
require.False(t, resp.Data.WeChatOAuthOpenEnabled) require.True(t, resp.Data.WeChatOAuthOpenEnabled)
require.True(t, resp.Data.WeChatOAuthMPEnabled) require.True(t, resp.Data.WeChatOAuthMPEnabled)
} }

View File

@@ -115,6 +115,8 @@ const (
SettingKeyWeChatConnectEnabled = "wechat_connect_enabled" SettingKeyWeChatConnectEnabled = "wechat_connect_enabled"
SettingKeyWeChatConnectAppID = "wechat_connect_app_id" SettingKeyWeChatConnectAppID = "wechat_connect_app_id"
SettingKeyWeChatConnectAppSecret = "wechat_connect_app_secret" SettingKeyWeChatConnectAppSecret = "wechat_connect_app_secret"
SettingKeyWeChatConnectOpenEnabled = "wechat_connect_open_enabled"
SettingKeyWeChatConnectMPEnabled = "wechat_connect_mp_enabled"
SettingKeyWeChatConnectMode = "wechat_connect_mode" SettingKeyWeChatConnectMode = "wechat_connect_mode"
SettingKeyWeChatConnectScopes = "wechat_connect_scopes" SettingKeyWeChatConnectScopes = "wechat_connect_scopes"
SettingKeyWeChatConnectRedirectURL = "wechat_connect_redirect_url" SettingKeyWeChatConnectRedirectURL = "wechat_connect_redirect_url"

View File

@@ -519,7 +519,7 @@ func (s *PaymentService) getWeChatPaymentOAuthCredential(ctx context.Context) (s
) )
} }
cfg, err := (&SettingService{settingRepo: s.configService.settingRepo}).GetWeChatConnectOAuthConfig(ctx) cfg, err := (&SettingService{settingRepo: s.configService.settingRepo}).GetWeChatConnectOAuthConfig(ctx)
if err != nil || cfg.Mode != "mp" || strings.TrimSpace(cfg.AppID) == "" || strings.TrimSpace(cfg.AppSecret) == "" { if err != nil || !cfg.SupportsMode("mp") || strings.TrimSpace(cfg.AppID) == "" || strings.TrimSpace(cfg.AppSecret) == "" {
return "", "", infraerrors.ServiceUnavailable( return "", "", infraerrors.ServiceUnavailable(
"WECHAT_PAYMENT_MP_NOT_CONFIGURED", "WECHAT_PAYMENT_MP_NOT_CONFIGURED",
"wechat in-app payment requires a complete WeChat MP OAuth credential", "wechat in-app payment requires a complete WeChat MP OAuth credential",

View File

@@ -209,6 +209,39 @@ func normalizeWeChatConnectScopeSetting(raw, mode string) string {
} }
} }
func parseWeChatConnectCapabilitySettings(settings map[string]string, enabled bool, mode string) (bool, bool) {
mode = normalizeWeChatConnectModeSetting(mode)
rawOpen, hasOpen := settings[SettingKeyWeChatConnectOpenEnabled]
rawMP, hasMP := settings[SettingKeyWeChatConnectMPEnabled]
openConfigured := hasOpen && strings.TrimSpace(rawOpen) != ""
mpConfigured := hasMP && strings.TrimSpace(rawMP) != ""
if openConfigured || mpConfigured {
openEnabled := strings.TrimSpace(rawOpen) == "true"
mpEnabled := strings.TrimSpace(rawMP) == "true"
return openEnabled, mpEnabled
}
if !enabled {
return false, false
}
if mode == "mp" {
return false, true
}
return true, false
}
func normalizeWeChatConnectStoredMode(openEnabled, mpEnabled bool, mode string) string {
switch {
case mpEnabled:
return "mp"
case openEnabled:
return "open"
default:
return normalizeWeChatConnectModeSetting(mode)
}
}
// NewSettingService 创建系统设置服务实例 // NewSettingService 创建系统设置服务实例
func NewSettingService(settingRepo SettingRepository, cfg *config.Config) *SettingService { func NewSettingService(settingRepo SettingRepository, cfg *config.Config) *SettingService {
return &SettingService{ return &SettingService{
@@ -277,6 +310,8 @@ func (s *SettingService) GetPublicSettings(ctx context.Context) (*PublicSettings
SettingKeyWeChatConnectEnabled, SettingKeyWeChatConnectEnabled,
SettingKeyWeChatConnectAppID, SettingKeyWeChatConnectAppID,
SettingKeyWeChatConnectAppSecret, SettingKeyWeChatConnectAppSecret,
SettingKeyWeChatConnectOpenEnabled,
SettingKeyWeChatConnectMPEnabled,
SettingKeyWeChatConnectMode, SettingKeyWeChatConnectMode,
SettingKeyWeChatConnectScopes, SettingKeyWeChatConnectScopes,
SettingKeyWeChatConnectRedirectURL, SettingKeyWeChatConnectRedirectURL,
@@ -475,12 +510,19 @@ func DefaultWeChatConnectScopesForMode(mode string) string {
} }
func (s *SettingService) parseWeChatConnectOAuthConfig(settings map[string]string) (WeChatConnectOAuthConfig, error) { func (s *SettingService) parseWeChatConnectOAuthConfig(settings map[string]string) (WeChatConnectOAuthConfig, error) {
enabled := settings[SettingKeyWeChatConnectEnabled] == "true"
mode := normalizeWeChatConnectModeSetting(settings[SettingKeyWeChatConnectMode])
openEnabled, mpEnabled := parseWeChatConnectCapabilitySettings(settings, enabled, mode)
mode = normalizeWeChatConnectStoredMode(openEnabled, mpEnabled, mode)
cfg := WeChatConnectOAuthConfig{ cfg := WeChatConnectOAuthConfig{
Enabled: settings[SettingKeyWeChatConnectEnabled] == "true", Enabled: enabled,
AppID: strings.TrimSpace(settings[SettingKeyWeChatConnectAppID]), AppID: strings.TrimSpace(settings[SettingKeyWeChatConnectAppID]),
AppSecret: strings.TrimSpace(settings[SettingKeyWeChatConnectAppSecret]), AppSecret: strings.TrimSpace(settings[SettingKeyWeChatConnectAppSecret]),
Mode: normalizeWeChatConnectModeSetting(settings[SettingKeyWeChatConnectMode]), OpenEnabled: openEnabled,
Scopes: normalizeWeChatConnectScopeSetting(settings[SettingKeyWeChatConnectScopes], settings[SettingKeyWeChatConnectMode]), MPEnabled: mpEnabled,
Mode: mode,
Scopes: normalizeWeChatConnectScopeSetting(settings[SettingKeyWeChatConnectScopes], mode),
RedirectURL: strings.TrimSpace(settings[SettingKeyWeChatConnectRedirectURL]), RedirectURL: strings.TrimSpace(settings[SettingKeyWeChatConnectRedirectURL]),
FrontendRedirectURL: strings.TrimSpace(settings[SettingKeyWeChatConnectFrontendRedirectURL]), FrontendRedirectURL: strings.TrimSpace(settings[SettingKeyWeChatConnectFrontendRedirectURL]),
} }
@@ -488,7 +530,7 @@ func (s *SettingService) parseWeChatConnectOAuthConfig(settings map[string]strin
cfg.FrontendRedirectURL = defaultWeChatConnectFrontend cfg.FrontendRedirectURL = defaultWeChatConnectFrontend
} }
if !cfg.Enabled { if !cfg.Enabled || (!cfg.OpenEnabled && !cfg.MPEnabled) {
return WeChatConnectOAuthConfig{}, infraerrors.NotFound("OAUTH_DISABLED", "wechat oauth is disabled") return WeChatConnectOAuthConfig{}, infraerrors.NotFound("OAUTH_DISABLED", "wechat oauth is disabled")
} }
if cfg.AppID == "" { if cfg.AppID == "" {
@@ -517,7 +559,7 @@ func (s *SettingService) weChatOAuthCapabilitiesFromSettings(settings map[string
if err != nil { if err != nil {
return false, false, false return false, false, false
} }
return true, cfg.Mode == "open", cfg.Mode == "mp" return true, cfg.OpenEnabled, cfg.MPEnabled
} }
// filterUserVisibleMenuItems filters out admin-only menu items from a raw JSON // filterUserVisibleMenuItems filters out admin-only menu items from a raw JSON
@@ -702,7 +744,11 @@ func (s *SettingService) buildSystemSettingsUpdates(ctx context.Context, setting
settings.PaymentVisibleMethodWxpaySource = wxpaySource settings.PaymentVisibleMethodWxpaySource = wxpaySource
settings.WeChatConnectAppID = strings.TrimSpace(settings.WeChatConnectAppID) settings.WeChatConnectAppID = strings.TrimSpace(settings.WeChatConnectAppID)
settings.WeChatConnectAppSecret = strings.TrimSpace(settings.WeChatConnectAppSecret) settings.WeChatConnectAppSecret = strings.TrimSpace(settings.WeChatConnectAppSecret)
settings.WeChatConnectMode = normalizeWeChatConnectModeSetting(settings.WeChatConnectMode) settings.WeChatConnectMode = normalizeWeChatConnectStoredMode(
settings.WeChatConnectOpenEnabled,
settings.WeChatConnectMPEnabled,
settings.WeChatConnectMode,
)
settings.WeChatConnectScopes = normalizeWeChatConnectScopeSetting(settings.WeChatConnectScopes, settings.WeChatConnectMode) settings.WeChatConnectScopes = normalizeWeChatConnectScopeSetting(settings.WeChatConnectScopes, settings.WeChatConnectMode)
settings.WeChatConnectRedirectURL = strings.TrimSpace(settings.WeChatConnectRedirectURL) settings.WeChatConnectRedirectURL = strings.TrimSpace(settings.WeChatConnectRedirectURL)
settings.WeChatConnectFrontendRedirectURL = strings.TrimSpace(settings.WeChatConnectFrontendRedirectURL) settings.WeChatConnectFrontendRedirectURL = strings.TrimSpace(settings.WeChatConnectFrontendRedirectURL)
@@ -781,6 +827,8 @@ func (s *SettingService) buildSystemSettingsUpdates(ctx context.Context, setting
// WeChat Connect OAuth 登录 // WeChat Connect OAuth 登录
updates[SettingKeyWeChatConnectEnabled] = strconv.FormatBool(settings.WeChatConnectEnabled) updates[SettingKeyWeChatConnectEnabled] = strconv.FormatBool(settings.WeChatConnectEnabled)
updates[SettingKeyWeChatConnectAppID] = settings.WeChatConnectAppID updates[SettingKeyWeChatConnectAppID] = settings.WeChatConnectAppID
updates[SettingKeyWeChatConnectOpenEnabled] = strconv.FormatBool(settings.WeChatConnectOpenEnabled)
updates[SettingKeyWeChatConnectMPEnabled] = strconv.FormatBool(settings.WeChatConnectMPEnabled)
updates[SettingKeyWeChatConnectMode] = settings.WeChatConnectMode updates[SettingKeyWeChatConnectMode] = settings.WeChatConnectMode
updates[SettingKeyWeChatConnectScopes] = settings.WeChatConnectScopes updates[SettingKeyWeChatConnectScopes] = settings.WeChatConnectScopes
updates[SettingKeyWeChatConnectRedirectURL] = settings.WeChatConnectRedirectURL updates[SettingKeyWeChatConnectRedirectURL] = settings.WeChatConnectRedirectURL
@@ -1296,6 +1344,8 @@ func (s *SettingService) InitializeDefaultSettings(ctx context.Context) error {
SettingKeyCustomMenuItems: "[]", SettingKeyCustomMenuItems: "[]",
SettingKeyCustomEndpoints: "[]", SettingKeyCustomEndpoints: "[]",
SettingKeyWeChatConnectEnabled: "false", SettingKeyWeChatConnectEnabled: "false",
SettingKeyWeChatConnectOpenEnabled: "false",
SettingKeyWeChatConnectMPEnabled: "false",
SettingKeyWeChatConnectMode: "open", SettingKeyWeChatConnectMode: "open",
SettingKeyWeChatConnectScopes: "snsapi_login", SettingKeyWeChatConnectScopes: "snsapi_login",
SettingKeyWeChatConnectFrontendRedirectURL: defaultWeChatConnectFrontend, SettingKeyWeChatConnectFrontendRedirectURL: defaultWeChatConnectFrontend,
@@ -1307,22 +1357,22 @@ func (s *SettingService) InitializeDefaultSettings(ctx context.Context) error {
SettingKeyAuthSourceDefaultEmailBalance: "0", SettingKeyAuthSourceDefaultEmailBalance: "0",
SettingKeyAuthSourceDefaultEmailConcurrency: "5", SettingKeyAuthSourceDefaultEmailConcurrency: "5",
SettingKeyAuthSourceDefaultEmailSubscriptions: "[]", SettingKeyAuthSourceDefaultEmailSubscriptions: "[]",
SettingKeyAuthSourceDefaultEmailGrantOnSignup: "true", SettingKeyAuthSourceDefaultEmailGrantOnSignup: "false",
SettingKeyAuthSourceDefaultEmailGrantOnFirstBind: "false", SettingKeyAuthSourceDefaultEmailGrantOnFirstBind: "false",
SettingKeyAuthSourceDefaultLinuxDoBalance: "0", SettingKeyAuthSourceDefaultLinuxDoBalance: "0",
SettingKeyAuthSourceDefaultLinuxDoConcurrency: "5", SettingKeyAuthSourceDefaultLinuxDoConcurrency: "5",
SettingKeyAuthSourceDefaultLinuxDoSubscriptions: "[]", SettingKeyAuthSourceDefaultLinuxDoSubscriptions: "[]",
SettingKeyAuthSourceDefaultLinuxDoGrantOnSignup: "true", SettingKeyAuthSourceDefaultLinuxDoGrantOnSignup: "false",
SettingKeyAuthSourceDefaultLinuxDoGrantOnFirstBind: "false", SettingKeyAuthSourceDefaultLinuxDoGrantOnFirstBind: "false",
SettingKeyAuthSourceDefaultOIDCBalance: "0", SettingKeyAuthSourceDefaultOIDCBalance: "0",
SettingKeyAuthSourceDefaultOIDCConcurrency: "5", SettingKeyAuthSourceDefaultOIDCConcurrency: "5",
SettingKeyAuthSourceDefaultOIDCSubscriptions: "[]", SettingKeyAuthSourceDefaultOIDCSubscriptions: "[]",
SettingKeyAuthSourceDefaultOIDCGrantOnSignup: "true", SettingKeyAuthSourceDefaultOIDCGrantOnSignup: "false",
SettingKeyAuthSourceDefaultOIDCGrantOnFirstBind: "false", SettingKeyAuthSourceDefaultOIDCGrantOnFirstBind: "false",
SettingKeyAuthSourceDefaultWeChatBalance: "0", SettingKeyAuthSourceDefaultWeChatBalance: "0",
SettingKeyAuthSourceDefaultWeChatConcurrency: "5", SettingKeyAuthSourceDefaultWeChatConcurrency: "5",
SettingKeyAuthSourceDefaultWeChatSubscriptions: "[]", SettingKeyAuthSourceDefaultWeChatSubscriptions: "[]",
SettingKeyAuthSourceDefaultWeChatGrantOnSignup: "true", SettingKeyAuthSourceDefaultWeChatGrantOnSignup: "false",
SettingKeyAuthSourceDefaultWeChatGrantOnFirstBind: "false", SettingKeyAuthSourceDefaultWeChatGrantOnFirstBind: "false",
SettingKeyForceEmailOnThirdPartySignup: "false", SettingKeyForceEmailOnThirdPartySignup: "false",
SettingKeySMTPPort: "587", SettingKeySMTPPort: "587",
@@ -1595,8 +1645,17 @@ func (s *SettingService) parseSettings(settings map[string]string) *SystemSettin
result.WeChatConnectAppID = strings.TrimSpace(settings[SettingKeyWeChatConnectAppID]) result.WeChatConnectAppID = strings.TrimSpace(settings[SettingKeyWeChatConnectAppID])
result.WeChatConnectAppSecret = strings.TrimSpace(settings[SettingKeyWeChatConnectAppSecret]) result.WeChatConnectAppSecret = strings.TrimSpace(settings[SettingKeyWeChatConnectAppSecret])
result.WeChatConnectAppSecretConfigured = result.WeChatConnectAppSecret != "" result.WeChatConnectAppSecretConfigured = result.WeChatConnectAppSecret != ""
result.WeChatConnectMode = normalizeWeChatConnectModeSetting(settings[SettingKeyWeChatConnectMode]) result.WeChatConnectOpenEnabled, result.WeChatConnectMPEnabled = parseWeChatConnectCapabilitySettings(
result.WeChatConnectScopes = normalizeWeChatConnectScopeSetting(settings[SettingKeyWeChatConnectScopes], settings[SettingKeyWeChatConnectMode]) settings,
result.WeChatConnectEnabled,
settings[SettingKeyWeChatConnectMode],
)
result.WeChatConnectMode = normalizeWeChatConnectStoredMode(
result.WeChatConnectOpenEnabled,
result.WeChatConnectMPEnabled,
settings[SettingKeyWeChatConnectMode],
)
result.WeChatConnectScopes = normalizeWeChatConnectScopeSetting(settings[SettingKeyWeChatConnectScopes], result.WeChatConnectMode)
result.WeChatConnectRedirectURL = strings.TrimSpace(settings[SettingKeyWeChatConnectRedirectURL]) result.WeChatConnectRedirectURL = strings.TrimSpace(settings[SettingKeyWeChatConnectRedirectURL])
result.WeChatConnectFrontendRedirectURL = strings.TrimSpace(settings[SettingKeyWeChatConnectFrontendRedirectURL]) result.WeChatConnectFrontendRedirectURL = strings.TrimSpace(settings[SettingKeyWeChatConnectFrontendRedirectURL])
if result.WeChatConnectFrontendRedirectURL == "" { if result.WeChatConnectFrontendRedirectURL == "" {
@@ -1744,7 +1803,7 @@ func parseProviderDefaultGrantSettings(settings map[string]string, keys authSour
Balance: defaultAuthSourceBalance, Balance: defaultAuthSourceBalance,
Concurrency: defaultAuthSourceConcurrency, Concurrency: defaultAuthSourceConcurrency,
Subscriptions: []DefaultSubscriptionSetting{}, Subscriptions: []DefaultSubscriptionSetting{},
GrantOnSignup: true, GrantOnSignup: false,
GrantOnFirstBind: false, GrantOnFirstBind: false,
} }
@@ -2092,6 +2151,8 @@ func (s *SettingService) GetWeChatConnectOAuthConfig(ctx context.Context) (WeCha
SettingKeyWeChatConnectEnabled, SettingKeyWeChatConnectEnabled,
SettingKeyWeChatConnectAppID, SettingKeyWeChatConnectAppID,
SettingKeyWeChatConnectAppSecret, SettingKeyWeChatConnectAppSecret,
SettingKeyWeChatConnectOpenEnabled,
SettingKeyWeChatConnectMPEnabled,
SettingKeyWeChatConnectMode, SettingKeyWeChatConnectMode,
SettingKeyWeChatConnectScopes, SettingKeyWeChatConnectScopes,
SettingKeyWeChatConnectRedirectURL, SettingKeyWeChatConnectRedirectURL,

View File

@@ -81,10 +81,12 @@ func TestSettingService_GetAuthSourceDefaultSettings_ParsesValuesAndDefaults(t *
require.Equal(t, 0.0, got.LinuxDo.Balance) require.Equal(t, 0.0, got.LinuxDo.Balance)
require.Equal(t, 5, got.LinuxDo.Concurrency) require.Equal(t, 5, got.LinuxDo.Concurrency)
require.Equal(t, []DefaultSubscriptionSetting{}, got.LinuxDo.Subscriptions) require.Equal(t, []DefaultSubscriptionSetting{}, got.LinuxDo.Subscriptions)
require.True(t, got.LinuxDo.GrantOnSignup) require.False(t, got.LinuxDo.GrantOnSignup)
require.True(t, got.LinuxDo.GrantOnFirstBind) require.True(t, got.LinuxDo.GrantOnFirstBind)
require.Equal(t, 5, got.OIDC.Concurrency) require.Equal(t, 5, got.OIDC.Concurrency)
require.Equal(t, 5, got.WeChat.Concurrency) require.Equal(t, 5, got.WeChat.Concurrency)
require.False(t, got.OIDC.GrantOnSignup)
require.False(t, got.WeChat.GrantOnSignup)
require.True(t, got.ForceEmailOnThirdPartySignup) require.True(t, got.ForceEmailOnThirdPartySignup)
} }

View File

@@ -99,6 +99,8 @@ func TestSettingService_GetPublicSettings_ExposesWeChatOAuthModeCapabilities(t *
SettingKeyWeChatConnectAppSecret: "wx-mp-secret", SettingKeyWeChatConnectAppSecret: "wx-mp-secret",
SettingKeyWeChatConnectMode: "mp", SettingKeyWeChatConnectMode: "mp",
SettingKeyWeChatConnectScopes: "snsapi_base", SettingKeyWeChatConnectScopes: "snsapi_base",
SettingKeyWeChatConnectOpenEnabled: "true",
SettingKeyWeChatConnectMPEnabled: "true",
SettingKeyWeChatConnectRedirectURL: "https://api.example.com/api/v1/auth/oauth/wechat/callback", SettingKeyWeChatConnectRedirectURL: "https://api.example.com/api/v1/auth/oauth/wechat/callback",
SettingKeyWeChatConnectFrontendRedirectURL: "/auth/wechat/callback", SettingKeyWeChatConnectFrontendRedirectURL: "/auth/wechat/callback",
}, },
@@ -107,6 +109,6 @@ func TestSettingService_GetPublicSettings_ExposesWeChatOAuthModeCapabilities(t *
settings, err := svc.GetPublicSettings(context.Background()) settings, err := svc.GetPublicSettings(context.Background())
require.NoError(t, err) require.NoError(t, err)
require.True(t, settings.WeChatOAuthEnabled) require.True(t, settings.WeChatOAuthEnabled)
require.False(t, settings.WeChatOAuthOpenEnabled) require.True(t, settings.WeChatOAuthOpenEnabled)
require.True(t, settings.WeChatOAuthMPEnabled) require.True(t, settings.WeChatOAuthMPEnabled)
} }

View File

@@ -59,6 +59,8 @@ func TestSettingService_GetWeChatConnectOAuthConfig_UsesDatabaseOverrides(t *tes
SettingKeyWeChatConnectAppSecret: "wx-db-secret", SettingKeyWeChatConnectAppSecret: "wx-db-secret",
SettingKeyWeChatConnectMode: "mp", SettingKeyWeChatConnectMode: "mp",
SettingKeyWeChatConnectScopes: "snsapi_base", SettingKeyWeChatConnectScopes: "snsapi_base",
SettingKeyWeChatConnectOpenEnabled: "true",
SettingKeyWeChatConnectMPEnabled: "true",
SettingKeyWeChatConnectRedirectURL: "https://api.example.com/api/v1/auth/oauth/wechat/callback", SettingKeyWeChatConnectRedirectURL: "https://api.example.com/api/v1/auth/oauth/wechat/callback",
SettingKeyWeChatConnectFrontendRedirectURL: "/auth/wechat/callback", SettingKeyWeChatConnectFrontendRedirectURL: "/auth/wechat/callback",
}, },
@@ -70,6 +72,8 @@ func TestSettingService_GetWeChatConnectOAuthConfig_UsesDatabaseOverrides(t *tes
require.True(t, got.Enabled) require.True(t, got.Enabled)
require.Equal(t, "wx-db-app", got.AppID) require.Equal(t, "wx-db-app", got.AppID)
require.Equal(t, "wx-db-secret", got.AppSecret) require.Equal(t, "wx-db-secret", got.AppSecret)
require.True(t, got.OpenEnabled)
require.True(t, got.MPEnabled)
require.Equal(t, "mp", got.Mode) require.Equal(t, "mp", got.Mode)
require.Equal(t, "snsapi_base", got.Scopes) 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, "https://api.example.com/api/v1/auth/oauth/wechat/callback", got.RedirectURL)

View File

@@ -36,6 +36,8 @@ type SystemSettings struct {
WeChatConnectAppID string WeChatConnectAppID string
WeChatConnectAppSecret string WeChatConnectAppSecret string
WeChatConnectAppSecretConfigured bool WeChatConnectAppSecretConfigured bool
WeChatConnectOpenEnabled bool
WeChatConnectMPEnabled bool
WeChatConnectMode string WeChatConnectMode string
WeChatConnectScopes string WeChatConnectScopes string
WeChatConnectRedirectURL string WeChatConnectRedirectURL string
@@ -191,12 +193,30 @@ type WeChatConnectOAuthConfig struct {
Enabled bool Enabled bool
AppID string AppID string
AppSecret string AppSecret string
OpenEnabled bool
MPEnabled bool
Mode string Mode string
Scopes string Scopes string
RedirectURL string RedirectURL string
FrontendRedirectURL string FrontendRedirectURL string
} }
func (cfg WeChatConnectOAuthConfig) SupportsMode(mode string) bool {
switch normalizeWeChatConnectModeSetting(mode) {
case "mp":
return cfg.MPEnabled
default:
return cfg.OpenEnabled
}
}
func (cfg WeChatConnectOAuthConfig) ScopeForMode(mode string) string {
if normalizeWeChatConnectModeSetting(mode) == "mp" {
return normalizeWeChatConnectScopeSetting(cfg.Scopes, "mp")
}
return defaultWeChatConnectScopeForMode("open")
}
// StreamTimeoutSettings 流超时处理配置(仅控制超时后的处理方式,超时判定由网关配置控制) // StreamTimeoutSettings 流超时处理配置(仅控制超时后的处理方式,超时判定由网关配置控制)
type StreamTimeoutSettings struct { type StreamTimeoutSettings struct {
// Enabled 是否启用流超时处理 // Enabled 是否启用流超时处理

View File

@@ -0,0 +1,32 @@
INSERT INTO settings (key, value)
VALUES
(
'wechat_connect_open_enabled',
CASE
WHEN COALESCE((SELECT value FROM settings WHERE key = 'wechat_connect_enabled'), 'false') <> 'true' THEN 'false'
WHEN LOWER(TRIM(COALESCE((SELECT value FROM settings WHERE key = 'wechat_connect_mode'), 'open'))) = 'mp' THEN 'false'
ELSE 'true'
END
),
(
'wechat_connect_mp_enabled',
CASE
WHEN COALESCE((SELECT value FROM settings WHERE key = 'wechat_connect_enabled'), 'false') <> 'true' THEN 'false'
WHEN LOWER(TRIM(COALESCE((SELECT value FROM settings WHERE key = 'wechat_connect_mode'), 'open'))) = 'mp' THEN 'true'
ELSE 'false'
END
),
('auth_source_default_email_grant_on_signup', 'false'),
('auth_source_default_linuxdo_grant_on_signup', 'false'),
('auth_source_default_oidc_grant_on_signup', 'false'),
('auth_source_default_wechat_grant_on_signup', 'false')
ON CONFLICT (key) DO NOTHING;
UPDATE settings
SET value = 'false'
WHERE key IN (
'auth_source_default_email_grant_on_signup',
'auth_source_default_linuxdo_grant_on_signup',
'auth_source_default_oidc_grant_on_signup',
'auth_source_default_wechat_grant_on_signup'
);

View File

@@ -1,13 +1,13 @@
import { describe, expect, it } from 'vitest' import { describe, expect, it } from "vitest";
import { import {
appendAuthSourceDefaultsToUpdateRequest, appendAuthSourceDefaultsToUpdateRequest,
buildAuthSourceDefaultsState, buildAuthSourceDefaultsState,
type UpdateSettingsRequest, type UpdateSettingsRequest,
} from '@/api/admin/settings' } from "@/api/admin/settings";
describe('admin settings auth source defaults helpers', () => { describe("admin settings auth source defaults helpers", () => {
it('builds auth source defaults state from flat settings fields', () => { it("builds auth source defaults state from flat settings fields", () => {
const state = buildAuthSourceDefaultsState({ const state = buildAuthSourceDefaultsState({
auth_source_default_email_balance: 9.5, auth_source_default_email_balance: 9.5,
auth_source_default_email_concurrency: 3, auth_source_default_email_concurrency: 3,
@@ -23,7 +23,7 @@ describe('admin settings auth source defaults helpers', () => {
], ],
auth_source_default_linuxdo_grant_on_signup: true, auth_source_default_linuxdo_grant_on_signup: true,
auth_source_default_linuxdo_grant_on_first_bind: false, auth_source_default_linuxdo_grant_on_first_bind: false,
}) });
expect(state.email).toEqual({ expect(state.email).toEqual({
balance: 9.5, balance: 9.5,
@@ -31,34 +31,43 @@ describe('admin settings auth source defaults helpers', () => {
subscriptions: [{ group_id: 1, validity_days: 30 }], subscriptions: [{ group_id: 1, validity_days: 30 }],
grant_on_signup: false, grant_on_signup: false,
grant_on_first_bind: true, grant_on_first_bind: true,
}) });
expect(state.linuxdo).toEqual({ expect(state.linuxdo).toEqual({
balance: 6, balance: 6,
concurrency: 8, concurrency: 8,
subscriptions: [{ group_id: 2, validity_days: 60 }], subscriptions: [{ group_id: 2, validity_days: 60 }],
grant_on_signup: true, grant_on_signup: true,
grant_on_first_bind: false, grant_on_first_bind: false,
}) });
expect(state.oidc).toEqual({ expect(state.oidc).toEqual({
balance: 0, balance: 0,
concurrency: 5, concurrency: 5,
subscriptions: [], subscriptions: [],
grant_on_signup: true, grant_on_signup: false,
grant_on_first_bind: false, grant_on_first_bind: false,
}) });
expect(state.wechat).toEqual({ expect(state.wechat).toEqual({
balance: 0, balance: 0,
concurrency: 5, concurrency: 5,
subscriptions: [], subscriptions: [],
grant_on_signup: true, grant_on_signup: false,
grant_on_first_bind: false, grant_on_first_bind: false,
}) });
}) });
it('appends auth source defaults back onto update payload', () => { it("defaults grant-on-signup to disabled when settings are missing", () => {
const state = buildAuthSourceDefaultsState({});
expect(state.email.grant_on_signup).toBe(false);
expect(state.linuxdo.grant_on_signup).toBe(false);
expect(state.oidc.grant_on_signup).toBe(false);
expect(state.wechat.grant_on_signup).toBe(false);
});
it("appends auth source defaults back onto update payload", () => {
const payload: UpdateSettingsRequest = { const payload: UpdateSettingsRequest = {
site_name: 'Sub2API', site_name: "Sub2API",
} };
appendAuthSourceDefaultsToUpdateRequest(payload, { appendAuthSourceDefaultsToUpdateRequest(payload, {
email: { email: {
@@ -89,13 +98,15 @@ describe('admin settings auth source defaults helpers', () => {
grant_on_signup: false, grant_on_signup: false,
grant_on_first_bind: false, grant_on_first_bind: false,
}, },
}) });
expect(payload).toMatchObject({ expect(payload).toMatchObject({
site_name: 'Sub2API', site_name: "Sub2API",
auth_source_default_email_balance: 1.25, auth_source_default_email_balance: 1.25,
auth_source_default_email_concurrency: 2, auth_source_default_email_concurrency: 2,
auth_source_default_email_subscriptions: [{ group_id: 3, validity_days: 7 }], auth_source_default_email_subscriptions: [
{ group_id: 3, validity_days: 7 },
],
auth_source_default_email_grant_on_signup: true, auth_source_default_email_grant_on_signup: true,
auth_source_default_email_grant_on_first_bind: false, auth_source_default_email_grant_on_first_bind: false,
auth_source_default_linuxdo_balance: 0, auth_source_default_linuxdo_balance: 0,
@@ -105,7 +116,9 @@ describe('admin settings auth source defaults helpers', () => {
auth_source_default_linuxdo_grant_on_first_bind: true, auth_source_default_linuxdo_grant_on_first_bind: true,
auth_source_default_oidc_balance: 4, auth_source_default_oidc_balance: 4,
auth_source_default_oidc_concurrency: 9, auth_source_default_oidc_concurrency: 9,
auth_source_default_oidc_subscriptions: [{ group_id: 9, validity_days: 90 }], auth_source_default_oidc_subscriptions: [
{ group_id: 9, validity_days: 90 },
],
auth_source_default_oidc_grant_on_signup: true, auth_source_default_oidc_grant_on_signup: true,
auth_source_default_oidc_grant_on_first_bind: true, auth_source_default_oidc_grant_on_first_bind: true,
auth_source_default_wechat_balance: 2, auth_source_default_wechat_balance: 2,
@@ -113,6 +126,6 @@ describe('admin settings auth source defaults helpers', () => {
auth_source_default_wechat_subscriptions: [], auth_source_default_wechat_subscriptions: [],
auth_source_default_wechat_grant_on_signup: false, auth_source_default_wechat_grant_on_signup: false,
auth_source_default_wechat_grant_on_first_bind: false, auth_source_default_wechat_grant_on_first_bind: false,
}) });
}) });
}) });

View File

@@ -167,7 +167,7 @@ export function buildAuthSourceDefaultsState(
: [], : [],
), ),
grant_on_signup: grant_on_signup:
raw[`auth_source_default_${source}_grant_on_signup`] !== false, raw[`auth_source_default_${source}_grant_on_signup`] === true,
grant_on_first_bind: grant_on_first_bind:
raw[`auth_source_default_${source}_grant_on_first_bind`] === true, raw[`auth_source_default_${source}_grant_on_first_bind`] === true,
}; };
@@ -239,6 +239,33 @@ export function defaultWeChatConnectScopesForMode(mode: unknown): string {
: "snsapi_login"; : "snsapi_login";
} }
export function resolveWeChatConnectModeCapabilities(
openEnabled: unknown,
mpEnabled: unknown,
legacyMode: unknown,
): { openEnabled: boolean; mpEnabled: boolean } {
if (typeof openEnabled === "boolean" || typeof mpEnabled === "boolean") {
return {
openEnabled: openEnabled === true,
mpEnabled: mpEnabled === true,
};
}
return normalizeWeChatConnectMode(legacyMode) === "mp"
? { openEnabled: false, mpEnabled: true }
: { openEnabled: true, mpEnabled: false };
}
export function deriveWeChatConnectStoredMode(
openEnabled: boolean,
mpEnabled: boolean,
legacyMode: unknown,
): WeChatConnectMode {
if (mpEnabled) return "mp";
if (openEnabled) return "open";
return normalizeWeChatConnectMode(legacyMode);
}
/** /**
* System settings interface * System settings interface
*/ */
@@ -315,6 +342,8 @@ export interface SystemSettings {
wechat_connect_enabled: boolean; wechat_connect_enabled: boolean;
wechat_connect_app_id: string; wechat_connect_app_id: string;
wechat_connect_app_secret_configured: boolean; wechat_connect_app_secret_configured: boolean;
wechat_connect_open_enabled?: boolean;
wechat_connect_mp_enabled?: boolean;
wechat_connect_mode: string; wechat_connect_mode: string;
wechat_connect_scopes: string; wechat_connect_scopes: string;
wechat_connect_redirect_url: string; wechat_connect_redirect_url: string;
@@ -472,6 +501,8 @@ export interface UpdateSettingsRequest {
wechat_connect_enabled?: boolean; wechat_connect_enabled?: boolean;
wechat_connect_app_id?: string; wechat_connect_app_id?: string;
wechat_connect_app_secret?: string; wechat_connect_app_secret?: string;
wechat_connect_open_enabled?: boolean;
wechat_connect_mp_enabled?: boolean;
wechat_connect_mode?: string; wechat_connect_mode?: string;
wechat_connect_scopes?: string; wechat_connect_scopes?: string;
wechat_connect_redirect_url?: string; wechat_connect_redirect_url?: string;

View File

@@ -1408,7 +1408,7 @@
v-if="form.wechat_connect_enabled" v-if="form.wechat_connect_enabled"
class="space-y-6 border-t border-gray-100 pt-4 dark:border-dark-700" class="space-y-6 border-t border-gray-100 pt-4 dark:border-dark-700"
> >
<div class="grid grid-cols-1 gap-6 lg:grid-cols-3"> <div class="grid grid-cols-1 gap-6 lg:grid-cols-2">
<div> <div>
<label <label
class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300" class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
@@ -1463,68 +1463,73 @@
}} }}
</p> </p>
</div> </div>
</div>
<div> <div class="grid grid-cols-1 gap-6 lg:grid-cols-2">
<div class="space-y-3">
<label <label
class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300" class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
> >
{{ localText("模式", "Mode") }} {{ localText("模式", "Mode") }}
</label> </label>
<select <div
data-testid="wechat-connect-mode" class="flex items-center justify-between rounded border border-gray-200 px-4 py-3 dark:border-dark-700"
v-model="form.wechat_connect_mode"
class="input font-mono text-sm"
@change="syncWeChatConnectMode"
> >
<option value="open"> <div>
{{ localText("开放平台", "Open Platform") }} <div class="font-medium text-gray-900 dark:text-white">
</option> {{
<option value="mp"> localText(
{{ "非微信环境使用开放平台",
localText( "Use Open outside WeChat",
"公众号 / 小程序", )
"Official Account / Mini Program", }}
) </div>
}} <p
</option> class="mt-0.5 text-xs text-gray-500 dark:text-gray-400"
</select> >
<p class="mt-1.5 text-xs text-gray-500 dark:text-gray-400"> {{
{{ localText(
localText( "浏览器不在微信内时,自动走开放平台扫码授权。",
"open 对应微信开放平台mp 对应公众号/小程序授权。", "Use Open Platform QR authorization outside the WeChat browser.",
"open maps to WeChat Open Platform, mp maps to Official Account / Mini Program authorization.", )
) }}
}} </p>
</p> </div>
</div> <Toggle
</div> v-model="form.wechat_connect_open_enabled"
data-testid="wechat-connect-open-enabled"
<div class="grid grid-cols-1 gap-6 lg:grid-cols-2"> @update:model-value="syncWeChatConnectMode"
<div> />
<label </div>
class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300" <div
class="flex items-center justify-between rounded border border-gray-200 px-4 py-3 dark:border-dark-700"
> >
{{ localText("Scopes", "Scopes") }} <div>
</label> <div class="font-medium text-gray-900 dark:text-white">
<input {{
data-testid="wechat-connect-scopes" localText(
v-model="form.wechat_connect_scopes" "微信环境使用公众号",
type="text" "Use MP inside WeChat",
class="input font-mono text-sm" )
:placeholder=" }}
form.wechat_connect_mode === 'mp' </div>
? 'snsapi_userinfo' <p
: 'snsapi_login' class="mt-0.5 text-xs text-gray-500 dark:text-gray-400"
" >
/> {{
<p class="mt-1.5 text-xs text-gray-500 dark:text-gray-400"> localText(
{{ "浏览器在微信内时,自动走公众号授权。",
localText( "Use Official Account authorization inside the WeChat browser.",
"留空时会按模式自动回填默认值。", )
"Leave empty to use the default scope for the selected mode.", }}
) </p>
}} </div>
</p> <Toggle
v-model="form.wechat_connect_mp_enabled"
data-testid="wechat-connect-mp-enabled"
@update:model-value="syncWeChatConnectMode"
/>
</div>
</div> </div>
<div> <div>
@@ -2246,83 +2251,77 @@
<Toggle v-model="form.force_email_on_third_party_signup" /> <Toggle v-model="form.force_email_on_third_party_signup" />
</div> </div>
<div class="grid grid-cols-1 gap-4 xl:grid-cols-2"> <div class="space-y-4">
<div <div
v-for="authSource in authSourceDefaultsMeta" v-for="authSource in authSourceDefaultsMeta"
:key="authSource.source" :key="authSource.source"
class="rounded-xl border border-gray-200 p-4 dark:border-dark-700" class="rounded-xl border border-gray-200 p-4 dark:border-dark-700"
> >
<div class="mb-4"> <div class="flex items-center justify-between gap-4">
<div class="font-medium text-gray-900 dark:text-white"> <div>
{{ authSource.title }} <div class="font-medium text-gray-900 dark:text-white">
{{ authSource.title }}
</div>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
{{ authSource.description }}
</p>
</div> </div>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400"> <Toggle
{{ authSource.description }} v-model="
authSourceDefaults[authSource.source].grant_on_signup
"
:data-testid="`auth-source-${authSource.source}-enabled`"
/>
</div>
<div
v-if="authSourceDefaults[authSource.source].grant_on_signup"
:data-testid="`auth-source-${authSource.source}-panel`"
class="mt-4 space-y-4 border-t border-gray-100 pt-4 dark:border-dark-700"
>
<p class="text-sm text-gray-500 dark:text-gray-400">
{{
localText(
"以下默认值会在该来源注册新用户时发放;首次绑定时授权仅作用于已有账号绑定该来源。",
"These defaults apply when a new user registers through this source. Grant on first bind only applies when an existing user binds this source.",
)
}}
</p> </p>
</div>
<div class="grid grid-cols-1 gap-4 md:grid-cols-2"> <div class="grid grid-cols-1 gap-4 md:grid-cols-2">
<div>
<label
class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
>
{{ t("admin.settings.defaults.defaultBalance") }}
</label>
<input
v-model.number="
authSourceDefaults[authSource.source].balance
"
type="number"
step="0.01"
min="0"
class="input"
placeholder="0.00"
/>
</div>
<div>
<label
class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
>
{{ t("admin.settings.defaults.defaultConcurrency") }}
</label>
<input
v-model.number="
authSourceDefaults[authSource.source].concurrency
"
type="number"
min="1"
class="input"
placeholder="5"
/>
</div>
</div>
<div class="mt-4 grid grid-cols-1 gap-3 md:grid-cols-2">
<div
class="flex items-center justify-between rounded border border-gray-200 px-4 py-3 dark:border-dark-700"
>
<div> <div>
<label <label
class="font-medium text-gray-900 dark:text-white" class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
> >
{{ localText("注册即授权", "Grant on signup") }} {{ t("admin.settings.defaults.defaultBalance") }}
</label> </label>
<p <input
class="mt-0.5 text-xs text-gray-500 dark:text-gray-400" v-model.number="
> authSourceDefaults[authSource.source].balance
{{ "
localText( type="number"
"来源首次注册成功后立即发放默认权益。", step="0.01"
"Grant default entitlements immediately after signup.", min="0"
) class="input"
}} placeholder="0.00"
</p> />
</div>
<div>
<label
class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
>
{{ t("admin.settings.defaults.defaultConcurrency") }}
</label>
<input
v-model.number="
authSourceDefaults[authSource.source].concurrency
"
type="number"
min="1"
class="input"
placeholder="5"
/>
</div> </div>
<Toggle
v-model="
authSourceDefaults[authSource.source].grant_on_signup
"
/>
</div> </div>
<div <div
@@ -2341,8 +2340,8 @@
> >
{{ {{
localText( localText(
"来源首次绑定到现有账号时发放默认权益。", "已有账号首次绑定该来源时发放默认权益。",
"Grant default entitlements when the source is first bound to an existing user.", "Grant default entitlements when an existing user first binds this source.",
) )
}} }}
</p> </p>
@@ -2354,11 +2353,7 @@
" "
/> />
</div> </div>
</div>
<div
class="mt-4 border-t border-gray-100 pt-4 dark:border-dark-700"
>
<div class="mb-3 flex items-center justify-between"> <div class="mb-3 flex items-center justify-between">
<div> <div>
<label <label
@@ -4710,12 +4705,13 @@ import { useI18n } from "vue-i18n";
import { adminAPI } from "@/api"; import { adminAPI } from "@/api";
import { import {
appendAuthSourceDefaultsToUpdateRequest, appendAuthSourceDefaultsToUpdateRequest,
defaultWeChatConnectScopesForMode,
buildAuthSourceDefaultsState, buildAuthSourceDefaultsState,
defaultWeChatConnectScopesForMode,
deriveWeChatConnectStoredMode,
getPaymentVisibleMethodSourceOptions, getPaymentVisibleMethodSourceOptions,
normalizePaymentVisibleMethodSource, normalizePaymentVisibleMethodSource,
normalizeDefaultSubscriptionSettings, normalizeDefaultSubscriptionSettings,
normalizeWeChatConnectMode, resolveWeChatConnectModeCapabilities,
} from "@/api/admin/settings"; } from "@/api/admin/settings";
import type { import type {
AuthSourceDefaultsState, AuthSourceDefaultsState,
@@ -4859,11 +4855,16 @@ interface DefaultSubscriptionGroupOption {
[key: string]: unknown; [key: string]: unknown;
} }
type SettingsForm = SystemSettings & { type SettingsForm = Omit<
SystemSettings,
"wechat_connect_open_enabled" | "wechat_connect_mp_enabled"
> & {
smtp_password: string; smtp_password: string;
turnstile_secret_key: string; turnstile_secret_key: string;
linuxdo_connect_client_secret: string; linuxdo_connect_client_secret: string;
wechat_connect_app_secret: string; wechat_connect_app_secret: string;
wechat_connect_open_enabled: boolean;
wechat_connect_mp_enabled: boolean;
oidc_connect_client_secret: string; oidc_connect_client_secret: string;
force_email_on_third_party_signup: boolean; force_email_on_third_party_signup: boolean;
payment_visible_method_alipay_source: string; payment_visible_method_alipay_source: string;
@@ -4958,6 +4959,8 @@ const form = reactive<SettingsForm>({
wechat_connect_app_id: "", wechat_connect_app_id: "",
wechat_connect_app_secret: "", wechat_connect_app_secret: "",
wechat_connect_app_secret_configured: false, wechat_connect_app_secret_configured: false,
wechat_connect_open_enabled: false,
wechat_connect_mp_enabled: false,
wechat_connect_mode: "open", wechat_connect_mode: "open",
wechat_connect_scopes: "snsapi_login", wechat_connect_scopes: "snsapi_login",
wechat_connect_redirect_url: "", wechat_connect_redirect_url: "",
@@ -5452,14 +5455,21 @@ const wechatRedirectUrlSuggestion = computed(() => {
}); });
function syncWeChatConnectMode() { function syncWeChatConnectMode() {
form.wechat_connect_mode = normalizeWeChatConnectMode( const capabilities = resolveWeChatConnectModeCapabilities(
form.wechat_connect_open_enabled,
form.wechat_connect_mp_enabled,
form.wechat_connect_mode,
);
form.wechat_connect_open_enabled = capabilities.openEnabled;
form.wechat_connect_mp_enabled = capabilities.mpEnabled;
form.wechat_connect_mode = deriveWeChatConnectStoredMode(
capabilities.openEnabled,
capabilities.mpEnabled,
form.wechat_connect_mode,
);
form.wechat_connect_scopes = defaultWeChatConnectScopesForMode(
form.wechat_connect_mode, form.wechat_connect_mode,
); );
if (!form.wechat_connect_scopes.trim()) {
form.wechat_connect_scopes = defaultWeChatConnectScopesForMode(
form.wechat_connect_mode,
);
}
} }
async function setAndCopyWeChatRedirectUrl() { async function setAndCopyWeChatRedirectUrl() {
@@ -5608,16 +5618,21 @@ async function loadSettings() {
form.turnstile_secret_key = ""; form.turnstile_secret_key = "";
form.linuxdo_connect_client_secret = ""; form.linuxdo_connect_client_secret = "";
form.wechat_connect_app_secret = ""; form.wechat_connect_app_secret = "";
form.wechat_connect_mode = normalizeWeChatConnectMode( const wechatCapabilities = resolveWeChatConnectModeCapabilities(
settings.wechat_connect_open_enabled,
settings.wechat_connect_mp_enabled,
settings.wechat_connect_mode, settings.wechat_connect_mode,
); );
const wechatConnectScopes = form.wechat_connect_open_enabled = wechatCapabilities.openEnabled;
typeof settings.wechat_connect_scopes === "string" form.wechat_connect_mp_enabled = wechatCapabilities.mpEnabled;
? settings.wechat_connect_scopes.trim() form.wechat_connect_mode = deriveWeChatConnectStoredMode(
: ""; wechatCapabilities.openEnabled,
form.wechat_connect_scopes = wechatCapabilities.mpEnabled,
wechatConnectScopes || settings.wechat_connect_mode,
defaultWeChatConnectScopesForMode(form.wechat_connect_mode); );
form.wechat_connect_scopes = defaultWeChatConnectScopesForMode(
form.wechat_connect_mode,
);
form.oidc_connect_client_secret = ""; form.oidc_connect_client_secret = "";
// Load web search emulation config separately // Load web search emulation config separately
@@ -5789,6 +5804,12 @@ async function saveSettings() {
// Optional URL fields: auto-clear invalid values so they don't cause backend 400 errors // Optional URL fields: auto-clear invalid values so they don't cause backend 400 errors
if (!isValidHttpUrl(form.frontend_url)) form.frontend_url = ""; if (!isValidHttpUrl(form.frontend_url)) form.frontend_url = "";
if (!isValidHttpUrl(form.doc_url)) form.doc_url = ""; if (!isValidHttpUrl(form.doc_url)) form.doc_url = "";
syncWeChatConnectMode();
const wechatStoredMode = deriveWeChatConnectStoredMode(
form.wechat_connect_open_enabled,
form.wechat_connect_mp_enabled,
form.wechat_connect_mode,
);
const payload: UpdateSettingsRequest = { const payload: UpdateSettingsRequest = {
registration_enabled: form.registration_enabled, registration_enabled: form.registration_enabled,
@@ -5837,10 +5858,11 @@ async function saveSettings() {
wechat_connect_enabled: form.wechat_connect_enabled, wechat_connect_enabled: form.wechat_connect_enabled,
wechat_connect_app_id: form.wechat_connect_app_id, wechat_connect_app_id: form.wechat_connect_app_id,
wechat_connect_app_secret: form.wechat_connect_app_secret || undefined, wechat_connect_app_secret: form.wechat_connect_app_secret || undefined,
wechat_connect_mode: normalizeWeChatConnectMode(form.wechat_connect_mode), wechat_connect_open_enabled: form.wechat_connect_open_enabled,
wechat_connect_mp_enabled: form.wechat_connect_mp_enabled,
wechat_connect_mode: wechatStoredMode,
wechat_connect_scopes: wechat_connect_scopes:
form.wechat_connect_scopes.trim() || defaultWeChatConnectScopesForMode(wechatStoredMode),
defaultWeChatConnectScopesForMode(form.wechat_connect_mode),
wechat_connect_redirect_url: form.wechat_connect_redirect_url, wechat_connect_redirect_url: form.wechat_connect_redirect_url,
wechat_connect_frontend_redirect_url: wechat_connect_frontend_redirect_url:
form.wechat_connect_frontend_redirect_url, form.wechat_connect_frontend_redirect_url,
@@ -5967,16 +5989,21 @@ async function saveSettings() {
form.turnstile_secret_key = ""; form.turnstile_secret_key = "";
form.linuxdo_connect_client_secret = ""; form.linuxdo_connect_client_secret = "";
form.wechat_connect_app_secret = ""; form.wechat_connect_app_secret = "";
form.wechat_connect_mode = normalizeWeChatConnectMode( const updatedWechatCapabilities = resolveWeChatConnectModeCapabilities(
updated.wechat_connect_open_enabled,
updated.wechat_connect_mp_enabled,
updated.wechat_connect_mode, updated.wechat_connect_mode,
); );
const updatedWechatConnectScopes = form.wechat_connect_open_enabled = updatedWechatCapabilities.openEnabled;
typeof updated.wechat_connect_scopes === "string" form.wechat_connect_mp_enabled = updatedWechatCapabilities.mpEnabled;
? updated.wechat_connect_scopes.trim() form.wechat_connect_mode = deriveWeChatConnectStoredMode(
: ""; updatedWechatCapabilities.openEnabled,
form.wechat_connect_scopes = updatedWechatCapabilities.mpEnabled,
updatedWechatConnectScopes || updated.wechat_connect_mode,
defaultWeChatConnectScopesForMode(form.wechat_connect_mode); );
form.wechat_connect_scopes = defaultWeChatConnectScopesForMode(
form.wechat_connect_mode,
);
form.oidc_connect_client_secret = ""; form.oidc_connect_client_secret = "";
// Save web search emulation config separately (errors handled internally) // Save web search emulation config separately (errors handled internally)
const wsOk = await saveWebSearchConfig(); const wsOk = await saveWebSearchConfig();

View File

@@ -111,9 +111,11 @@ const ToggleStub = defineComponent({
}, },
}, },
emits: ["update:modelValue"], emits: ["update:modelValue"],
setup(props, { emit }) { inheritAttrs: false,
setup(props, { attrs, emit }) {
return () => return () =>
h("input", { h("input", {
...attrs,
class: "toggle-stub", class: "toggle-stub",
type: "checkbox", type: "checkbox",
checked: props.modelValue, checked: props.modelValue,
@@ -217,6 +219,8 @@ const baseSettingsResponse = {
wechat_connect_enabled: true, wechat_connect_enabled: true,
wechat_connect_app_id: "wx-app-id-123", wechat_connect_app_id: "wx-app-id-123",
wechat_connect_app_secret_configured: true, wechat_connect_app_secret_configured: true,
wechat_connect_open_enabled: false,
wechat_connect_mp_enabled: true,
wechat_connect_mode: "mp", wechat_connect_mode: "mp",
wechat_connect_scopes: "", wechat_connect_scopes: "",
wechat_connect_redirect_url: wechat_connect_redirect_url:
@@ -334,6 +338,16 @@ async function openSecurityTab(wrapper: ReturnType<typeof mountView>) {
await flushPromises(); await flushPromises();
} }
async function openUsersTab(wrapper: ReturnType<typeof mountView>) {
const usersTabButton = wrapper
.findAll("button")
.find((node) => node.text().includes("admin.settings.tabs.users"));
expect(usersTabButton).toBeDefined();
await usersTabButton?.trigger("click");
await flushPromises();
}
describe("admin SettingsView payment visible method controls", () => { describe("admin SettingsView payment visible method controls", () => {
beforeEach(() => { beforeEach(() => {
getSettings.mockReset(); getSettings.mockReset();
@@ -595,16 +609,19 @@ describe("admin SettingsView wechat connect controls", () => {
).toBe("wx-app-id-123"); ).toBe("wx-app-id-123");
expect( expect(
( (
wrapper.get('[data-testid="wechat-connect-mode"]') wrapper.get('[data-testid="wechat-connect-open-enabled"]')
.element as HTMLSelectElement .element as HTMLInputElement
).value, ).checked,
).toBe("mp"); ).toBe(false);
expect( expect(
( (
wrapper.get('[data-testid="wechat-connect-scopes"]') wrapper.get('[data-testid="wechat-connect-mp-enabled"]')
.element as HTMLInputElement .element as HTMLInputElement
).value, ).checked,
).toBe("snsapi_userinfo"); ).toBe(true);
expect(wrapper.find('[data-testid="wechat-connect-scopes"]').exists()).toBe(
false,
);
expect( expect(
wrapper wrapper
.get('[data-testid="wechat-connect-app-secret"]') .get('[data-testid="wechat-connect-app-secret"]')
@@ -630,10 +647,12 @@ describe("admin SettingsView wechat connect controls", () => {
await wrapper await wrapper
.get('[data-testid="wechat-connect-app-secret"]') .get('[data-testid="wechat-connect-app-secret"]')
.setValue("new-secret"); .setValue("new-secret");
await wrapper.get('[data-testid="wechat-connect-mode"]').setValue("open");
await wrapper await wrapper
.get('[data-testid="wechat-connect-scopes"]') .get('[data-testid="wechat-connect-open-enabled"]')
.setValue(" snsapi_base "); .setValue(true);
await wrapper
.get('[data-testid="wechat-connect-mp-enabled"]')
.setValue(true);
await wrapper await wrapper
.get('[data-testid="wechat-connect-redirect-url"]') .get('[data-testid="wechat-connect-redirect-url"]')
.setValue("https://admin.example.com/api/v1/auth/oauth/wechat/callback"); .setValue("https://admin.example.com/api/v1/auth/oauth/wechat/callback");
@@ -649,8 +668,8 @@ describe("admin SettingsView wechat connect controls", () => {
wechat_connect_enabled: true, wechat_connect_enabled: true,
wechat_connect_app_id: "wx-app-id-updated", wechat_connect_app_id: "wx-app-id-updated",
wechat_connect_app_secret: "new-secret", wechat_connect_app_secret: "new-secret",
wechat_connect_mode: "open", wechat_connect_open_enabled: true,
wechat_connect_scopes: "snsapi_base", wechat_connect_mp_enabled: true,
wechat_connect_redirect_url: wechat_connect_redirect_url:
"https://admin.example.com/api/v1/auth/oauth/wechat/callback", "https://admin.example.com/api/v1/auth/oauth/wechat/callback",
wechat_connect_frontend_redirect_url: "/auth/wechat/callback", wechat_connect_frontend_redirect_url: "/auth/wechat/callback",
@@ -668,4 +687,31 @@ describe("admin SettingsView wechat connect controls", () => {
.attributes("placeholder"), .attributes("placeholder"),
).toContain("密钥已配置"); ).toContain("密钥已配置");
}); });
it("collapses auth source defaults until the source is enabled", async () => {
const wrapper = mountView();
await flushPromises();
await openUsersTab(wrapper);
expect(
(
wrapper.get('[data-testid="auth-source-email-enabled"]')
.element as HTMLInputElement
).checked,
).toBe(false);
expect(
wrapper.find('[data-testid="auth-source-email-panel"]').exists(),
).toBe(false);
expect(wrapper.text()).not.toContain("注册即授权");
await wrapper
.get('[data-testid="auth-source-email-enabled"]')
.setValue(true);
expect(
wrapper.find('[data-testid="auth-source-email-panel"]').exists(),
).toBe(true);
expect(wrapper.text()).toContain("首次绑定时授权");
});
}); });