From 2cebb0dc60b451498cbf53766a37dc2189689c8f Mon Sep 17 00:00:00 2001 From: IanShaw027 Date: Tue, 21 Apr 2026 20:36:10 +0800 Subject: [PATCH] feat(settings): support dual-mode wechat oauth defaults --- .../internal/handler/admin/setting_handler.go | 48 ++- backend/internal/handler/dto/settings.go | 2 + .../handler/setting_handler_public_test.go | 4 +- backend/internal/service/domain_constants.go | 2 + backend/internal/service/payment_order.go | 2 +- backend/internal/service/setting_service.go | 87 ++++- ...tting_service_auth_source_defaults_test.go | 4 +- .../service/setting_service_public_test.go | 4 +- .../setting_service_wechat_config_test.go | 4 + backend/internal/service/settings_view.go | 20 ++ ...hat_dual_mode_and_auth_source_defaults.sql | 32 ++ .../settings.authSourceDefaults.spec.ts | 57 +-- frontend/src/api/admin/settings.ts | 33 +- frontend/src/views/admin/SettingsView.vue | 333 ++++++++++-------- .../admin/__tests__/SettingsView.spec.ts | 72 +++- 15 files changed, 490 insertions(+), 214 deletions(-) create mode 100644 backend/migrations/118_wechat_dual_mode_and_auth_source_defaults.sql diff --git a/backend/internal/handler/admin/setting_handler.go b/backend/internal/handler/admin/setting_handler.go index 9bc20771..eb8b28a7 100644 --- a/backend/internal/handler/admin/setting_handler.go +++ b/backend/internal/handler/admin/setting_handler.go @@ -125,6 +125,8 @@ func (h *SettingHandler) GetSettings(c *gin.Context) { WeChatConnectEnabled: settings.WeChatConnectEnabled, WeChatConnectAppID: settings.WeChatConnectAppID, WeChatConnectAppSecretConfigured: settings.WeChatConnectAppSecretConfigured, + WeChatConnectOpenEnabled: settings.WeChatConnectOpenEnabled, + WeChatConnectMPEnabled: settings.WeChatConnectMPEnabled, WeChatConnectMode: settings.WeChatConnectMode, WeChatConnectScopes: settings.WeChatConnectScopes, WeChatConnectRedirectURL: settings.WeChatConnectRedirectURL, @@ -257,6 +259,8 @@ type UpdateSettingsRequest struct { WeChatConnectEnabled bool `json:"wechat_connect_enabled"` WeChatConnectAppID string `json:"wechat_connect_app_id"` 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"` WeChatConnectScopes string `json:"wechat_connect_scopes"` WeChatConnectRedirectURL string `json:"wechat_connect_redirect_url"` @@ -544,17 +548,35 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) { } req.WeChatConnectAppSecret = previousSettings.WeChatConnectAppSecret } - if req.WeChatConnectMode == "" { - req.WeChatConnectMode = "open" + if req.WeChatConnectMode != "" { + switch req.WeChatConnectMode { + case "open", "mp": + default: + response.BadRequest(c, "WeChat mode must be open or mp") + return + } } - switch req.WeChatConnectMode { - case "open", "mp": - default: - response.BadRequest(c, "WeChat mode must be open or mp") - return + if !req.WeChatConnectOpenEnabled && !req.WeChatConnectMPEnabled { + switch req.WeChatConnectMode { + case "mp": + req.WeChatConnectMPEnabled = true + default: + req.WeChatConnectOpenEnabled = true + } + } + if req.WeChatConnectMode == "" { + if req.WeChatConnectMPEnabled { + req.WeChatConnectMode = "mp" + } else { + req.WeChatConnectMode = "open" + } } 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 == "" { response.BadRequest(c, "WeChat Redirect URL is required when enabled") @@ -924,6 +946,8 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) { WeChatConnectEnabled: req.WeChatConnectEnabled, WeChatConnectAppID: req.WeChatConnectAppID, WeChatConnectAppSecret: req.WeChatConnectAppSecret, + WeChatConnectOpenEnabled: req.WeChatConnectOpenEnabled, + WeChatConnectMPEnabled: req.WeChatConnectMPEnabled, WeChatConnectMode: req.WeChatConnectMode, WeChatConnectScopes: req.WeChatConnectScopes, WeChatConnectRedirectURL: req.WeChatConnectRedirectURL, @@ -1210,6 +1234,8 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) { WeChatConnectEnabled: updatedSettings.WeChatConnectEnabled, WeChatConnectAppID: updatedSettings.WeChatConnectAppID, WeChatConnectAppSecretConfigured: updatedSettings.WeChatConnectAppSecretConfigured, + WeChatConnectOpenEnabled: updatedSettings.WeChatConnectOpenEnabled, + WeChatConnectMPEnabled: updatedSettings.WeChatConnectMPEnabled, WeChatConnectMode: updatedSettings.WeChatConnectMode, WeChatConnectScopes: updatedSettings.WeChatConnectScopes, WeChatConnectRedirectURL: updatedSettings.WeChatConnectRedirectURL, @@ -1416,6 +1442,12 @@ func diffSettings(before *service.SystemSettings, after *service.SystemSettings, if req.WeChatConnectAppSecret != "" { 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 { changed = append(changed, "wechat_connect_mode") } diff --git a/backend/internal/handler/dto/settings.go b/backend/internal/handler/dto/settings.go index 4c5edfbf..17c1abd4 100644 --- a/backend/internal/handler/dto/settings.go +++ b/backend/internal/handler/dto/settings.go @@ -54,6 +54,8 @@ type SystemSettings struct { WeChatConnectEnabled bool `json:"wechat_connect_enabled"` WeChatConnectAppID string `json:"wechat_connect_app_id"` 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"` WeChatConnectScopes string `json:"wechat_connect_scopes"` WeChatConnectRedirectURL string `json:"wechat_connect_redirect_url"` diff --git a/backend/internal/handler/setting_handler_public_test.go b/backend/internal/handler/setting_handler_public_test.go index 628d9341..45d66f8e 100644 --- a/backend/internal/handler/setting_handler_public_test.go +++ b/backend/internal/handler/setting_handler_public_test.go @@ -91,6 +91,8 @@ func TestSettingHandler_GetPublicSettings_ExposesWeChatOAuthModeCapabilities(t * service.SettingKeyWeChatConnectAppSecret: "wx-mp-secret", service.SettingKeyWeChatConnectMode: "mp", service.SettingKeyWeChatConnectScopes: "snsapi_base", + service.SettingKeyWeChatConnectOpenEnabled: "true", + service.SettingKeyWeChatConnectMPEnabled: "true", service.SettingKeyWeChatConnectRedirectURL: "https://api.example.com/api/v1/auth/oauth/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.Equal(t, 0, resp.Code) require.True(t, resp.Data.WeChatOAuthEnabled) - require.False(t, resp.Data.WeChatOAuthOpenEnabled) + require.True(t, resp.Data.WeChatOAuthOpenEnabled) require.True(t, resp.Data.WeChatOAuthMPEnabled) } diff --git a/backend/internal/service/domain_constants.go b/backend/internal/service/domain_constants.go index 4d63cabc..49b3c5ce 100644 --- a/backend/internal/service/domain_constants.go +++ b/backend/internal/service/domain_constants.go @@ -115,6 +115,8 @@ const ( SettingKeyWeChatConnectEnabled = "wechat_connect_enabled" SettingKeyWeChatConnectAppID = "wechat_connect_app_id" SettingKeyWeChatConnectAppSecret = "wechat_connect_app_secret" + SettingKeyWeChatConnectOpenEnabled = "wechat_connect_open_enabled" + SettingKeyWeChatConnectMPEnabled = "wechat_connect_mp_enabled" SettingKeyWeChatConnectMode = "wechat_connect_mode" SettingKeyWeChatConnectScopes = "wechat_connect_scopes" SettingKeyWeChatConnectRedirectURL = "wechat_connect_redirect_url" diff --git a/backend/internal/service/payment_order.go b/backend/internal/service/payment_order.go index 01d8c642..5e315625 100644 --- a/backend/internal/service/payment_order.go +++ b/backend/internal/service/payment_order.go @@ -519,7 +519,7 @@ func (s *PaymentService) getWeChatPaymentOAuthCredential(ctx context.Context) (s ) } 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( "WECHAT_PAYMENT_MP_NOT_CONFIGURED", "wechat in-app payment requires a complete WeChat MP OAuth credential", diff --git a/backend/internal/service/setting_service.go b/backend/internal/service/setting_service.go index 373c1aef..f0808996 100644 --- a/backend/internal/service/setting_service.go +++ b/backend/internal/service/setting_service.go @@ -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 创建系统设置服务实例 func NewSettingService(settingRepo SettingRepository, cfg *config.Config) *SettingService { return &SettingService{ @@ -277,6 +310,8 @@ func (s *SettingService) GetPublicSettings(ctx context.Context) (*PublicSettings SettingKeyWeChatConnectEnabled, SettingKeyWeChatConnectAppID, SettingKeyWeChatConnectAppSecret, + SettingKeyWeChatConnectOpenEnabled, + SettingKeyWeChatConnectMPEnabled, SettingKeyWeChatConnectMode, SettingKeyWeChatConnectScopes, SettingKeyWeChatConnectRedirectURL, @@ -475,12 +510,19 @@ func DefaultWeChatConnectScopesForMode(mode string) string { } 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{ - Enabled: settings[SettingKeyWeChatConnectEnabled] == "true", + Enabled: enabled, AppID: strings.TrimSpace(settings[SettingKeyWeChatConnectAppID]), AppSecret: strings.TrimSpace(settings[SettingKeyWeChatConnectAppSecret]), - Mode: normalizeWeChatConnectModeSetting(settings[SettingKeyWeChatConnectMode]), - Scopes: normalizeWeChatConnectScopeSetting(settings[SettingKeyWeChatConnectScopes], settings[SettingKeyWeChatConnectMode]), + OpenEnabled: openEnabled, + MPEnabled: mpEnabled, + Mode: mode, + Scopes: normalizeWeChatConnectScopeSetting(settings[SettingKeyWeChatConnectScopes], mode), RedirectURL: strings.TrimSpace(settings[SettingKeyWeChatConnectRedirectURL]), FrontendRedirectURL: strings.TrimSpace(settings[SettingKeyWeChatConnectFrontendRedirectURL]), } @@ -488,7 +530,7 @@ func (s *SettingService) parseWeChatConnectOAuthConfig(settings map[string]strin cfg.FrontendRedirectURL = defaultWeChatConnectFrontend } - if !cfg.Enabled { + if !cfg.Enabled || (!cfg.OpenEnabled && !cfg.MPEnabled) { return WeChatConnectOAuthConfig{}, infraerrors.NotFound("OAUTH_DISABLED", "wechat oauth is disabled") } if cfg.AppID == "" { @@ -517,7 +559,7 @@ func (s *SettingService) weChatOAuthCapabilitiesFromSettings(settings map[string if err != nil { 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 @@ -702,7 +744,11 @@ func (s *SettingService) buildSystemSettingsUpdates(ctx context.Context, setting settings.PaymentVisibleMethodWxpaySource = wxpaySource settings.WeChatConnectAppID = strings.TrimSpace(settings.WeChatConnectAppID) 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.WeChatConnectRedirectURL = strings.TrimSpace(settings.WeChatConnectRedirectURL) settings.WeChatConnectFrontendRedirectURL = strings.TrimSpace(settings.WeChatConnectFrontendRedirectURL) @@ -781,6 +827,8 @@ func (s *SettingService) buildSystemSettingsUpdates(ctx context.Context, setting // WeChat Connect OAuth 登录 updates[SettingKeyWeChatConnectEnabled] = strconv.FormatBool(settings.WeChatConnectEnabled) updates[SettingKeyWeChatConnectAppID] = settings.WeChatConnectAppID + updates[SettingKeyWeChatConnectOpenEnabled] = strconv.FormatBool(settings.WeChatConnectOpenEnabled) + updates[SettingKeyWeChatConnectMPEnabled] = strconv.FormatBool(settings.WeChatConnectMPEnabled) updates[SettingKeyWeChatConnectMode] = settings.WeChatConnectMode updates[SettingKeyWeChatConnectScopes] = settings.WeChatConnectScopes updates[SettingKeyWeChatConnectRedirectURL] = settings.WeChatConnectRedirectURL @@ -1296,6 +1344,8 @@ func (s *SettingService) InitializeDefaultSettings(ctx context.Context) error { SettingKeyCustomMenuItems: "[]", SettingKeyCustomEndpoints: "[]", SettingKeyWeChatConnectEnabled: "false", + SettingKeyWeChatConnectOpenEnabled: "false", + SettingKeyWeChatConnectMPEnabled: "false", SettingKeyWeChatConnectMode: "open", SettingKeyWeChatConnectScopes: "snsapi_login", SettingKeyWeChatConnectFrontendRedirectURL: defaultWeChatConnectFrontend, @@ -1307,22 +1357,22 @@ func (s *SettingService) InitializeDefaultSettings(ctx context.Context) error { SettingKeyAuthSourceDefaultEmailBalance: "0", SettingKeyAuthSourceDefaultEmailConcurrency: "5", SettingKeyAuthSourceDefaultEmailSubscriptions: "[]", - SettingKeyAuthSourceDefaultEmailGrantOnSignup: "true", + SettingKeyAuthSourceDefaultEmailGrantOnSignup: "false", SettingKeyAuthSourceDefaultEmailGrantOnFirstBind: "false", SettingKeyAuthSourceDefaultLinuxDoBalance: "0", SettingKeyAuthSourceDefaultLinuxDoConcurrency: "5", SettingKeyAuthSourceDefaultLinuxDoSubscriptions: "[]", - SettingKeyAuthSourceDefaultLinuxDoGrantOnSignup: "true", + SettingKeyAuthSourceDefaultLinuxDoGrantOnSignup: "false", SettingKeyAuthSourceDefaultLinuxDoGrantOnFirstBind: "false", SettingKeyAuthSourceDefaultOIDCBalance: "0", SettingKeyAuthSourceDefaultOIDCConcurrency: "5", SettingKeyAuthSourceDefaultOIDCSubscriptions: "[]", - SettingKeyAuthSourceDefaultOIDCGrantOnSignup: "true", + SettingKeyAuthSourceDefaultOIDCGrantOnSignup: "false", SettingKeyAuthSourceDefaultOIDCGrantOnFirstBind: "false", SettingKeyAuthSourceDefaultWeChatBalance: "0", SettingKeyAuthSourceDefaultWeChatConcurrency: "5", SettingKeyAuthSourceDefaultWeChatSubscriptions: "[]", - SettingKeyAuthSourceDefaultWeChatGrantOnSignup: "true", + SettingKeyAuthSourceDefaultWeChatGrantOnSignup: "false", SettingKeyAuthSourceDefaultWeChatGrantOnFirstBind: "false", SettingKeyForceEmailOnThirdPartySignup: "false", SettingKeySMTPPort: "587", @@ -1595,8 +1645,17 @@ func (s *SettingService) parseSettings(settings map[string]string) *SystemSettin 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.WeChatConnectOpenEnabled, result.WeChatConnectMPEnabled = parseWeChatConnectCapabilitySettings( + 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.WeChatConnectFrontendRedirectURL = strings.TrimSpace(settings[SettingKeyWeChatConnectFrontendRedirectURL]) if result.WeChatConnectFrontendRedirectURL == "" { @@ -1744,7 +1803,7 @@ func parseProviderDefaultGrantSettings(settings map[string]string, keys authSour Balance: defaultAuthSourceBalance, Concurrency: defaultAuthSourceConcurrency, Subscriptions: []DefaultSubscriptionSetting{}, - GrantOnSignup: true, + GrantOnSignup: false, GrantOnFirstBind: false, } @@ -2092,6 +2151,8 @@ func (s *SettingService) GetWeChatConnectOAuthConfig(ctx context.Context) (WeCha SettingKeyWeChatConnectEnabled, SettingKeyWeChatConnectAppID, SettingKeyWeChatConnectAppSecret, + SettingKeyWeChatConnectOpenEnabled, + SettingKeyWeChatConnectMPEnabled, SettingKeyWeChatConnectMode, SettingKeyWeChatConnectScopes, SettingKeyWeChatConnectRedirectURL, diff --git a/backend/internal/service/setting_service_auth_source_defaults_test.go b/backend/internal/service/setting_service_auth_source_defaults_test.go index 097bf604..1ff49740 100644 --- a/backend/internal/service/setting_service_auth_source_defaults_test.go +++ b/backend/internal/service/setting_service_auth_source_defaults_test.go @@ -81,10 +81,12 @@ func TestSettingService_GetAuthSourceDefaultSettings_ParsesValuesAndDefaults(t * require.Equal(t, 0.0, got.LinuxDo.Balance) require.Equal(t, 5, got.LinuxDo.Concurrency) 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.Equal(t, 5, got.OIDC.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) } diff --git a/backend/internal/service/setting_service_public_test.go b/backend/internal/service/setting_service_public_test.go index ffc069dc..497d1e36 100644 --- a/backend/internal/service/setting_service_public_test.go +++ b/backend/internal/service/setting_service_public_test.go @@ -99,6 +99,8 @@ func TestSettingService_GetPublicSettings_ExposesWeChatOAuthModeCapabilities(t * SettingKeyWeChatConnectAppSecret: "wx-mp-secret", SettingKeyWeChatConnectMode: "mp", SettingKeyWeChatConnectScopes: "snsapi_base", + SettingKeyWeChatConnectOpenEnabled: "true", + SettingKeyWeChatConnectMPEnabled: "true", SettingKeyWeChatConnectRedirectURL: "https://api.example.com/api/v1/auth/oauth/wechat/callback", SettingKeyWeChatConnectFrontendRedirectURL: "/auth/wechat/callback", }, @@ -107,6 +109,6 @@ func TestSettingService_GetPublicSettings_ExposesWeChatOAuthModeCapabilities(t * settings, err := svc.GetPublicSettings(context.Background()) require.NoError(t, err) require.True(t, settings.WeChatOAuthEnabled) - require.False(t, settings.WeChatOAuthOpenEnabled) + require.True(t, settings.WeChatOAuthOpenEnabled) require.True(t, settings.WeChatOAuthMPEnabled) } diff --git a/backend/internal/service/setting_service_wechat_config_test.go b/backend/internal/service/setting_service_wechat_config_test.go index 2cb312cc..45bf479e 100644 --- a/backend/internal/service/setting_service_wechat_config_test.go +++ b/backend/internal/service/setting_service_wechat_config_test.go @@ -59,6 +59,8 @@ func TestSettingService_GetWeChatConnectOAuthConfig_UsesDatabaseOverrides(t *tes SettingKeyWeChatConnectAppSecret: "wx-db-secret", SettingKeyWeChatConnectMode: "mp", SettingKeyWeChatConnectScopes: "snsapi_base", + SettingKeyWeChatConnectOpenEnabled: "true", + SettingKeyWeChatConnectMPEnabled: "true", SettingKeyWeChatConnectRedirectURL: "https://api.example.com/api/v1/auth/oauth/wechat/callback", SettingKeyWeChatConnectFrontendRedirectURL: "/auth/wechat/callback", }, @@ -70,6 +72,8 @@ func TestSettingService_GetWeChatConnectOAuthConfig_UsesDatabaseOverrides(t *tes require.True(t, got.Enabled) require.Equal(t, "wx-db-app", got.AppID) 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, "snsapi_base", got.Scopes) require.Equal(t, "https://api.example.com/api/v1/auth/oauth/wechat/callback", got.RedirectURL) diff --git a/backend/internal/service/settings_view.go b/backend/internal/service/settings_view.go index 41229bfb..1be206b9 100644 --- a/backend/internal/service/settings_view.go +++ b/backend/internal/service/settings_view.go @@ -36,6 +36,8 @@ type SystemSettings struct { WeChatConnectAppID string WeChatConnectAppSecret string WeChatConnectAppSecretConfigured bool + WeChatConnectOpenEnabled bool + WeChatConnectMPEnabled bool WeChatConnectMode string WeChatConnectScopes string WeChatConnectRedirectURL string @@ -191,12 +193,30 @@ type WeChatConnectOAuthConfig struct { Enabled bool AppID string AppSecret string + OpenEnabled bool + MPEnabled bool Mode string Scopes string RedirectURL 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 流超时处理配置(仅控制超时后的处理方式,超时判定由网关配置控制) type StreamTimeoutSettings struct { // Enabled 是否启用流超时处理 diff --git a/backend/migrations/118_wechat_dual_mode_and_auth_source_defaults.sql b/backend/migrations/118_wechat_dual_mode_and_auth_source_defaults.sql new file mode 100644 index 00000000..6eef59e2 --- /dev/null +++ b/backend/migrations/118_wechat_dual_mode_and_auth_source_defaults.sql @@ -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' +); diff --git a/frontend/src/api/__tests__/settings.authSourceDefaults.spec.ts b/frontend/src/api/__tests__/settings.authSourceDefaults.spec.ts index 8756146e..10f6247a 100644 --- a/frontend/src/api/__tests__/settings.authSourceDefaults.spec.ts +++ b/frontend/src/api/__tests__/settings.authSourceDefaults.spec.ts @@ -1,13 +1,13 @@ -import { describe, expect, it } from 'vitest' +import { describe, expect, it } from "vitest"; import { appendAuthSourceDefaultsToUpdateRequest, buildAuthSourceDefaultsState, type UpdateSettingsRequest, -} from '@/api/admin/settings' +} from "@/api/admin/settings"; -describe('admin settings auth source defaults helpers', () => { - it('builds auth source defaults state from flat settings fields', () => { +describe("admin settings auth source defaults helpers", () => { + it("builds auth source defaults state from flat settings fields", () => { const state = buildAuthSourceDefaultsState({ auth_source_default_email_balance: 9.5, 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_first_bind: false, - }) + }); expect(state.email).toEqual({ balance: 9.5, @@ -31,34 +31,43 @@ describe('admin settings auth source defaults helpers', () => { subscriptions: [{ group_id: 1, validity_days: 30 }], grant_on_signup: false, grant_on_first_bind: true, - }) + }); expect(state.linuxdo).toEqual({ balance: 6, concurrency: 8, subscriptions: [{ group_id: 2, validity_days: 60 }], grant_on_signup: true, grant_on_first_bind: false, - }) + }); expect(state.oidc).toEqual({ balance: 0, concurrency: 5, subscriptions: [], - grant_on_signup: true, + grant_on_signup: false, grant_on_first_bind: false, - }) + }); expect(state.wechat).toEqual({ balance: 0, concurrency: 5, subscriptions: [], - grant_on_signup: true, + grant_on_signup: 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 = { - site_name: 'Sub2API', - } + site_name: "Sub2API", + }; appendAuthSourceDefaultsToUpdateRequest(payload, { email: { @@ -89,13 +98,15 @@ describe('admin settings auth source defaults helpers', () => { grant_on_signup: false, grant_on_first_bind: false, }, - }) + }); expect(payload).toMatchObject({ - site_name: 'Sub2API', + site_name: "Sub2API", auth_source_default_email_balance: 1.25, 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_first_bind: false, 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_oidc_balance: 4, 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_first_bind: true, 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_grant_on_signup: false, auth_source_default_wechat_grant_on_first_bind: false, - }) - }) -}) + }); + }); +}); diff --git a/frontend/src/api/admin/settings.ts b/frontend/src/api/admin/settings.ts index ed78a1bc..500eb4dc 100644 --- a/frontend/src/api/admin/settings.ts +++ b/frontend/src/api/admin/settings.ts @@ -167,7 +167,7 @@ export function buildAuthSourceDefaultsState( : [], ), 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: raw[`auth_source_default_${source}_grant_on_first_bind`] === true, }; @@ -239,6 +239,33 @@ export function defaultWeChatConnectScopesForMode(mode: unknown): string { : "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 */ @@ -315,6 +342,8 @@ export interface SystemSettings { wechat_connect_enabled: boolean; wechat_connect_app_id: string; wechat_connect_app_secret_configured: boolean; + wechat_connect_open_enabled?: boolean; + wechat_connect_mp_enabled?: boolean; wechat_connect_mode: string; wechat_connect_scopes: string; wechat_connect_redirect_url: string; @@ -472,6 +501,8 @@ export interface UpdateSettingsRequest { wechat_connect_enabled?: boolean; wechat_connect_app_id?: string; wechat_connect_app_secret?: string; + wechat_connect_open_enabled?: boolean; + wechat_connect_mp_enabled?: boolean; wechat_connect_mode?: string; wechat_connect_scopes?: string; wechat_connect_redirect_url?: string; diff --git a/frontend/src/views/admin/SettingsView.vue b/frontend/src/views/admin/SettingsView.vue index 9612d9f9..d3683fcd 100644 --- a/frontend/src/views/admin/SettingsView.vue +++ b/frontend/src/views/admin/SettingsView.vue @@ -1408,7 +1408,7 @@ v-if="form.wechat_connect_enabled" class="space-y-6 border-t border-gray-100 pt-4 dark:border-dark-700" > -
+
+
-
+
+
- -

- {{ - localText( - "open 对应微信开放平台,mp 对应公众号/小程序授权。", - "open maps to WeChat Open Platform, mp maps to Official Account / Mini Program authorization.", - ) - }} -

-
-
- -
-
-
+ +
+
- {{ localText("Scopes", "Scopes") }} - - -

- {{ - localText( - "留空时会按模式自动回填默认值。", - "Leave empty to use the default scope for the selected mode.", - ) - }} -

+
+
+ {{ + localText( + "微信环境使用公众号", + "Use MP inside WeChat", + ) + }} +
+

+ {{ + localText( + "浏览器在微信内时,自动走公众号授权。", + "Use Official Account authorization inside the WeChat browser.", + ) + }} +

+
+ +
@@ -2246,83 +2251,77 @@
-
+
-
-
- {{ authSource.title }} +
+
+
+ {{ authSource.title }} +
+

+ {{ authSource.description }} +

-

- {{ authSource.description }} + +

+ +
+

+ {{ + 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.", + ) + }}

-
-
-
- - -
-
- - -
-
- -
-
+
-

- {{ - localText( - "来源首次注册成功后立即发放默认权益。", - "Grant default entitlements immediately after signup.", - ) - }} -

+ +
+
+ +
-
{{ 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.", ) }}

@@ -2354,11 +2353,7 @@ " />
-
-