diff --git a/backend/internal/handler/admin/setting_handler.go b/backend/internal/handler/admin/setting_handler.go index eb8b28a7..e6609c97 100644 --- a/backend/internal/handler/admin/setting_handler.go +++ b/backend/internal/handler/admin/setting_handler.go @@ -43,6 +43,15 @@ func scopesContainOpenID(scopes string) bool { return false } +func firstNonEmpty(values ...string) string { + for _, value := range values { + if trimmed := strings.TrimSpace(value); trimmed != "" { + return trimmed + } + } + return "" +} + // SettingHandler 系统设置处理器 type SettingHandler struct { settingService *service.SettingService @@ -99,126 +108,133 @@ func (h *SettingHandler) GetSettings(c *gin.Context) { } payload := dto.SystemSettings{ - RegistrationEnabled: settings.RegistrationEnabled, - EmailVerifyEnabled: settings.EmailVerifyEnabled, - RegistrationEmailSuffixWhitelist: settings.RegistrationEmailSuffixWhitelist, - PromoCodeEnabled: settings.PromoCodeEnabled, - PasswordResetEnabled: settings.PasswordResetEnabled, - FrontendURL: settings.FrontendURL, - InvitationCodeEnabled: settings.InvitationCodeEnabled, - TotpEnabled: settings.TotpEnabled, - TotpEncryptionKeyConfigured: h.settingService.IsTotpEncryptionKeyConfigured(), - SMTPHost: settings.SMTPHost, - SMTPPort: settings.SMTPPort, - SMTPUsername: settings.SMTPUsername, - SMTPPasswordConfigured: settings.SMTPPasswordConfigured, - SMTPFrom: settings.SMTPFrom, - SMTPFromName: settings.SMTPFromName, - SMTPUseTLS: settings.SMTPUseTLS, - TurnstileEnabled: settings.TurnstileEnabled, - TurnstileSiteKey: settings.TurnstileSiteKey, - TurnstileSecretKeyConfigured: settings.TurnstileSecretKeyConfigured, - LinuxDoConnectEnabled: settings.LinuxDoConnectEnabled, - LinuxDoConnectClientID: settings.LinuxDoConnectClientID, - LinuxDoConnectClientSecretConfigured: settings.LinuxDoConnectClientSecretConfigured, - LinuxDoConnectRedirectURL: settings.LinuxDoConnectRedirectURL, - WeChatConnectEnabled: settings.WeChatConnectEnabled, - WeChatConnectAppID: settings.WeChatConnectAppID, - WeChatConnectAppSecretConfigured: settings.WeChatConnectAppSecretConfigured, - WeChatConnectOpenEnabled: settings.WeChatConnectOpenEnabled, - WeChatConnectMPEnabled: settings.WeChatConnectMPEnabled, - WeChatConnectMode: settings.WeChatConnectMode, - WeChatConnectScopes: settings.WeChatConnectScopes, - WeChatConnectRedirectURL: settings.WeChatConnectRedirectURL, - WeChatConnectFrontendRedirectURL: settings.WeChatConnectFrontendRedirectURL, - OIDCConnectEnabled: settings.OIDCConnectEnabled, - OIDCConnectProviderName: settings.OIDCConnectProviderName, - OIDCConnectClientID: settings.OIDCConnectClientID, - OIDCConnectClientSecretConfigured: settings.OIDCConnectClientSecretConfigured, - OIDCConnectIssuerURL: settings.OIDCConnectIssuerURL, - OIDCConnectDiscoveryURL: settings.OIDCConnectDiscoveryURL, - OIDCConnectAuthorizeURL: settings.OIDCConnectAuthorizeURL, - OIDCConnectTokenURL: settings.OIDCConnectTokenURL, - OIDCConnectUserInfoURL: settings.OIDCConnectUserInfoURL, - OIDCConnectJWKSURL: settings.OIDCConnectJWKSURL, - OIDCConnectScopes: settings.OIDCConnectScopes, - OIDCConnectRedirectURL: settings.OIDCConnectRedirectURL, - OIDCConnectFrontendRedirectURL: settings.OIDCConnectFrontendRedirectURL, - OIDCConnectTokenAuthMethod: settings.OIDCConnectTokenAuthMethod, - OIDCConnectUsePKCE: settings.OIDCConnectUsePKCE, - OIDCConnectValidateIDToken: settings.OIDCConnectValidateIDToken, - OIDCConnectAllowedSigningAlgs: settings.OIDCConnectAllowedSigningAlgs, - OIDCConnectClockSkewSeconds: settings.OIDCConnectClockSkewSeconds, - OIDCConnectRequireEmailVerified: settings.OIDCConnectRequireEmailVerified, - OIDCConnectUserInfoEmailPath: settings.OIDCConnectUserInfoEmailPath, - OIDCConnectUserInfoIDPath: settings.OIDCConnectUserInfoIDPath, - OIDCConnectUserInfoUsernamePath: settings.OIDCConnectUserInfoUsernamePath, - SiteName: settings.SiteName, - SiteLogo: settings.SiteLogo, - SiteSubtitle: settings.SiteSubtitle, - APIBaseURL: settings.APIBaseURL, - ContactInfo: settings.ContactInfo, - DocURL: settings.DocURL, - HomeContent: settings.HomeContent, - HideCcsImportButton: settings.HideCcsImportButton, - PurchaseSubscriptionEnabled: settings.PurchaseSubscriptionEnabled, - PurchaseSubscriptionURL: settings.PurchaseSubscriptionURL, - TableDefaultPageSize: settings.TableDefaultPageSize, - TablePageSizeOptions: settings.TablePageSizeOptions, - CustomMenuItems: dto.ParseCustomMenuItems(settings.CustomMenuItems), - CustomEndpoints: dto.ParseCustomEndpoints(settings.CustomEndpoints), - DefaultConcurrency: settings.DefaultConcurrency, - DefaultBalance: settings.DefaultBalance, - DefaultSubscriptions: defaultSubscriptions, - EnableModelFallback: settings.EnableModelFallback, - FallbackModelAnthropic: settings.FallbackModelAnthropic, - FallbackModelOpenAI: settings.FallbackModelOpenAI, - FallbackModelGemini: settings.FallbackModelGemini, - FallbackModelAntigravity: settings.FallbackModelAntigravity, - EnableIdentityPatch: settings.EnableIdentityPatch, - IdentityPatchPrompt: settings.IdentityPatchPrompt, - OpsMonitoringEnabled: opsEnabled && settings.OpsMonitoringEnabled, - OpsRealtimeMonitoringEnabled: settings.OpsRealtimeMonitoringEnabled, - OpsQueryModeDefault: settings.OpsQueryModeDefault, - OpsMetricsIntervalSeconds: settings.OpsMetricsIntervalSeconds, - MinClaudeCodeVersion: settings.MinClaudeCodeVersion, - MaxClaudeCodeVersion: settings.MaxClaudeCodeVersion, - AllowUngroupedKeyScheduling: settings.AllowUngroupedKeyScheduling, - BackendModeEnabled: settings.BackendModeEnabled, - EnableFingerprintUnification: settings.EnableFingerprintUnification, - EnableMetadataPassthrough: settings.EnableMetadataPassthrough, - EnableCCHSigning: settings.EnableCCHSigning, - WebSearchEmulationEnabled: settings.WebSearchEmulationEnabled, - PaymentVisibleMethodAlipaySource: settings.PaymentVisibleMethodAlipaySource, - PaymentVisibleMethodWxpaySource: settings.PaymentVisibleMethodWxpaySource, - PaymentVisibleMethodAlipayEnabled: settings.PaymentVisibleMethodAlipayEnabled, - PaymentVisibleMethodWxpayEnabled: settings.PaymentVisibleMethodWxpayEnabled, - OpenAIAdvancedSchedulerEnabled: settings.OpenAIAdvancedSchedulerEnabled, - BalanceLowNotifyEnabled: settings.BalanceLowNotifyEnabled, - BalanceLowNotifyThreshold: settings.BalanceLowNotifyThreshold, - BalanceLowNotifyRechargeURL: settings.BalanceLowNotifyRechargeURL, - AccountQuotaNotifyEnabled: settings.AccountQuotaNotifyEnabled, - AccountQuotaNotifyEmails: dto.NotifyEmailEntriesFromService(settings.AccountQuotaNotifyEmails), - PaymentEnabled: paymentCfg.Enabled, - PaymentMinAmount: paymentCfg.MinAmount, - PaymentMaxAmount: paymentCfg.MaxAmount, - PaymentDailyLimit: paymentCfg.DailyLimit, - PaymentOrderTimeoutMin: paymentCfg.OrderTimeoutMin, - PaymentMaxPendingOrders: paymentCfg.MaxPendingOrders, - PaymentEnabledTypes: paymentCfg.EnabledTypes, - PaymentBalanceDisabled: paymentCfg.BalanceDisabled, - PaymentBalanceRechargeMultiplier: paymentCfg.BalanceRechargeMultiplier, - PaymentRechargeFeeRate: paymentCfg.RechargeFeeRate, - PaymentLoadBalanceStrat: paymentCfg.LoadBalanceStrategy, - PaymentProductNamePrefix: paymentCfg.ProductNamePrefix, - PaymentProductNameSuffix: paymentCfg.ProductNameSuffix, - PaymentHelpImageURL: paymentCfg.HelpImageURL, - PaymentHelpText: paymentCfg.HelpText, - PaymentCancelRateLimitEnabled: paymentCfg.CancelRateLimitEnabled, - PaymentCancelRateLimitMax: paymentCfg.CancelRateLimitMax, - PaymentCancelRateLimitWindow: paymentCfg.CancelRateLimitWindow, - PaymentCancelRateLimitUnit: paymentCfg.CancelRateLimitUnit, - PaymentCancelRateLimitMode: paymentCfg.CancelRateLimitMode, + RegistrationEnabled: settings.RegistrationEnabled, + EmailVerifyEnabled: settings.EmailVerifyEnabled, + RegistrationEmailSuffixWhitelist: settings.RegistrationEmailSuffixWhitelist, + PromoCodeEnabled: settings.PromoCodeEnabled, + PasswordResetEnabled: settings.PasswordResetEnabled, + FrontendURL: settings.FrontendURL, + InvitationCodeEnabled: settings.InvitationCodeEnabled, + TotpEnabled: settings.TotpEnabled, + TotpEncryptionKeyConfigured: h.settingService.IsTotpEncryptionKeyConfigured(), + SMTPHost: settings.SMTPHost, + SMTPPort: settings.SMTPPort, + SMTPUsername: settings.SMTPUsername, + SMTPPasswordConfigured: settings.SMTPPasswordConfigured, + SMTPFrom: settings.SMTPFrom, + SMTPFromName: settings.SMTPFromName, + SMTPUseTLS: settings.SMTPUseTLS, + TurnstileEnabled: settings.TurnstileEnabled, + TurnstileSiteKey: settings.TurnstileSiteKey, + TurnstileSecretKeyConfigured: settings.TurnstileSecretKeyConfigured, + LinuxDoConnectEnabled: settings.LinuxDoConnectEnabled, + LinuxDoConnectClientID: settings.LinuxDoConnectClientID, + LinuxDoConnectClientSecretConfigured: settings.LinuxDoConnectClientSecretConfigured, + LinuxDoConnectRedirectURL: settings.LinuxDoConnectRedirectURL, + WeChatConnectEnabled: settings.WeChatConnectEnabled, + WeChatConnectAppID: settings.WeChatConnectAppID, + WeChatConnectAppSecretConfigured: settings.WeChatConnectAppSecretConfigured, + WeChatConnectOpenAppID: settings.WeChatConnectOpenAppID, + WeChatConnectOpenAppSecretConfigured: settings.WeChatConnectOpenAppSecretConfigured, + WeChatConnectMPAppID: settings.WeChatConnectMPAppID, + WeChatConnectMPAppSecretConfigured: settings.WeChatConnectMPAppSecretConfigured, + WeChatConnectMobileAppID: settings.WeChatConnectMobileAppID, + WeChatConnectMobileAppSecretConfigured: settings.WeChatConnectMobileAppSecretConfigured, + WeChatConnectOpenEnabled: settings.WeChatConnectOpenEnabled, + WeChatConnectMPEnabled: settings.WeChatConnectMPEnabled, + WeChatConnectMobileEnabled: settings.WeChatConnectMobileEnabled, + WeChatConnectMode: settings.WeChatConnectMode, + WeChatConnectScopes: settings.WeChatConnectScopes, + WeChatConnectRedirectURL: settings.WeChatConnectRedirectURL, + WeChatConnectFrontendRedirectURL: settings.WeChatConnectFrontendRedirectURL, + OIDCConnectEnabled: settings.OIDCConnectEnabled, + OIDCConnectProviderName: settings.OIDCConnectProviderName, + OIDCConnectClientID: settings.OIDCConnectClientID, + OIDCConnectClientSecretConfigured: settings.OIDCConnectClientSecretConfigured, + OIDCConnectIssuerURL: settings.OIDCConnectIssuerURL, + OIDCConnectDiscoveryURL: settings.OIDCConnectDiscoveryURL, + OIDCConnectAuthorizeURL: settings.OIDCConnectAuthorizeURL, + OIDCConnectTokenURL: settings.OIDCConnectTokenURL, + OIDCConnectUserInfoURL: settings.OIDCConnectUserInfoURL, + OIDCConnectJWKSURL: settings.OIDCConnectJWKSURL, + OIDCConnectScopes: settings.OIDCConnectScopes, + OIDCConnectRedirectURL: settings.OIDCConnectRedirectURL, + OIDCConnectFrontendRedirectURL: settings.OIDCConnectFrontendRedirectURL, + OIDCConnectTokenAuthMethod: settings.OIDCConnectTokenAuthMethod, + OIDCConnectUsePKCE: settings.OIDCConnectUsePKCE, + OIDCConnectValidateIDToken: settings.OIDCConnectValidateIDToken, + OIDCConnectAllowedSigningAlgs: settings.OIDCConnectAllowedSigningAlgs, + OIDCConnectClockSkewSeconds: settings.OIDCConnectClockSkewSeconds, + OIDCConnectRequireEmailVerified: settings.OIDCConnectRequireEmailVerified, + OIDCConnectUserInfoEmailPath: settings.OIDCConnectUserInfoEmailPath, + OIDCConnectUserInfoIDPath: settings.OIDCConnectUserInfoIDPath, + OIDCConnectUserInfoUsernamePath: settings.OIDCConnectUserInfoUsernamePath, + SiteName: settings.SiteName, + SiteLogo: settings.SiteLogo, + SiteSubtitle: settings.SiteSubtitle, + APIBaseURL: settings.APIBaseURL, + ContactInfo: settings.ContactInfo, + DocURL: settings.DocURL, + HomeContent: settings.HomeContent, + HideCcsImportButton: settings.HideCcsImportButton, + PurchaseSubscriptionEnabled: settings.PurchaseSubscriptionEnabled, + PurchaseSubscriptionURL: settings.PurchaseSubscriptionURL, + TableDefaultPageSize: settings.TableDefaultPageSize, + TablePageSizeOptions: settings.TablePageSizeOptions, + CustomMenuItems: dto.ParseCustomMenuItems(settings.CustomMenuItems), + CustomEndpoints: dto.ParseCustomEndpoints(settings.CustomEndpoints), + DefaultConcurrency: settings.DefaultConcurrency, + DefaultBalance: settings.DefaultBalance, + DefaultSubscriptions: defaultSubscriptions, + EnableModelFallback: settings.EnableModelFallback, + FallbackModelAnthropic: settings.FallbackModelAnthropic, + FallbackModelOpenAI: settings.FallbackModelOpenAI, + FallbackModelGemini: settings.FallbackModelGemini, + FallbackModelAntigravity: settings.FallbackModelAntigravity, + EnableIdentityPatch: settings.EnableIdentityPatch, + IdentityPatchPrompt: settings.IdentityPatchPrompt, + OpsMonitoringEnabled: opsEnabled && settings.OpsMonitoringEnabled, + OpsRealtimeMonitoringEnabled: settings.OpsRealtimeMonitoringEnabled, + OpsQueryModeDefault: settings.OpsQueryModeDefault, + OpsMetricsIntervalSeconds: settings.OpsMetricsIntervalSeconds, + MinClaudeCodeVersion: settings.MinClaudeCodeVersion, + MaxClaudeCodeVersion: settings.MaxClaudeCodeVersion, + AllowUngroupedKeyScheduling: settings.AllowUngroupedKeyScheduling, + BackendModeEnabled: settings.BackendModeEnabled, + EnableFingerprintUnification: settings.EnableFingerprintUnification, + EnableMetadataPassthrough: settings.EnableMetadataPassthrough, + EnableCCHSigning: settings.EnableCCHSigning, + WebSearchEmulationEnabled: settings.WebSearchEmulationEnabled, + PaymentVisibleMethodAlipaySource: settings.PaymentVisibleMethodAlipaySource, + PaymentVisibleMethodWxpaySource: settings.PaymentVisibleMethodWxpaySource, + PaymentVisibleMethodAlipayEnabled: settings.PaymentVisibleMethodAlipayEnabled, + PaymentVisibleMethodWxpayEnabled: settings.PaymentVisibleMethodWxpayEnabled, + OpenAIAdvancedSchedulerEnabled: settings.OpenAIAdvancedSchedulerEnabled, + BalanceLowNotifyEnabled: settings.BalanceLowNotifyEnabled, + BalanceLowNotifyThreshold: settings.BalanceLowNotifyThreshold, + BalanceLowNotifyRechargeURL: settings.BalanceLowNotifyRechargeURL, + AccountQuotaNotifyEnabled: settings.AccountQuotaNotifyEnabled, + AccountQuotaNotifyEmails: dto.NotifyEmailEntriesFromService(settings.AccountQuotaNotifyEmails), + PaymentEnabled: paymentCfg.Enabled, + PaymentMinAmount: paymentCfg.MinAmount, + PaymentMaxAmount: paymentCfg.MaxAmount, + PaymentDailyLimit: paymentCfg.DailyLimit, + PaymentOrderTimeoutMin: paymentCfg.OrderTimeoutMin, + PaymentMaxPendingOrders: paymentCfg.MaxPendingOrders, + PaymentEnabledTypes: paymentCfg.EnabledTypes, + PaymentBalanceDisabled: paymentCfg.BalanceDisabled, + PaymentBalanceRechargeMultiplier: paymentCfg.BalanceRechargeMultiplier, + PaymentRechargeFeeRate: paymentCfg.RechargeFeeRate, + PaymentLoadBalanceStrat: paymentCfg.LoadBalanceStrategy, + PaymentProductNamePrefix: paymentCfg.ProductNamePrefix, + PaymentProductNameSuffix: paymentCfg.ProductNameSuffix, + PaymentHelpImageURL: paymentCfg.HelpImageURL, + PaymentHelpText: paymentCfg.HelpText, + PaymentCancelRateLimitEnabled: paymentCfg.CancelRateLimitEnabled, + PaymentCancelRateLimitMax: paymentCfg.CancelRateLimitMax, + PaymentCancelRateLimitWindow: paymentCfg.CancelRateLimitWindow, + PaymentCancelRateLimitUnit: paymentCfg.CancelRateLimitUnit, + PaymentCancelRateLimitMode: paymentCfg.CancelRateLimitMode, } response.Success(c, systemSettingsResponseData(payload, authSourceDefaults)) } @@ -259,8 +275,15 @@ type UpdateSettingsRequest struct { WeChatConnectEnabled bool `json:"wechat_connect_enabled"` WeChatConnectAppID string `json:"wechat_connect_app_id"` WeChatConnectAppSecret string `json:"wechat_connect_app_secret"` + WeChatConnectOpenAppID string `json:"wechat_connect_open_app_id"` + WeChatConnectOpenAppSecret string `json:"wechat_connect_open_app_secret"` + WeChatConnectMPAppID string `json:"wechat_connect_mp_app_id"` + WeChatConnectMPAppSecret string `json:"wechat_connect_mp_app_secret"` + WeChatConnectMobileAppID string `json:"wechat_connect_mobile_app_id"` + WeChatConnectMobileAppSecret string `json:"wechat_connect_mobile_app_secret"` WeChatConnectOpenEnabled bool `json:"wechat_connect_open_enabled"` WeChatConnectMPEnabled bool `json:"wechat_connect_mp_enabled"` + WeChatConnectMobileEnabled bool `json:"wechat_connect_mobile_enabled"` WeChatConnectMode string `json:"wechat_connect_mode"` WeChatConnectScopes string `json:"wechat_connect_scopes"` WeChatConnectRedirectURL string `json:"wechat_connect_redirect_url"` @@ -532,34 +555,35 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) { if req.WeChatConnectEnabled { req.WeChatConnectAppID = strings.TrimSpace(req.WeChatConnectAppID) req.WeChatConnectAppSecret = strings.TrimSpace(req.WeChatConnectAppSecret) + req.WeChatConnectOpenAppID = strings.TrimSpace(req.WeChatConnectOpenAppID) + req.WeChatConnectOpenAppSecret = strings.TrimSpace(req.WeChatConnectOpenAppSecret) + req.WeChatConnectMPAppID = strings.TrimSpace(req.WeChatConnectMPAppID) + req.WeChatConnectMPAppSecret = strings.TrimSpace(req.WeChatConnectMPAppSecret) + req.WeChatConnectMobileAppID = strings.TrimSpace(req.WeChatConnectMobileAppID) + req.WeChatConnectMobileAppSecret = strings.TrimSpace(req.WeChatConnectMobileAppSecret) 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") + if req.WeChatConnectMPEnabled && req.WeChatConnectMobileEnabled { + response.BadRequest(c, "WeChat Official Account and Mobile App cannot be enabled at the same time") 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 != "" { switch req.WeChatConnectMode { - case "open", "mp": + case "open", "mp", "mobile": default: - response.BadRequest(c, "WeChat mode must be open or mp") + response.BadRequest(c, "WeChat mode must be open, mp, or mobile") return } } - if !req.WeChatConnectOpenEnabled && !req.WeChatConnectMPEnabled { + if !req.WeChatConnectOpenEnabled && !req.WeChatConnectMPEnabled && !req.WeChatConnectMobileEnabled { switch req.WeChatConnectMode { case "mp": req.WeChatConnectMPEnabled = true + case "mobile": + req.WeChatConnectMobileEnabled = true default: req.WeChatConnectOpenEnabled = true } @@ -567,10 +591,61 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) { if req.WeChatConnectMode == "" { if req.WeChatConnectMPEnabled { req.WeChatConnectMode = "mp" + } else if req.WeChatConnectMobileEnabled { + req.WeChatConnectMode = "mobile" } else { req.WeChatConnectMode = "open" } } + + req.WeChatConnectOpenAppID = strings.TrimSpace(firstNonEmpty(req.WeChatConnectOpenAppID, req.WeChatConnectAppID)) + req.WeChatConnectMPAppID = strings.TrimSpace(firstNonEmpty(req.WeChatConnectMPAppID, req.WeChatConnectAppID)) + req.WeChatConnectMobileAppID = strings.TrimSpace(firstNonEmpty(req.WeChatConnectMobileAppID, req.WeChatConnectAppID)) + + if req.WeChatConnectOpenAppSecret == "" { + req.WeChatConnectOpenAppSecret = strings.TrimSpace(firstNonEmpty(previousSettings.WeChatConnectOpenAppSecret, previousSettings.WeChatConnectAppSecret, req.WeChatConnectAppSecret)) + } + if req.WeChatConnectMPAppSecret == "" { + req.WeChatConnectMPAppSecret = strings.TrimSpace(firstNonEmpty(previousSettings.WeChatConnectMPAppSecret, previousSettings.WeChatConnectAppSecret, req.WeChatConnectAppSecret)) + } + if req.WeChatConnectMobileAppSecret == "" { + req.WeChatConnectMobileAppSecret = strings.TrimSpace(firstNonEmpty(previousSettings.WeChatConnectMobileAppSecret, previousSettings.WeChatConnectAppSecret, req.WeChatConnectAppSecret)) + } + if req.WeChatConnectAppSecret == "" { + req.WeChatConnectAppSecret = strings.TrimSpace(firstNonEmpty(req.WeChatConnectOpenAppSecret, req.WeChatConnectMPAppSecret, req.WeChatConnectMobileAppSecret, previousSettings.WeChatConnectAppSecret)) + } + + if req.WeChatConnectOpenEnabled { + if req.WeChatConnectOpenAppID == "" { + response.BadRequest(c, "WeChat PC App ID is required when enabled") + return + } + if req.WeChatConnectOpenAppSecret == "" { + response.BadRequest(c, "WeChat PC App Secret is required when enabled") + return + } + } + if req.WeChatConnectMPEnabled { + if req.WeChatConnectMPAppID == "" { + response.BadRequest(c, "WeChat Official Account App ID is required when enabled") + return + } + if req.WeChatConnectMPAppSecret == "" { + response.BadRequest(c, "WeChat Official Account App Secret is required when enabled") + return + } + } + if req.WeChatConnectMobileEnabled { + if req.WeChatConnectMobileAppID == "" { + response.BadRequest(c, "WeChat Mobile App ID is required when enabled") + return + } + if req.WeChatConnectMobileAppSecret == "" { + response.BadRequest(c, "WeChat Mobile App Secret is required when enabled") + return + } + } + if req.WeChatConnectScopes == "" { if req.WeChatConnectMPEnabled { req.WeChatConnectScopes = service.DefaultWeChatConnectScopesForMode("mp") @@ -946,8 +1021,15 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) { WeChatConnectEnabled: req.WeChatConnectEnabled, WeChatConnectAppID: req.WeChatConnectAppID, WeChatConnectAppSecret: req.WeChatConnectAppSecret, + WeChatConnectOpenAppID: req.WeChatConnectOpenAppID, + WeChatConnectOpenAppSecret: req.WeChatConnectOpenAppSecret, + WeChatConnectMPAppID: req.WeChatConnectMPAppID, + WeChatConnectMPAppSecret: req.WeChatConnectMPAppSecret, + WeChatConnectMobileAppID: req.WeChatConnectMobileAppID, + WeChatConnectMobileAppSecret: req.WeChatConnectMobileAppSecret, WeChatConnectOpenEnabled: req.WeChatConnectOpenEnabled, WeChatConnectMPEnabled: req.WeChatConnectMPEnabled, + WeChatConnectMobileEnabled: req.WeChatConnectMobileEnabled, WeChatConnectMode: req.WeChatConnectMode, WeChatConnectScopes: req.WeChatConnectScopes, WeChatConnectRedirectURL: req.WeChatConnectRedirectURL, @@ -1208,125 +1290,132 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) { } payload := dto.SystemSettings{ - RegistrationEnabled: updatedSettings.RegistrationEnabled, - EmailVerifyEnabled: updatedSettings.EmailVerifyEnabled, - RegistrationEmailSuffixWhitelist: updatedSettings.RegistrationEmailSuffixWhitelist, - PromoCodeEnabled: updatedSettings.PromoCodeEnabled, - PasswordResetEnabled: updatedSettings.PasswordResetEnabled, - FrontendURL: updatedSettings.FrontendURL, - InvitationCodeEnabled: updatedSettings.InvitationCodeEnabled, - TotpEnabled: updatedSettings.TotpEnabled, - TotpEncryptionKeyConfigured: h.settingService.IsTotpEncryptionKeyConfigured(), - SMTPHost: updatedSettings.SMTPHost, - SMTPPort: updatedSettings.SMTPPort, - SMTPUsername: updatedSettings.SMTPUsername, - SMTPPasswordConfigured: updatedSettings.SMTPPasswordConfigured, - SMTPFrom: updatedSettings.SMTPFrom, - SMTPFromName: updatedSettings.SMTPFromName, - SMTPUseTLS: updatedSettings.SMTPUseTLS, - TurnstileEnabled: updatedSettings.TurnstileEnabled, - TurnstileSiteKey: updatedSettings.TurnstileSiteKey, - TurnstileSecretKeyConfigured: updatedSettings.TurnstileSecretKeyConfigured, - LinuxDoConnectEnabled: updatedSettings.LinuxDoConnectEnabled, - LinuxDoConnectClientID: updatedSettings.LinuxDoConnectClientID, - LinuxDoConnectClientSecretConfigured: updatedSettings.LinuxDoConnectClientSecretConfigured, - LinuxDoConnectRedirectURL: updatedSettings.LinuxDoConnectRedirectURL, - WeChatConnectEnabled: updatedSettings.WeChatConnectEnabled, - WeChatConnectAppID: updatedSettings.WeChatConnectAppID, - WeChatConnectAppSecretConfigured: updatedSettings.WeChatConnectAppSecretConfigured, - WeChatConnectOpenEnabled: updatedSettings.WeChatConnectOpenEnabled, - WeChatConnectMPEnabled: updatedSettings.WeChatConnectMPEnabled, - WeChatConnectMode: updatedSettings.WeChatConnectMode, - WeChatConnectScopes: updatedSettings.WeChatConnectScopes, - WeChatConnectRedirectURL: updatedSettings.WeChatConnectRedirectURL, - WeChatConnectFrontendRedirectURL: updatedSettings.WeChatConnectFrontendRedirectURL, - OIDCConnectEnabled: updatedSettings.OIDCConnectEnabled, - OIDCConnectProviderName: updatedSettings.OIDCConnectProviderName, - OIDCConnectClientID: updatedSettings.OIDCConnectClientID, - OIDCConnectClientSecretConfigured: updatedSettings.OIDCConnectClientSecretConfigured, - OIDCConnectIssuerURL: updatedSettings.OIDCConnectIssuerURL, - OIDCConnectDiscoveryURL: updatedSettings.OIDCConnectDiscoveryURL, - OIDCConnectAuthorizeURL: updatedSettings.OIDCConnectAuthorizeURL, - OIDCConnectTokenURL: updatedSettings.OIDCConnectTokenURL, - OIDCConnectUserInfoURL: updatedSettings.OIDCConnectUserInfoURL, - OIDCConnectJWKSURL: updatedSettings.OIDCConnectJWKSURL, - OIDCConnectScopes: updatedSettings.OIDCConnectScopes, - OIDCConnectRedirectURL: updatedSettings.OIDCConnectRedirectURL, - OIDCConnectFrontendRedirectURL: updatedSettings.OIDCConnectFrontendRedirectURL, - OIDCConnectTokenAuthMethod: updatedSettings.OIDCConnectTokenAuthMethod, - OIDCConnectUsePKCE: updatedSettings.OIDCConnectUsePKCE, - OIDCConnectValidateIDToken: updatedSettings.OIDCConnectValidateIDToken, - OIDCConnectAllowedSigningAlgs: updatedSettings.OIDCConnectAllowedSigningAlgs, - OIDCConnectClockSkewSeconds: updatedSettings.OIDCConnectClockSkewSeconds, - OIDCConnectRequireEmailVerified: updatedSettings.OIDCConnectRequireEmailVerified, - OIDCConnectUserInfoEmailPath: updatedSettings.OIDCConnectUserInfoEmailPath, - OIDCConnectUserInfoIDPath: updatedSettings.OIDCConnectUserInfoIDPath, - OIDCConnectUserInfoUsernamePath: updatedSettings.OIDCConnectUserInfoUsernamePath, - SiteName: updatedSettings.SiteName, - SiteLogo: updatedSettings.SiteLogo, - SiteSubtitle: updatedSettings.SiteSubtitle, - APIBaseURL: updatedSettings.APIBaseURL, - ContactInfo: updatedSettings.ContactInfo, - DocURL: updatedSettings.DocURL, - HomeContent: updatedSettings.HomeContent, - HideCcsImportButton: updatedSettings.HideCcsImportButton, - PurchaseSubscriptionEnabled: updatedSettings.PurchaseSubscriptionEnabled, - PurchaseSubscriptionURL: updatedSettings.PurchaseSubscriptionURL, - TableDefaultPageSize: updatedSettings.TableDefaultPageSize, - TablePageSizeOptions: updatedSettings.TablePageSizeOptions, - CustomMenuItems: dto.ParseCustomMenuItems(updatedSettings.CustomMenuItems), - CustomEndpoints: dto.ParseCustomEndpoints(updatedSettings.CustomEndpoints), - DefaultConcurrency: updatedSettings.DefaultConcurrency, - DefaultBalance: updatedSettings.DefaultBalance, - DefaultSubscriptions: updatedDefaultSubscriptions, - EnableModelFallback: updatedSettings.EnableModelFallback, - FallbackModelAnthropic: updatedSettings.FallbackModelAnthropic, - FallbackModelOpenAI: updatedSettings.FallbackModelOpenAI, - FallbackModelGemini: updatedSettings.FallbackModelGemini, - FallbackModelAntigravity: updatedSettings.FallbackModelAntigravity, - EnableIdentityPatch: updatedSettings.EnableIdentityPatch, - IdentityPatchPrompt: updatedSettings.IdentityPatchPrompt, - OpsMonitoringEnabled: updatedSettings.OpsMonitoringEnabled, - OpsRealtimeMonitoringEnabled: updatedSettings.OpsRealtimeMonitoringEnabled, - OpsQueryModeDefault: updatedSettings.OpsQueryModeDefault, - OpsMetricsIntervalSeconds: updatedSettings.OpsMetricsIntervalSeconds, - MinClaudeCodeVersion: updatedSettings.MinClaudeCodeVersion, - MaxClaudeCodeVersion: updatedSettings.MaxClaudeCodeVersion, - AllowUngroupedKeyScheduling: updatedSettings.AllowUngroupedKeyScheduling, - BackendModeEnabled: updatedSettings.BackendModeEnabled, - EnableFingerprintUnification: updatedSettings.EnableFingerprintUnification, - EnableMetadataPassthrough: updatedSettings.EnableMetadataPassthrough, - EnableCCHSigning: updatedSettings.EnableCCHSigning, - PaymentVisibleMethodAlipaySource: updatedSettings.PaymentVisibleMethodAlipaySource, - PaymentVisibleMethodWxpaySource: updatedSettings.PaymentVisibleMethodWxpaySource, - PaymentVisibleMethodAlipayEnabled: updatedSettings.PaymentVisibleMethodAlipayEnabled, - PaymentVisibleMethodWxpayEnabled: updatedSettings.PaymentVisibleMethodWxpayEnabled, - OpenAIAdvancedSchedulerEnabled: updatedSettings.OpenAIAdvancedSchedulerEnabled, - BalanceLowNotifyEnabled: updatedSettings.BalanceLowNotifyEnabled, - BalanceLowNotifyThreshold: updatedSettings.BalanceLowNotifyThreshold, - BalanceLowNotifyRechargeURL: updatedSettings.BalanceLowNotifyRechargeURL, - AccountQuotaNotifyEnabled: updatedSettings.AccountQuotaNotifyEnabled, - AccountQuotaNotifyEmails: dto.NotifyEmailEntriesFromService(updatedSettings.AccountQuotaNotifyEmails), - PaymentEnabled: updatedPaymentCfg.Enabled, - PaymentMinAmount: updatedPaymentCfg.MinAmount, - PaymentMaxAmount: updatedPaymentCfg.MaxAmount, - PaymentDailyLimit: updatedPaymentCfg.DailyLimit, - PaymentOrderTimeoutMin: updatedPaymentCfg.OrderTimeoutMin, - PaymentMaxPendingOrders: updatedPaymentCfg.MaxPendingOrders, - PaymentEnabledTypes: updatedPaymentCfg.EnabledTypes, - PaymentBalanceDisabled: updatedPaymentCfg.BalanceDisabled, - PaymentBalanceRechargeMultiplier: updatedPaymentCfg.BalanceRechargeMultiplier, - PaymentRechargeFeeRate: updatedPaymentCfg.RechargeFeeRate, - PaymentLoadBalanceStrat: updatedPaymentCfg.LoadBalanceStrategy, - PaymentProductNamePrefix: updatedPaymentCfg.ProductNamePrefix, - PaymentProductNameSuffix: updatedPaymentCfg.ProductNameSuffix, - PaymentHelpImageURL: updatedPaymentCfg.HelpImageURL, - PaymentHelpText: updatedPaymentCfg.HelpText, - PaymentCancelRateLimitEnabled: updatedPaymentCfg.CancelRateLimitEnabled, - PaymentCancelRateLimitMax: updatedPaymentCfg.CancelRateLimitMax, - PaymentCancelRateLimitWindow: updatedPaymentCfg.CancelRateLimitWindow, - PaymentCancelRateLimitUnit: updatedPaymentCfg.CancelRateLimitUnit, - PaymentCancelRateLimitMode: updatedPaymentCfg.CancelRateLimitMode, + RegistrationEnabled: updatedSettings.RegistrationEnabled, + EmailVerifyEnabled: updatedSettings.EmailVerifyEnabled, + RegistrationEmailSuffixWhitelist: updatedSettings.RegistrationEmailSuffixWhitelist, + PromoCodeEnabled: updatedSettings.PromoCodeEnabled, + PasswordResetEnabled: updatedSettings.PasswordResetEnabled, + FrontendURL: updatedSettings.FrontendURL, + InvitationCodeEnabled: updatedSettings.InvitationCodeEnabled, + TotpEnabled: updatedSettings.TotpEnabled, + TotpEncryptionKeyConfigured: h.settingService.IsTotpEncryptionKeyConfigured(), + SMTPHost: updatedSettings.SMTPHost, + SMTPPort: updatedSettings.SMTPPort, + SMTPUsername: updatedSettings.SMTPUsername, + SMTPPasswordConfigured: updatedSettings.SMTPPasswordConfigured, + SMTPFrom: updatedSettings.SMTPFrom, + SMTPFromName: updatedSettings.SMTPFromName, + SMTPUseTLS: updatedSettings.SMTPUseTLS, + TurnstileEnabled: updatedSettings.TurnstileEnabled, + TurnstileSiteKey: updatedSettings.TurnstileSiteKey, + TurnstileSecretKeyConfigured: updatedSettings.TurnstileSecretKeyConfigured, + LinuxDoConnectEnabled: updatedSettings.LinuxDoConnectEnabled, + LinuxDoConnectClientID: updatedSettings.LinuxDoConnectClientID, + LinuxDoConnectClientSecretConfigured: updatedSettings.LinuxDoConnectClientSecretConfigured, + LinuxDoConnectRedirectURL: updatedSettings.LinuxDoConnectRedirectURL, + WeChatConnectEnabled: updatedSettings.WeChatConnectEnabled, + WeChatConnectAppID: updatedSettings.WeChatConnectAppID, + WeChatConnectAppSecretConfigured: updatedSettings.WeChatConnectAppSecretConfigured, + WeChatConnectOpenAppID: updatedSettings.WeChatConnectOpenAppID, + WeChatConnectOpenAppSecretConfigured: updatedSettings.WeChatConnectOpenAppSecretConfigured, + WeChatConnectMPAppID: updatedSettings.WeChatConnectMPAppID, + WeChatConnectMPAppSecretConfigured: updatedSettings.WeChatConnectMPAppSecretConfigured, + WeChatConnectMobileAppID: updatedSettings.WeChatConnectMobileAppID, + WeChatConnectMobileAppSecretConfigured: updatedSettings.WeChatConnectMobileAppSecretConfigured, + WeChatConnectOpenEnabled: updatedSettings.WeChatConnectOpenEnabled, + WeChatConnectMPEnabled: updatedSettings.WeChatConnectMPEnabled, + WeChatConnectMobileEnabled: updatedSettings.WeChatConnectMobileEnabled, + WeChatConnectMode: updatedSettings.WeChatConnectMode, + WeChatConnectScopes: updatedSettings.WeChatConnectScopes, + WeChatConnectRedirectURL: updatedSettings.WeChatConnectRedirectURL, + WeChatConnectFrontendRedirectURL: updatedSettings.WeChatConnectFrontendRedirectURL, + OIDCConnectEnabled: updatedSettings.OIDCConnectEnabled, + OIDCConnectProviderName: updatedSettings.OIDCConnectProviderName, + OIDCConnectClientID: updatedSettings.OIDCConnectClientID, + OIDCConnectClientSecretConfigured: updatedSettings.OIDCConnectClientSecretConfigured, + OIDCConnectIssuerURL: updatedSettings.OIDCConnectIssuerURL, + OIDCConnectDiscoveryURL: updatedSettings.OIDCConnectDiscoveryURL, + OIDCConnectAuthorizeURL: updatedSettings.OIDCConnectAuthorizeURL, + OIDCConnectTokenURL: updatedSettings.OIDCConnectTokenURL, + OIDCConnectUserInfoURL: updatedSettings.OIDCConnectUserInfoURL, + OIDCConnectJWKSURL: updatedSettings.OIDCConnectJWKSURL, + OIDCConnectScopes: updatedSettings.OIDCConnectScopes, + OIDCConnectRedirectURL: updatedSettings.OIDCConnectRedirectURL, + OIDCConnectFrontendRedirectURL: updatedSettings.OIDCConnectFrontendRedirectURL, + OIDCConnectTokenAuthMethod: updatedSettings.OIDCConnectTokenAuthMethod, + OIDCConnectUsePKCE: updatedSettings.OIDCConnectUsePKCE, + OIDCConnectValidateIDToken: updatedSettings.OIDCConnectValidateIDToken, + OIDCConnectAllowedSigningAlgs: updatedSettings.OIDCConnectAllowedSigningAlgs, + OIDCConnectClockSkewSeconds: updatedSettings.OIDCConnectClockSkewSeconds, + OIDCConnectRequireEmailVerified: updatedSettings.OIDCConnectRequireEmailVerified, + OIDCConnectUserInfoEmailPath: updatedSettings.OIDCConnectUserInfoEmailPath, + OIDCConnectUserInfoIDPath: updatedSettings.OIDCConnectUserInfoIDPath, + OIDCConnectUserInfoUsernamePath: updatedSettings.OIDCConnectUserInfoUsernamePath, + SiteName: updatedSettings.SiteName, + SiteLogo: updatedSettings.SiteLogo, + SiteSubtitle: updatedSettings.SiteSubtitle, + APIBaseURL: updatedSettings.APIBaseURL, + ContactInfo: updatedSettings.ContactInfo, + DocURL: updatedSettings.DocURL, + HomeContent: updatedSettings.HomeContent, + HideCcsImportButton: updatedSettings.HideCcsImportButton, + PurchaseSubscriptionEnabled: updatedSettings.PurchaseSubscriptionEnabled, + PurchaseSubscriptionURL: updatedSettings.PurchaseSubscriptionURL, + TableDefaultPageSize: updatedSettings.TableDefaultPageSize, + TablePageSizeOptions: updatedSettings.TablePageSizeOptions, + CustomMenuItems: dto.ParseCustomMenuItems(updatedSettings.CustomMenuItems), + CustomEndpoints: dto.ParseCustomEndpoints(updatedSettings.CustomEndpoints), + DefaultConcurrency: updatedSettings.DefaultConcurrency, + DefaultBalance: updatedSettings.DefaultBalance, + DefaultSubscriptions: updatedDefaultSubscriptions, + EnableModelFallback: updatedSettings.EnableModelFallback, + FallbackModelAnthropic: updatedSettings.FallbackModelAnthropic, + FallbackModelOpenAI: updatedSettings.FallbackModelOpenAI, + FallbackModelGemini: updatedSettings.FallbackModelGemini, + FallbackModelAntigravity: updatedSettings.FallbackModelAntigravity, + EnableIdentityPatch: updatedSettings.EnableIdentityPatch, + IdentityPatchPrompt: updatedSettings.IdentityPatchPrompt, + OpsMonitoringEnabled: updatedSettings.OpsMonitoringEnabled, + OpsRealtimeMonitoringEnabled: updatedSettings.OpsRealtimeMonitoringEnabled, + OpsQueryModeDefault: updatedSettings.OpsQueryModeDefault, + OpsMetricsIntervalSeconds: updatedSettings.OpsMetricsIntervalSeconds, + MinClaudeCodeVersion: updatedSettings.MinClaudeCodeVersion, + MaxClaudeCodeVersion: updatedSettings.MaxClaudeCodeVersion, + AllowUngroupedKeyScheduling: updatedSettings.AllowUngroupedKeyScheduling, + BackendModeEnabled: updatedSettings.BackendModeEnabled, + EnableFingerprintUnification: updatedSettings.EnableFingerprintUnification, + EnableMetadataPassthrough: updatedSettings.EnableMetadataPassthrough, + EnableCCHSigning: updatedSettings.EnableCCHSigning, + PaymentVisibleMethodAlipaySource: updatedSettings.PaymentVisibleMethodAlipaySource, + PaymentVisibleMethodWxpaySource: updatedSettings.PaymentVisibleMethodWxpaySource, + PaymentVisibleMethodAlipayEnabled: updatedSettings.PaymentVisibleMethodAlipayEnabled, + PaymentVisibleMethodWxpayEnabled: updatedSettings.PaymentVisibleMethodWxpayEnabled, + OpenAIAdvancedSchedulerEnabled: updatedSettings.OpenAIAdvancedSchedulerEnabled, + BalanceLowNotifyEnabled: updatedSettings.BalanceLowNotifyEnabled, + BalanceLowNotifyThreshold: updatedSettings.BalanceLowNotifyThreshold, + BalanceLowNotifyRechargeURL: updatedSettings.BalanceLowNotifyRechargeURL, + AccountQuotaNotifyEnabled: updatedSettings.AccountQuotaNotifyEnabled, + AccountQuotaNotifyEmails: dto.NotifyEmailEntriesFromService(updatedSettings.AccountQuotaNotifyEmails), + PaymentEnabled: updatedPaymentCfg.Enabled, + PaymentMinAmount: updatedPaymentCfg.MinAmount, + PaymentMaxAmount: updatedPaymentCfg.MaxAmount, + PaymentDailyLimit: updatedPaymentCfg.DailyLimit, + PaymentOrderTimeoutMin: updatedPaymentCfg.OrderTimeoutMin, + PaymentMaxPendingOrders: updatedPaymentCfg.MaxPendingOrders, + PaymentEnabledTypes: updatedPaymentCfg.EnabledTypes, + PaymentBalanceDisabled: updatedPaymentCfg.BalanceDisabled, + PaymentBalanceRechargeMultiplier: updatedPaymentCfg.BalanceRechargeMultiplier, + PaymentRechargeFeeRate: updatedPaymentCfg.RechargeFeeRate, + PaymentLoadBalanceStrat: updatedPaymentCfg.LoadBalanceStrategy, + PaymentProductNamePrefix: updatedPaymentCfg.ProductNamePrefix, + PaymentProductNameSuffix: updatedPaymentCfg.ProductNameSuffix, + PaymentHelpImageURL: updatedPaymentCfg.HelpImageURL, + PaymentHelpText: updatedPaymentCfg.HelpText, + PaymentCancelRateLimitEnabled: updatedPaymentCfg.CancelRateLimitEnabled, + PaymentCancelRateLimitMax: updatedPaymentCfg.CancelRateLimitMax, + PaymentCancelRateLimitWindow: updatedPaymentCfg.CancelRateLimitWindow, + PaymentCancelRateLimitUnit: updatedPaymentCfg.CancelRateLimitUnit, + PaymentCancelRateLimitMode: updatedPaymentCfg.CancelRateLimitMode, } response.Success(c, systemSettingsResponseData(payload, updatedAuthSourceDefaults)) } @@ -1442,12 +1531,33 @@ func diffSettings(before *service.SystemSettings, after *service.SystemSettings, if req.WeChatConnectAppSecret != "" { changed = append(changed, "wechat_connect_app_secret") } + if before.WeChatConnectOpenAppID != after.WeChatConnectOpenAppID { + changed = append(changed, "wechat_connect_open_app_id") + } + if req.WeChatConnectOpenAppSecret != "" { + changed = append(changed, "wechat_connect_open_app_secret") + } + if before.WeChatConnectMPAppID != after.WeChatConnectMPAppID { + changed = append(changed, "wechat_connect_mp_app_id") + } + if req.WeChatConnectMPAppSecret != "" { + changed = append(changed, "wechat_connect_mp_app_secret") + } + if before.WeChatConnectMobileAppID != after.WeChatConnectMobileAppID { + changed = append(changed, "wechat_connect_mobile_app_id") + } + if req.WeChatConnectMobileAppSecret != "" { + changed = append(changed, "wechat_connect_mobile_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.WeChatConnectMobileEnabled != after.WeChatConnectMobileEnabled { + changed = append(changed, "wechat_connect_mobile_enabled") + } if before.WeChatConnectMode != after.WeChatConnectMode { changed = append(changed, "wechat_connect_mode") } diff --git a/backend/internal/handler/auth_wechat_oauth.go b/backend/internal/handler/auth_wechat_oauth.go index 2fa035a5..ad184c46 100644 --- a/backend/internal/handler/auth_wechat_oauth.go +++ b/backend/internal/handler/auth_wechat_oauth.go @@ -753,7 +753,13 @@ func (h *AuthHandler) ensureWeChatBindOwnership( } for _, identity := range identities { if identity != nil && identity.UserID != userID { - return infraerrors.Conflict("AUTH_IDENTITY_OWNERSHIP_CONFLICT", "auth identity already belongs to another user") + activeOwner, lookupErr := findActiveUserByID(ctx, client, identity.UserID) + if lookupErr != nil { + return lookupErr + } + if activeOwner != nil { + return infraerrors.Conflict("AUTH_IDENTITY_OWNERSHIP_CONFLICT", "auth identity already belongs to another user") + } } } @@ -778,7 +784,13 @@ func (h *AuthHandler) ensureWeChatBindOwnership( } for _, channel := range channels { if channel != nil && channel.Edges.Identity != nil && channel.Edges.Identity.UserID != userID { - return infraerrors.Conflict("AUTH_IDENTITY_CHANNEL_OWNERSHIP_CONFLICT", "auth identity channel already belongs to another user") + activeOwner, lookupErr := findActiveUserByID(ctx, client, channel.Edges.Identity.UserID) + if lookupErr != nil { + return lookupErr + } + if activeOwner != nil { + return infraerrors.Conflict("AUTH_IDENTITY_CHANNEL_OWNERSHIP_CONFLICT", "auth identity channel already belongs to another user") + } } } return nil @@ -960,8 +972,8 @@ func (h *AuthHandler) getWeChatOAuthConfig(ctx context.Context, rawMode string, cfg := wechatOAuthConfig{ mode: mode, - appID: strings.TrimSpace(effective.AppID), - appSecret: strings.TrimSpace(effective.AppSecret), + appID: strings.TrimSpace(effective.AppIDForMode(mode)), + appSecret: strings.TrimSpace(effective.AppSecretForMode(mode)), redirectURI: firstNonEmpty(strings.TrimSpace(effective.RedirectURL), resolveWeChatOAuthAbsoluteURL(apiBaseURL, c, "/api/v1/auth/oauth/wechat/callback")), frontendCallback: firstNonEmpty(strings.TrimSpace(effective.FrontendRedirectURL), wechatOAuthDefaultFrontendCB), scope: effective.ScopeForMode(mode), diff --git a/backend/internal/handler/dto/settings.go b/backend/internal/handler/dto/settings.go index 17c1abd4..ee0a2c9a 100644 --- a/backend/internal/handler/dto/settings.go +++ b/backend/internal/handler/dto/settings.go @@ -54,8 +54,15 @@ type SystemSettings struct { WeChatConnectEnabled bool `json:"wechat_connect_enabled"` WeChatConnectAppID string `json:"wechat_connect_app_id"` WeChatConnectAppSecretConfigured bool `json:"wechat_connect_app_secret_configured"` + WeChatConnectOpenAppID string `json:"wechat_connect_open_app_id"` + WeChatConnectOpenAppSecretConfigured bool `json:"wechat_connect_open_app_secret_configured"` + WeChatConnectMPAppID string `json:"wechat_connect_mp_app_id"` + WeChatConnectMPAppSecretConfigured bool `json:"wechat_connect_mp_app_secret_configured"` + WeChatConnectMobileAppID string `json:"wechat_connect_mobile_app_id"` + WeChatConnectMobileAppSecretConfigured bool `json:"wechat_connect_mobile_app_secret_configured"` WeChatConnectOpenEnabled bool `json:"wechat_connect_open_enabled"` WeChatConnectMPEnabled bool `json:"wechat_connect_mp_enabled"` + WeChatConnectMobileEnabled bool `json:"wechat_connect_mobile_enabled"` WeChatConnectMode string `json:"wechat_connect_mode"` WeChatConnectScopes string `json:"wechat_connect_scopes"` WeChatConnectRedirectURL string `json:"wechat_connect_redirect_url"` @@ -212,6 +219,7 @@ type PublicSettings struct { WeChatOAuthEnabled bool `json:"wechat_oauth_enabled"` WeChatOAuthOpenEnabled bool `json:"wechat_oauth_open_enabled"` WeChatOAuthMPEnabled bool `json:"wechat_oauth_mp_enabled"` + WeChatOAuthMobileEnabled bool `json:"wechat_oauth_mobile_enabled"` OIDCOAuthEnabled bool `json:"oidc_oauth_enabled"` OIDCOAuthProviderName string `json:"oidc_oauth_provider_name"` SoraClientEnabled bool `json:"sora_client_enabled"` diff --git a/backend/internal/handler/setting_handler.go b/backend/internal/handler/setting_handler.go index 522290ec..c0f5c28b 100644 --- a/backend/internal/handler/setting_handler.go +++ b/backend/internal/handler/setting_handler.go @@ -60,6 +60,7 @@ func (h *SettingHandler) GetPublicSettings(c *gin.Context) { WeChatOAuthEnabled: settings.WeChatOAuthEnabled, WeChatOAuthOpenEnabled: settings.WeChatOAuthOpenEnabled, WeChatOAuthMPEnabled: settings.WeChatOAuthMPEnabled, + WeChatOAuthMobileEnabled: settings.WeChatOAuthMobileEnabled, OIDCOAuthEnabled: settings.OIDCOAuthEnabled, OIDCOAuthProviderName: settings.OIDCOAuthProviderName, BackendModeEnabled: settings.BackendModeEnabled, diff --git a/backend/internal/service/domain_constants.go b/backend/internal/service/domain_constants.go index 49b3c5ce..3c6888b8 100644 --- a/backend/internal/service/domain_constants.go +++ b/backend/internal/service/domain_constants.go @@ -115,8 +115,15 @@ const ( SettingKeyWeChatConnectEnabled = "wechat_connect_enabled" SettingKeyWeChatConnectAppID = "wechat_connect_app_id" SettingKeyWeChatConnectAppSecret = "wechat_connect_app_secret" + SettingKeyWeChatConnectOpenAppID = "wechat_connect_open_app_id" + SettingKeyWeChatConnectOpenAppSecret = "wechat_connect_open_app_secret" + SettingKeyWeChatConnectMPAppID = "wechat_connect_mp_app_id" + SettingKeyWeChatConnectMPAppSecret = "wechat_connect_mp_app_secret" + SettingKeyWeChatConnectMobileAppID = "wechat_connect_mobile_app_id" + SettingKeyWeChatConnectMobileAppSecret = "wechat_connect_mobile_app_secret" SettingKeyWeChatConnectOpenEnabled = "wechat_connect_open_enabled" SettingKeyWeChatConnectMPEnabled = "wechat_connect_mp_enabled" + SettingKeyWeChatConnectMobileEnabled = "wechat_connect_mobile_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 5e315625..def45543 100644 --- a/backend/internal/service/payment_order.go +++ b/backend/internal/service/payment_order.go @@ -519,13 +519,15 @@ func (s *PaymentService) getWeChatPaymentOAuthCredential(ctx context.Context) (s ) } cfg, err := (&SettingService{settingRepo: s.configService.settingRepo}).GetWeChatConnectOAuthConfig(ctx) - if err != nil || !cfg.SupportsMode("mp") || strings.TrimSpace(cfg.AppID) == "" || strings.TrimSpace(cfg.AppSecret) == "" { + appID := strings.TrimSpace(cfg.AppIDForMode("mp")) + appSecret := strings.TrimSpace(cfg.AppSecretForMode("mp")) + if err != nil || !cfg.SupportsMode("mp") || appID == "" || appSecret == "" { return "", "", infraerrors.ServiceUnavailable( "WECHAT_PAYMENT_MP_NOT_CONFIGURED", "wechat in-app payment requires a complete WeChat MP OAuth credential", ) } - return strings.TrimSpace(cfg.AppID), strings.TrimSpace(cfg.AppSecret), nil + return appID, appSecret, nil } func classifyCreatePaymentError(req CreateOrderRequest, providerKey string, err error) error { diff --git a/backend/internal/service/setting_service.go b/backend/internal/service/setting_service.go index f0808996..fe566fec 100644 --- a/backend/internal/service/setting_service.go +++ b/backend/internal/service/setting_service.go @@ -181,14 +181,19 @@ func normalizeWeChatConnectModeSetting(raw string) string { switch strings.ToLower(strings.TrimSpace(raw)) { case "mp": return "mp" + case "mobile": + return "mobile" default: return "open" } } func defaultWeChatConnectScopeForMode(mode string) string { - if normalizeWeChatConnectModeSetting(mode) == "mp" { + switch normalizeWeChatConnectModeSetting(mode) { + case "mp": return "snsapi_userinfo" + case "mobile": + return "" } return defaultWeChatConnectScopes } @@ -204,37 +209,47 @@ func normalizeWeChatConnectScopeSetting(raw, mode string) string { default: return defaultWeChatConnectScopeForMode(mode) } + case "mobile": + return "" default: return defaultWeChatConnectScopes } } -func parseWeChatConnectCapabilitySettings(settings map[string]string, enabled bool, mode string) (bool, bool) { +func parseWeChatConnectCapabilitySettings(settings map[string]string, enabled bool, mode string) (bool, bool, bool) { mode = normalizeWeChatConnectModeSetting(mode) rawOpen, hasOpen := settings[SettingKeyWeChatConnectOpenEnabled] rawMP, hasMP := settings[SettingKeyWeChatConnectMPEnabled] + rawMobile, hasMobile := settings[SettingKeyWeChatConnectMobileEnabled] openConfigured := hasOpen && strings.TrimSpace(rawOpen) != "" mpConfigured := hasMP && strings.TrimSpace(rawMP) != "" + mobileConfigured := hasMobile && strings.TrimSpace(rawMobile) != "" - if openConfigured || mpConfigured { + if openConfigured || mpConfigured || mobileConfigured { openEnabled := strings.TrimSpace(rawOpen) == "true" mpEnabled := strings.TrimSpace(rawMP) == "true" - return openEnabled, mpEnabled + mobileEnabled := strings.TrimSpace(rawMobile) == "true" + return openEnabled, mpEnabled, mobileEnabled } if !enabled { - return false, false + return false, false, false } if mode == "mp" { - return false, true + return false, true, false } - return true, false + if mode == "mobile" { + return false, false, true + } + return true, false, false } -func normalizeWeChatConnectStoredMode(openEnabled, mpEnabled bool, mode string) string { +func normalizeWeChatConnectStoredMode(openEnabled, mpEnabled, mobileEnabled bool, mode string) string { switch { case mpEnabled: return "mp" + case mobileEnabled: + return "mobile" case openEnabled: return "open" default: @@ -310,8 +325,15 @@ func (s *SettingService) GetPublicSettings(ctx context.Context) (*PublicSettings SettingKeyWeChatConnectEnabled, SettingKeyWeChatConnectAppID, SettingKeyWeChatConnectAppSecret, + SettingKeyWeChatConnectOpenAppID, + SettingKeyWeChatConnectOpenAppSecret, + SettingKeyWeChatConnectMPAppID, + SettingKeyWeChatConnectMPAppSecret, + SettingKeyWeChatConnectMobileAppID, + SettingKeyWeChatConnectMobileAppSecret, SettingKeyWeChatConnectOpenEnabled, SettingKeyWeChatConnectMPEnabled, + SettingKeyWeChatConnectMobileEnabled, SettingKeyWeChatConnectMode, SettingKeyWeChatConnectScopes, SettingKeyWeChatConnectRedirectURL, @@ -350,7 +372,7 @@ func (s *SettingService) GetPublicSettings(ctx context.Context) (*PublicSettings if oidcProviderName == "" { oidcProviderName = "OIDC" } - weChatEnabled, weChatOpenEnabled, weChatMPEnabled := s.weChatOAuthCapabilitiesFromSettings(settings) + weChatEnabled, weChatOpenEnabled, weChatMPEnabled, weChatMobileEnabled := s.weChatOAuthCapabilitiesFromSettings(settings) // Password reset requires email verification to be enabled emailVerifyEnabled := settings[SettingKeyEmailVerifyEnabled] == "true" @@ -397,6 +419,7 @@ func (s *SettingService) GetPublicSettings(ctx context.Context) (*PublicSettings WeChatOAuthEnabled: weChatEnabled, WeChatOAuthOpenEnabled: weChatOpenEnabled, WeChatOAuthMPEnabled: weChatMPEnabled, + WeChatOAuthMobileEnabled: weChatMobileEnabled, BackendModeEnabled: settings[SettingKeyBackendModeEnabled] == "true", PaymentEnabled: settings[SettingPaymentEnabled] == "true", OIDCOAuthEnabled: oidcEnabled, @@ -456,6 +479,7 @@ func (s *SettingService) GetPublicSettingsForInjection(ctx context.Context) (any WeChatOAuthEnabled bool `json:"wechat_oauth_enabled"` WeChatOAuthOpenEnabled bool `json:"wechat_oauth_open_enabled"` WeChatOAuthMPEnabled bool `json:"wechat_oauth_mp_enabled"` + WeChatOAuthMobileEnabled bool `json:"wechat_oauth_mobile_enabled"` BackendModeEnabled bool `json:"backend_mode_enabled"` PaymentEnabled bool `json:"payment_enabled"` OIDCOAuthEnabled bool `json:"oidc_oauth_enabled"` @@ -493,6 +517,7 @@ func (s *SettingService) GetPublicSettingsForInjection(ctx context.Context) (any WeChatOAuthEnabled: settings.WeChatOAuthEnabled, WeChatOAuthOpenEnabled: settings.WeChatOAuthOpenEnabled, WeChatOAuthMPEnabled: settings.WeChatOAuthMPEnabled, + WeChatOAuthMobileEnabled: settings.WeChatOAuthMobileEnabled, BackendModeEnabled: settings.BackendModeEnabled, PaymentEnabled: settings.PaymentEnabled, OIDCOAuthEnabled: settings.OIDCOAuthEnabled, @@ -512,15 +537,22 @@ 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) + openEnabled, mpEnabled, mobileEnabled := parseWeChatConnectCapabilitySettings(settings, enabled, mode) + mode = normalizeWeChatConnectStoredMode(openEnabled, mpEnabled, mobileEnabled, mode) cfg := WeChatConnectOAuthConfig{ Enabled: enabled, - AppID: strings.TrimSpace(settings[SettingKeyWeChatConnectAppID]), - AppSecret: strings.TrimSpace(settings[SettingKeyWeChatConnectAppSecret]), + LegacyAppID: strings.TrimSpace(settings[SettingKeyWeChatConnectAppID]), + LegacyAppSecret: strings.TrimSpace(settings[SettingKeyWeChatConnectAppSecret]), + OpenAppID: strings.TrimSpace(firstNonEmpty(settings[SettingKeyWeChatConnectOpenAppID], settings[SettingKeyWeChatConnectAppID])), + OpenAppSecret: strings.TrimSpace(firstNonEmpty(settings[SettingKeyWeChatConnectOpenAppSecret], settings[SettingKeyWeChatConnectAppSecret])), + MPAppID: strings.TrimSpace(firstNonEmpty(settings[SettingKeyWeChatConnectMPAppID], settings[SettingKeyWeChatConnectAppID])), + MPAppSecret: strings.TrimSpace(firstNonEmpty(settings[SettingKeyWeChatConnectMPAppSecret], settings[SettingKeyWeChatConnectAppSecret])), + MobileAppID: strings.TrimSpace(firstNonEmpty(settings[SettingKeyWeChatConnectMobileAppID], settings[SettingKeyWeChatConnectAppID])), + MobileAppSecret: strings.TrimSpace(firstNonEmpty(settings[SettingKeyWeChatConnectMobileAppSecret], settings[SettingKeyWeChatConnectAppSecret])), OpenEnabled: openEnabled, MPEnabled: mpEnabled, + MobileEnabled: mobileEnabled, Mode: mode, Scopes: normalizeWeChatConnectScopeSetting(settings[SettingKeyWeChatConnectScopes], mode), RedirectURL: strings.TrimSpace(settings[SettingKeyWeChatConnectRedirectURL]), @@ -533,11 +565,29 @@ func (s *SettingService) parseWeChatConnectOAuthConfig(settings map[string]strin if !cfg.Enabled || (!cfg.OpenEnabled && !cfg.MPEnabled) { 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.OpenEnabled { + if cfg.AppIDForMode("open") == "" { + return WeChatConnectOAuthConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "wechat oauth pc app id not configured") + } + if cfg.AppSecretForMode("open") == "" { + return WeChatConnectOAuthConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "wechat oauth pc app secret not configured") + } } - if cfg.AppSecret == "" { - return WeChatConnectOAuthConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "wechat oauth app secret not configured") + if cfg.MPEnabled { + if cfg.AppIDForMode("mp") == "" { + return WeChatConnectOAuthConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "wechat oauth official account app id not configured") + } + if cfg.AppSecretForMode("mp") == "" { + return WeChatConnectOAuthConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "wechat oauth official account app secret not configured") + } + } + if cfg.MobileEnabled { + if cfg.AppIDForMode("mobile") == "" { + return WeChatConnectOAuthConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "wechat oauth mobile app id not configured") + } + if cfg.AppSecretForMode("mobile") == "" { + return WeChatConnectOAuthConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "wechat oauth mobile app secret not configured") + } } if cfg.RedirectURL == "" { return WeChatConnectOAuthConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "wechat oauth redirect url not configured") @@ -554,12 +604,34 @@ func (s *SettingService) parseWeChatConnectOAuthConfig(settings map[string]strin 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 +func (s *SettingService) weChatOAuthCapabilitiesFromSettings(settings map[string]string) (bool, bool, bool, bool) { + if settings[SettingKeyWeChatConnectEnabled] != "true" { + return false, false, false, false } - return true, cfg.OpenEnabled, cfg.MPEnabled + + mode := normalizeWeChatConnectModeSetting(settings[SettingKeyWeChatConnectMode]) + openEnabled, mpEnabled, mobileEnabled := parseWeChatConnectCapabilitySettings(settings, true, mode) + redirectURL := strings.TrimSpace(settings[SettingKeyWeChatConnectRedirectURL]) + frontendRedirectURL := strings.TrimSpace(settings[SettingKeyWeChatConnectFrontendRedirectURL]) + if frontendRedirectURL == "" { + frontendRedirectURL = defaultWeChatConnectFrontend + } + + legacyAppID := strings.TrimSpace(settings[SettingKeyWeChatConnectAppID]) + legacyAppSecret := strings.TrimSpace(settings[SettingKeyWeChatConnectAppSecret]) + openAppID := strings.TrimSpace(firstNonEmpty(settings[SettingKeyWeChatConnectOpenAppID], legacyAppID)) + openAppSecret := strings.TrimSpace(firstNonEmpty(settings[SettingKeyWeChatConnectOpenAppSecret], legacyAppSecret)) + mpAppID := strings.TrimSpace(firstNonEmpty(settings[SettingKeyWeChatConnectMPAppID], legacyAppID)) + mpAppSecret := strings.TrimSpace(firstNonEmpty(settings[SettingKeyWeChatConnectMPAppSecret], legacyAppSecret)) + mobileAppID := strings.TrimSpace(firstNonEmpty(settings[SettingKeyWeChatConnectMobileAppID], legacyAppID)) + mobileAppSecret := strings.TrimSpace(firstNonEmpty(settings[SettingKeyWeChatConnectMobileAppSecret], legacyAppSecret)) + + webRedirectReady := redirectURL != "" && frontendRedirectURL != "" + openReady := openEnabled && webRedirectReady && openAppID != "" && openAppSecret != "" + mpReady := mpEnabled && webRedirectReady && mpAppID != "" && mpAppSecret != "" + mobileReady := mobileEnabled && mobileAppID != "" && mobileAppSecret != "" + + return openReady || mpReady || mobileReady, openReady, mpReady, mobileReady } // filterUserVisibleMenuItems filters out admin-only menu items from a raw JSON @@ -744,9 +816,16 @@ func (s *SettingService) buildSystemSettingsUpdates(ctx context.Context, setting settings.PaymentVisibleMethodWxpaySource = wxpaySource settings.WeChatConnectAppID = strings.TrimSpace(settings.WeChatConnectAppID) settings.WeChatConnectAppSecret = strings.TrimSpace(settings.WeChatConnectAppSecret) + settings.WeChatConnectOpenAppID = strings.TrimSpace(firstNonEmpty(settings.WeChatConnectOpenAppID, settings.WeChatConnectAppID)) + settings.WeChatConnectOpenAppSecret = strings.TrimSpace(firstNonEmpty(settings.WeChatConnectOpenAppSecret, settings.WeChatConnectAppSecret)) + settings.WeChatConnectMPAppID = strings.TrimSpace(firstNonEmpty(settings.WeChatConnectMPAppID, settings.WeChatConnectAppID)) + settings.WeChatConnectMPAppSecret = strings.TrimSpace(firstNonEmpty(settings.WeChatConnectMPAppSecret, settings.WeChatConnectAppSecret)) + settings.WeChatConnectMobileAppID = strings.TrimSpace(firstNonEmpty(settings.WeChatConnectMobileAppID, settings.WeChatConnectAppID)) + settings.WeChatConnectMobileAppSecret = strings.TrimSpace(firstNonEmpty(settings.WeChatConnectMobileAppSecret, settings.WeChatConnectAppSecret)) settings.WeChatConnectMode = normalizeWeChatConnectStoredMode( settings.WeChatConnectOpenEnabled, settings.WeChatConnectMPEnabled, + settings.WeChatConnectMobileEnabled, settings.WeChatConnectMode, ) settings.WeChatConnectScopes = normalizeWeChatConnectScopeSetting(settings.WeChatConnectScopes, settings.WeChatConnectMode) @@ -827,8 +906,12 @@ func (s *SettingService) buildSystemSettingsUpdates(ctx context.Context, setting // WeChat Connect OAuth 登录 updates[SettingKeyWeChatConnectEnabled] = strconv.FormatBool(settings.WeChatConnectEnabled) updates[SettingKeyWeChatConnectAppID] = settings.WeChatConnectAppID + updates[SettingKeyWeChatConnectOpenAppID] = settings.WeChatConnectOpenAppID + updates[SettingKeyWeChatConnectMPAppID] = settings.WeChatConnectMPAppID + updates[SettingKeyWeChatConnectMobileAppID] = settings.WeChatConnectMobileAppID updates[SettingKeyWeChatConnectOpenEnabled] = strconv.FormatBool(settings.WeChatConnectOpenEnabled) updates[SettingKeyWeChatConnectMPEnabled] = strconv.FormatBool(settings.WeChatConnectMPEnabled) + updates[SettingKeyWeChatConnectMobileEnabled] = strconv.FormatBool(settings.WeChatConnectMobileEnabled) updates[SettingKeyWeChatConnectMode] = settings.WeChatConnectMode updates[SettingKeyWeChatConnectScopes] = settings.WeChatConnectScopes updates[SettingKeyWeChatConnectRedirectURL] = settings.WeChatConnectRedirectURL @@ -836,6 +919,15 @@ func (s *SettingService) buildSystemSettingsUpdates(ctx context.Context, setting if settings.WeChatConnectAppSecret != "" { updates[SettingKeyWeChatConnectAppSecret] = settings.WeChatConnectAppSecret } + if settings.WeChatConnectOpenAppSecret != "" { + updates[SettingKeyWeChatConnectOpenAppSecret] = settings.WeChatConnectOpenAppSecret + } + if settings.WeChatConnectMPAppSecret != "" { + updates[SettingKeyWeChatConnectMPAppSecret] = settings.WeChatConnectMPAppSecret + } + if settings.WeChatConnectMobileAppSecret != "" { + updates[SettingKeyWeChatConnectMobileAppSecret] = settings.WeChatConnectMobileAppSecret + } // OEM设置 updates[SettingKeySiteName] = settings.SiteName @@ -1344,8 +1436,15 @@ func (s *SettingService) InitializeDefaultSettings(ctx context.Context) error { SettingKeyCustomMenuItems: "[]", SettingKeyCustomEndpoints: "[]", SettingKeyWeChatConnectEnabled: "false", + SettingKeyWeChatConnectOpenAppID: "", + SettingKeyWeChatConnectOpenAppSecret: "", + SettingKeyWeChatConnectMPAppID: "", + SettingKeyWeChatConnectMPAppSecret: "", + SettingKeyWeChatConnectMobileAppID: "", + SettingKeyWeChatConnectMobileAppSecret: "", SettingKeyWeChatConnectOpenEnabled: "false", SettingKeyWeChatConnectMPEnabled: "false", + SettingKeyWeChatConnectMobileEnabled: "false", SettingKeyWeChatConnectMode: "open", SettingKeyWeChatConnectScopes: "snsapi_login", SettingKeyWeChatConnectFrontendRedirectURL: defaultWeChatConnectFrontend, @@ -1645,7 +1744,16 @@ 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.WeChatConnectOpenEnabled, result.WeChatConnectMPEnabled = parseWeChatConnectCapabilitySettings( + result.WeChatConnectOpenAppID = strings.TrimSpace(firstNonEmpty(settings[SettingKeyWeChatConnectOpenAppID], result.WeChatConnectAppID)) + result.WeChatConnectOpenAppSecret = strings.TrimSpace(firstNonEmpty(settings[SettingKeyWeChatConnectOpenAppSecret], result.WeChatConnectAppSecret)) + result.WeChatConnectOpenAppSecretConfigured = result.WeChatConnectOpenAppSecret != "" + result.WeChatConnectMPAppID = strings.TrimSpace(firstNonEmpty(settings[SettingKeyWeChatConnectMPAppID], result.WeChatConnectAppID)) + result.WeChatConnectMPAppSecret = strings.TrimSpace(firstNonEmpty(settings[SettingKeyWeChatConnectMPAppSecret], result.WeChatConnectAppSecret)) + result.WeChatConnectMPAppSecretConfigured = result.WeChatConnectMPAppSecret != "" + result.WeChatConnectMobileAppID = strings.TrimSpace(firstNonEmpty(settings[SettingKeyWeChatConnectMobileAppID], result.WeChatConnectAppID)) + result.WeChatConnectMobileAppSecret = strings.TrimSpace(firstNonEmpty(settings[SettingKeyWeChatConnectMobileAppSecret], result.WeChatConnectAppSecret)) + result.WeChatConnectMobileAppSecretConfigured = result.WeChatConnectMobileAppSecret != "" + result.WeChatConnectOpenEnabled, result.WeChatConnectMPEnabled, result.WeChatConnectMobileEnabled = parseWeChatConnectCapabilitySettings( settings, result.WeChatConnectEnabled, settings[SettingKeyWeChatConnectMode], @@ -1653,6 +1761,7 @@ func (s *SettingService) parseSettings(settings map[string]string) *SystemSettin result.WeChatConnectMode = normalizeWeChatConnectStoredMode( result.WeChatConnectOpenEnabled, result.WeChatConnectMPEnabled, + result.WeChatConnectMobileEnabled, settings[SettingKeyWeChatConnectMode], ) result.WeChatConnectScopes = normalizeWeChatConnectScopeSetting(settings[SettingKeyWeChatConnectScopes], result.WeChatConnectMode) @@ -2151,8 +2260,15 @@ func (s *SettingService) GetWeChatConnectOAuthConfig(ctx context.Context) (WeCha SettingKeyWeChatConnectEnabled, SettingKeyWeChatConnectAppID, SettingKeyWeChatConnectAppSecret, + SettingKeyWeChatConnectOpenAppID, + SettingKeyWeChatConnectOpenAppSecret, + SettingKeyWeChatConnectMPAppID, + SettingKeyWeChatConnectMPAppSecret, + SettingKeyWeChatConnectMobileAppID, + SettingKeyWeChatConnectMobileAppSecret, SettingKeyWeChatConnectOpenEnabled, SettingKeyWeChatConnectMPEnabled, + SettingKeyWeChatConnectMobileEnabled, SettingKeyWeChatConnectMode, SettingKeyWeChatConnectScopes, SettingKeyWeChatConnectRedirectURL, diff --git a/backend/internal/service/settings_view.go b/backend/internal/service/settings_view.go index 1be206b9..d2ef8fae 100644 --- a/backend/internal/service/settings_view.go +++ b/backend/internal/service/settings_view.go @@ -1,5 +1,16 @@ package service +import "strings" + +func firstNonEmpty(values ...string) string { + for _, value := range values { + if trimmed := strings.TrimSpace(value); trimmed != "" { + return trimmed + } + } + return "" +} + type SystemSettings struct { RegistrationEnabled bool EmailVerifyEnabled bool @@ -32,16 +43,26 @@ type SystemSettings struct { LinuxDoConnectRedirectURL string // WeChat Connect OAuth 登录 - WeChatConnectEnabled bool - WeChatConnectAppID string - WeChatConnectAppSecret string - WeChatConnectAppSecretConfigured bool - WeChatConnectOpenEnabled bool - WeChatConnectMPEnabled bool - WeChatConnectMode string - WeChatConnectScopes string - WeChatConnectRedirectURL string - WeChatConnectFrontendRedirectURL string + WeChatConnectEnabled bool + WeChatConnectAppID string + WeChatConnectAppSecret string + WeChatConnectAppSecretConfigured bool + WeChatConnectOpenAppID string + WeChatConnectOpenAppSecret string + WeChatConnectOpenAppSecretConfigured bool + WeChatConnectMPAppID string + WeChatConnectMPAppSecret string + WeChatConnectMPAppSecretConfigured bool + WeChatConnectMobileAppID string + WeChatConnectMobileAppSecret string + WeChatConnectMobileAppSecretConfigured bool + WeChatConnectOpenEnabled bool + WeChatConnectMPEnabled bool + WeChatConnectMobileEnabled bool + WeChatConnectMode string + WeChatConnectScopes string + WeChatConnectRedirectURL string + WeChatConnectFrontendRedirectURL string // Generic OIDC OAuth 登录 OIDCConnectEnabled bool @@ -173,15 +194,16 @@ type PublicSettings struct { CustomMenuItems string // JSON array of custom menu items CustomEndpoints string // JSON array of custom endpoints - LinuxDoOAuthEnabled bool - WeChatOAuthEnabled bool - WeChatOAuthOpenEnabled bool - WeChatOAuthMPEnabled bool - BackendModeEnabled bool - PaymentEnabled bool - OIDCOAuthEnabled bool - OIDCOAuthProviderName string - Version string + LinuxDoOAuthEnabled bool + WeChatOAuthEnabled bool + WeChatOAuthOpenEnabled bool + WeChatOAuthMPEnabled bool + WeChatOAuthMobileEnabled bool + BackendModeEnabled bool + PaymentEnabled bool + OIDCOAuthEnabled bool + OIDCOAuthProviderName string + Version string BalanceLowNotifyEnabled bool AccountQuotaNotifyEnabled bool @@ -191,10 +213,17 @@ type PublicSettings struct { type WeChatConnectOAuthConfig struct { Enabled bool - AppID string - AppSecret string + LegacyAppID string + LegacyAppSecret string + OpenAppID string + OpenAppSecret string + MPAppID string + MPAppSecret string + MobileAppID string + MobileAppSecret string OpenEnabled bool MPEnabled bool + MobileEnabled bool Mode string Scopes string RedirectURL string @@ -205,18 +234,43 @@ func (cfg WeChatConnectOAuthConfig) SupportsMode(mode string) bool { switch normalizeWeChatConnectModeSetting(mode) { case "mp": return cfg.MPEnabled + case "mobile": + return cfg.MobileEnabled default: return cfg.OpenEnabled } } func (cfg WeChatConnectOAuthConfig) ScopeForMode(mode string) string { - if normalizeWeChatConnectModeSetting(mode) == "mp" { + switch normalizeWeChatConnectModeSetting(mode) { + case "mp": return normalizeWeChatConnectScopeSetting(cfg.Scopes, "mp") + case "mobile": + return "" } return defaultWeChatConnectScopeForMode("open") } +func (cfg WeChatConnectOAuthConfig) AppIDForMode(mode string) string { + switch normalizeWeChatConnectModeSetting(mode) { + case "mp": + return strings.TrimSpace(firstNonEmpty(cfg.MPAppID, cfg.LegacyAppID)) + case "mobile": + return strings.TrimSpace(firstNonEmpty(cfg.MobileAppID, cfg.LegacyAppID)) + } + return strings.TrimSpace(firstNonEmpty(cfg.OpenAppID, cfg.LegacyAppID)) +} + +func (cfg WeChatConnectOAuthConfig) AppSecretForMode(mode string) string { + switch normalizeWeChatConnectModeSetting(mode) { + case "mp": + return strings.TrimSpace(firstNonEmpty(cfg.MPAppSecret, cfg.LegacyAppSecret)) + case "mobile": + return strings.TrimSpace(firstNonEmpty(cfg.MobileAppSecret, cfg.LegacyAppSecret)) + } + return strings.TrimSpace(firstNonEmpty(cfg.OpenAppSecret, cfg.LegacyAppSecret)) +} + // StreamTimeoutSettings 流超时处理配置(仅控制超时后的处理方式,超时判定由网关配置控制) type StreamTimeoutSettings struct { // Enabled 是否启用流超时处理 diff --git a/frontend/src/api/admin/settings.ts b/frontend/src/api/admin/settings.ts index 500eb4dc..0403b0f3 100644 --- a/frontend/src/api/admin/settings.ts +++ b/frontend/src/api/admin/settings.ts @@ -32,7 +32,7 @@ export type PaymentVisibleMethodSource = | "easypay_alipay" | "official_wxpay" | "easypay_wxpay"; -export type WeChatConnectMode = "open" | "mp"; +export type WeChatConnectMode = "open" | "mp" | "mobile"; export interface PaymentVisibleMethodSourceOption { value: PaymentVisibleMethodSource; @@ -108,11 +108,16 @@ const PAYMENT_VISIBLE_METHOD_SOURCE_ALIASES: Record< }, }; const WECHAT_CONNECT_MODE_OPTIONS: WeChatConnectModeOption[] = [ - { value: "open", labelZh: "微信开放平台", labelEn: "WeChat Open Platform" }, + { value: "open", labelZh: "PC 应用", labelEn: "PC App" }, { value: "mp", - labelZh: "微信公众号 / 小程序", - labelEn: "WeChat Official Account / Mini Program", + labelZh: "公众号", + labelEn: "Official Account", + }, + { + value: "mobile", + labelZh: "移动应用", + labelEn: "Mobile App", }, ]; const WECHAT_CONNECT_MODE_ALIASES: Record = { @@ -124,6 +129,9 @@ const WECHAT_CONNECT_MODE_ALIASES: Record = { official_account: "mp", wechat_mp: "mp", mini_program: "mp", + mobile: "mobile", + mobile_app: "mobile", + native_app: "mobile", }; export function normalizeDefaultSubscriptionSettings( @@ -234,34 +242,52 @@ export function normalizeWeChatConnectMode(source: unknown): WeChatConnectMode { } export function defaultWeChatConnectScopesForMode(mode: unknown): string { - return normalizeWeChatConnectMode(mode) === "mp" - ? "snsapi_userinfo" - : "snsapi_login"; + switch (normalizeWeChatConnectMode(mode)) { + case "mp": + return "snsapi_userinfo"; + case "mobile": + return ""; + default: + return "snsapi_login"; + } } export function resolveWeChatConnectModeCapabilities( openEnabled: unknown, mpEnabled: unknown, + mobileEnabled: unknown, legacyMode: unknown, -): { openEnabled: boolean; mpEnabled: boolean } { - if (typeof openEnabled === "boolean" || typeof mpEnabled === "boolean") { +): { openEnabled: boolean; mpEnabled: boolean; mobileEnabled: boolean } { + if ( + typeof openEnabled === "boolean" || + typeof mpEnabled === "boolean" || + typeof mobileEnabled === "boolean" + ) { return { openEnabled: openEnabled === true, mpEnabled: mpEnabled === true, + mobileEnabled: mobileEnabled === true, }; } - return normalizeWeChatConnectMode(legacyMode) === "mp" - ? { openEnabled: false, mpEnabled: true } - : { openEnabled: true, mpEnabled: false }; + switch (normalizeWeChatConnectMode(legacyMode)) { + case "mp": + return { openEnabled: false, mpEnabled: true, mobileEnabled: false }; + case "mobile": + return { openEnabled: false, mpEnabled: false, mobileEnabled: true }; + default: + return { openEnabled: true, mpEnabled: false, mobileEnabled: false }; + } } export function deriveWeChatConnectStoredMode( openEnabled: boolean, mpEnabled: boolean, + mobileEnabled: boolean, legacyMode: unknown, ): WeChatConnectMode { if (mpEnabled) return "mp"; + if (mobileEnabled) return "mobile"; if (openEnabled) return "open"; return normalizeWeChatConnectMode(legacyMode); } @@ -342,8 +368,15 @@ export interface SystemSettings { wechat_connect_enabled: boolean; wechat_connect_app_id: string; wechat_connect_app_secret_configured: boolean; + wechat_connect_open_app_id?: string; + wechat_connect_open_app_secret_configured?: boolean; + wechat_connect_mp_app_id?: string; + wechat_connect_mp_app_secret_configured?: boolean; + wechat_connect_mobile_app_id?: string; + wechat_connect_mobile_app_secret_configured?: boolean; wechat_connect_open_enabled?: boolean; wechat_connect_mp_enabled?: boolean; + wechat_connect_mobile_enabled?: boolean; wechat_connect_mode: string; wechat_connect_scopes: string; wechat_connect_redirect_url: string; @@ -501,8 +534,15 @@ export interface UpdateSettingsRequest { wechat_connect_enabled?: boolean; wechat_connect_app_id?: string; wechat_connect_app_secret?: string; + wechat_connect_open_app_id?: string; + wechat_connect_open_app_secret?: string; + wechat_connect_mp_app_id?: string; + wechat_connect_mp_app_secret?: string; + wechat_connect_mobile_app_id?: string; + wechat_connect_mobile_app_secret?: string; wechat_connect_open_enabled?: boolean; wechat_connect_mp_enabled?: boolean; + wechat_connect_mobile_enabled?: boolean; wechat_connect_mode?: string; wechat_connect_scopes?: string; wechat_connect_redirect_url?: string; diff --git a/frontend/src/components/auth/WechatOAuthSection.vue b/frontend/src/components/auth/WechatOAuthSection.vue index 3b001ea9..6366582c 100644 --- a/frontend/src/components/auth/WechatOAuthSection.vue +++ b/frontend/src/components/auth/WechatOAuthSection.vue @@ -57,6 +57,11 @@ const disabledHint = computed(() => { return t('auth.oauthFlow.wechatSystemBrowserOnly') case 'wechat_browser_required': return t('auth.oauthFlow.wechatBrowserOnly') + case 'native_app_required': + return localizeWeChatHint( + '当前仅配置微信移动应用登录,需要在原生 App 中通过微信 SDK 发起授权。', + 'This site only has WeChat mobile app login configured. Continue from the native app through the WeChat SDK.', + ) case 'not_configured': return t('auth.oauthFlow.wechatNotConfigured') default: diff --git a/frontend/src/stores/app.ts b/frontend/src/stores/app.ts index b692088f..f5c39e62 100644 --- a/frontend/src/stores/app.ts +++ b/frontend/src/stores/app.ts @@ -344,6 +344,7 @@ export const useAppStore = defineStore('app', () => { wechat_oauth_enabled: false, wechat_oauth_open_enabled: false, wechat_oauth_mp_enabled: false, + wechat_oauth_mobile_enabled: false, oidc_oauth_enabled: false, oidc_oauth_provider_name: 'OIDC', backend_mode_enabled: false, diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts index 9f13f8a0..48c7bc4f 100644 --- a/frontend/src/types/index.ts +++ b/frontend/src/types/index.ts @@ -168,6 +168,7 @@ export interface PublicSettings { wechat_oauth_enabled: boolean wechat_oauth_open_enabled?: boolean wechat_oauth_mp_enabled?: boolean + wechat_oauth_mobile_enabled?: boolean oidc_oauth_enabled: boolean oidc_oauth_provider_name: string backend_mode_enabled: boolean diff --git a/frontend/src/views/admin/SettingsView.vue b/frontend/src/views/admin/SettingsView.vue index efe38dd6..fcf5de25 100644 --- a/frontend/src/views/admin/SettingsView.vue +++ b/frontend/src/views/admin/SettingsView.vue @@ -1398,101 +1398,253 @@ v-if="form.wechat_connect_enabled" class="space-y-6 border-t border-gray-100 pt-4 dark:border-dark-700" > -
-
-
-
-
- -
-
-
- {{ t("admin.settings.wechatConnect.openModeLabel") }} -
-

- {{ t("admin.settings.wechatConnect.openModeHint") }} -

-
- -
-
-
-
- {{ t("admin.settings.wechatConnect.mpModeLabel") }} -
-

- {{ t("admin.settings.wechatConnect.mpModeHint") }} -

-
- -
-
+
+ {{ + localText( + "如果同时启用 PC 应用和公众号/移动应用,这些应用需要挂在同一个微信开放平台主体下,否则 UnionID 无法稳定归并账号。", + "When PC App is enabled together with Official Account or Mobile App, they should belong to the same WeChat Open Platform account so UnionID can merge identities reliably.", + ) + }} +
+
+

+ {{ + localText( + "用于 PC 应用和公众号的网页回调。移动应用走原生 SDK 时不直接使用这个浏览器回调。", + "Used by PC App and Official Account browser callbacks. Native mobile SDK flows do not start from this browser callback directly.", + ) + }} +

@@ -4594,6 +4754,7 @@ import type { SystemSettings, UpdateSettingsRequest, DefaultSubscriptionSetting, + WeChatConnectMode, WebSearchEmulationConfig, WebSearchProviderConfig, WebSearchTestResult, @@ -4731,14 +4892,20 @@ interface DefaultSubscriptionGroupOption { type SettingsForm = Omit< SystemSettings, - "wechat_connect_open_enabled" | "wechat_connect_mp_enabled" + | "wechat_connect_open_enabled" + | "wechat_connect_mp_enabled" + | "wechat_connect_mobile_enabled" > & { smtp_password: string; turnstile_secret_key: string; linuxdo_connect_client_secret: string; wechat_connect_app_secret: string; + wechat_connect_open_app_secret: string; + wechat_connect_mp_app_secret: string; + wechat_connect_mobile_app_secret: string; wechat_connect_open_enabled: boolean; wechat_connect_mp_enabled: boolean; + wechat_connect_mobile_enabled: boolean; oidc_connect_client_secret: string; force_email_on_third_party_signup: boolean; payment_visible_method_alipay_source: string; @@ -4833,8 +5000,18 @@ const form = reactive({ wechat_connect_app_id: "", wechat_connect_app_secret: "", wechat_connect_app_secret_configured: false, + wechat_connect_open_app_id: "", + wechat_connect_open_app_secret: "", + wechat_connect_open_app_secret_configured: false, + wechat_connect_mp_app_id: "", + wechat_connect_mp_app_secret: "", + wechat_connect_mp_app_secret_configured: false, + wechat_connect_mobile_app_id: "", + wechat_connect_mobile_app_secret: "", + wechat_connect_mobile_app_secret_configured: false, wechat_connect_open_enabled: false, wechat_connect_mp_enabled: false, + wechat_connect_mobile_enabled: false, wechat_connect_mode: "open", wechat_connect_scopes: "snsapi_login", wechat_connect_redirect_url: "", @@ -5315,17 +5492,28 @@ const wechatRedirectUrlSuggestion = computed(() => { return `${origin}/api/v1/auth/oauth/wechat/callback`; }); -function syncWeChatConnectMode() { +function syncWeChatConnectMode(preferredMode?: WeChatConnectMode) { + if (form.wechat_connect_mp_enabled && form.wechat_connect_mobile_enabled) { + if (preferredMode === "mobile") { + form.wechat_connect_mp_enabled = false; + } else { + form.wechat_connect_mobile_enabled = false; + } + } + const capabilities = resolveWeChatConnectModeCapabilities( form.wechat_connect_open_enabled, form.wechat_connect_mp_enabled, + form.wechat_connect_mobile_enabled, form.wechat_connect_mode, ); form.wechat_connect_open_enabled = capabilities.openEnabled; form.wechat_connect_mp_enabled = capabilities.mpEnabled; + form.wechat_connect_mobile_enabled = capabilities.mobileEnabled; form.wechat_connect_mode = deriveWeChatConnectStoredMode( capabilities.openEnabled, capabilities.mpEnabled, + capabilities.mobileEnabled, form.wechat_connect_mode, ); form.wechat_connect_scopes = defaultWeChatConnectScopesForMode( @@ -5333,6 +5521,27 @@ function syncWeChatConnectMode() { ); } +function handleWeChatOpenEnabledChange(value: boolean) { + form.wechat_connect_open_enabled = value; + syncWeChatConnectMode(value ? "open" : undefined); +} + +function handleWeChatMPEnabledChange(value: boolean) { + form.wechat_connect_mp_enabled = value; + if (value) { + form.wechat_connect_mobile_enabled = false; + } + syncWeChatConnectMode(value ? "mp" : undefined); +} + +function handleWeChatMobileEnabledChange(value: boolean) { + form.wechat_connect_mobile_enabled = value; + if (value) { + form.wechat_connect_mp_enabled = false; + } + syncWeChatConnectMode(value ? "mobile" : undefined); +} + async function setAndCopyWeChatRedirectUrl() { const url = wechatRedirectUrlSuggestion.value; if (!url) return; @@ -5476,16 +5685,22 @@ async function loadSettings() { form.turnstile_secret_key = ""; form.linuxdo_connect_client_secret = ""; form.wechat_connect_app_secret = ""; + form.wechat_connect_open_app_secret = ""; + form.wechat_connect_mp_app_secret = ""; + form.wechat_connect_mobile_app_secret = ""; const wechatCapabilities = resolveWeChatConnectModeCapabilities( settings.wechat_connect_open_enabled, settings.wechat_connect_mp_enabled, + settings.wechat_connect_mobile_enabled, settings.wechat_connect_mode, ); form.wechat_connect_open_enabled = wechatCapabilities.openEnabled; form.wechat_connect_mp_enabled = wechatCapabilities.mpEnabled; + form.wechat_connect_mobile_enabled = wechatCapabilities.mobileEnabled; form.wechat_connect_mode = deriveWeChatConnectStoredMode( wechatCapabilities.openEnabled, wechatCapabilities.mpEnabled, + wechatCapabilities.mobileEnabled, settings.wechat_connect_mode, ); form.wechat_connect_scopes = defaultWeChatConnectScopesForMode( @@ -5649,6 +5864,16 @@ async function saveSettings() { return; } + if (form.wechat_connect_mp_enabled && form.wechat_connect_mobile_enabled) { + appStore.showError( + localText( + "公众号和移动应用不能同时启用。", + "Official Account and Mobile App cannot be enabled at the same time.", + ), + ); + return; + } + // Validate URL fields — novalidate disables browser-native checks, so we validate here const isValidHttpUrl = (url: string): boolean => { if (!url) return true; @@ -5666,6 +5891,7 @@ async function saveSettings() { const wechatStoredMode = deriveWeChatConnectStoredMode( form.wechat_connect_open_enabled, form.wechat_connect_mp_enabled, + form.wechat_connect_mobile_enabled, form.wechat_connect_mode, ); @@ -5714,10 +5940,24 @@ async function saveSettings() { form.linuxdo_connect_client_secret || undefined, linuxdo_connect_redirect_url: form.linuxdo_connect_redirect_url, wechat_connect_enabled: form.wechat_connect_enabled, - wechat_connect_app_id: form.wechat_connect_app_id, + wechat_connect_app_id: + form.wechat_connect_open_app_id || + form.wechat_connect_mp_app_id || + form.wechat_connect_mobile_app_id || + form.wechat_connect_app_id, wechat_connect_app_secret: form.wechat_connect_app_secret || undefined, + wechat_connect_open_app_id: form.wechat_connect_open_app_id, + wechat_connect_open_app_secret: + form.wechat_connect_open_app_secret || undefined, + wechat_connect_mp_app_id: form.wechat_connect_mp_app_id, + wechat_connect_mp_app_secret: + form.wechat_connect_mp_app_secret || undefined, + wechat_connect_mobile_app_id: form.wechat_connect_mobile_app_id, + wechat_connect_mobile_app_secret: + form.wechat_connect_mobile_app_secret || undefined, wechat_connect_open_enabled: form.wechat_connect_open_enabled, wechat_connect_mp_enabled: form.wechat_connect_mp_enabled, + wechat_connect_mobile_enabled: form.wechat_connect_mobile_enabled, wechat_connect_mode: wechatStoredMode, wechat_connect_scopes: defaultWeChatConnectScopesForMode(wechatStoredMode), @@ -5847,16 +6087,23 @@ async function saveSettings() { form.turnstile_secret_key = ""; form.linuxdo_connect_client_secret = ""; form.wechat_connect_app_secret = ""; + form.wechat_connect_open_app_secret = ""; + form.wechat_connect_mp_app_secret = ""; + form.wechat_connect_mobile_app_secret = ""; const updatedWechatCapabilities = resolveWeChatConnectModeCapabilities( updated.wechat_connect_open_enabled, updated.wechat_connect_mp_enabled, + updated.wechat_connect_mobile_enabled, updated.wechat_connect_mode, ); form.wechat_connect_open_enabled = updatedWechatCapabilities.openEnabled; form.wechat_connect_mp_enabled = updatedWechatCapabilities.mpEnabled; + form.wechat_connect_mobile_enabled = + updatedWechatCapabilities.mobileEnabled; form.wechat_connect_mode = deriveWeChatConnectStoredMode( updatedWechatCapabilities.openEnabled, updatedWechatCapabilities.mpEnabled, + updatedWechatCapabilities.mobileEnabled, updated.wechat_connect_mode, ); form.wechat_connect_scopes = defaultWeChatConnectScopesForMode( diff --git a/frontend/src/views/auth/LoginView.vue b/frontend/src/views/auth/LoginView.vue index dcf5229a..44c89b23 100644 --- a/frontend/src/views/auth/LoginView.vue +++ b/frontend/src/views/auth/LoginView.vue @@ -184,7 +184,7 @@ import TotpLoginModal from '@/components/auth/TotpLoginModal.vue' import Icon from '@/components/icons/Icon.vue' import TurnstileWidget from '@/components/TurnstileWidget.vue' import { useAuthStore, useAppStore } from '@/stores' -import { getPublicSettings, isTotp2FARequired } from '@/api/auth' +import { getPublicSettings, isTotp2FARequired, isWeChatWebOAuthEnabled } from '@/api/auth' import type { TotpLoginResponse } from '@/types' const { t } = useI18n() @@ -258,7 +258,7 @@ onMounted(async () => { turnstileEnabled.value = settings.turnstile_enabled turnstileSiteKey.value = settings.turnstile_site_key || '' linuxdoOAuthEnabled.value = settings.linuxdo_oauth_enabled - wechatOAuthEnabled.value = settings.wechat_oauth_enabled + wechatOAuthEnabled.value = isWeChatWebOAuthEnabled(settings) backendModeEnabled.value = settings.backend_mode_enabled oidcOAuthEnabled.value = settings.oidc_oauth_enabled oidcOAuthProviderName.value = settings.oidc_oauth_provider_name || 'OIDC' diff --git a/frontend/src/views/auth/RegisterView.vue b/frontend/src/views/auth/RegisterView.vue index 762290ff..b469683b 100644 --- a/frontend/src/views/auth/RegisterView.vue +++ b/frontend/src/views/auth/RegisterView.vue @@ -282,7 +282,12 @@ import WechatOAuthSection from '@/components/auth/WechatOAuthSection.vue' import Icon from '@/components/icons/Icon.vue' import TurnstileWidget from '@/components/TurnstileWidget.vue' import { useAuthStore, useAppStore } from '@/stores' -import { getPublicSettings, validatePromoCode, validateInvitationCode } from '@/api/auth' +import { + getPublicSettings, + isWeChatWebOAuthEnabled, + validatePromoCode, + validateInvitationCode +} from '@/api/auth' import { buildAuthErrorMessage } from '@/utils/authError' import { isRegistrationEmailSuffixAllowed, @@ -385,7 +390,7 @@ onMounted(async () => { turnstileSiteKey.value = settings.turnstile_site_key || '' siteName.value = settings.site_name || 'Sub2API' linuxdoOAuthEnabled.value = settings.linuxdo_oauth_enabled - wechatOAuthEnabled.value = settings.wechat_oauth_enabled + wechatOAuthEnabled.value = isWeChatWebOAuthEnabled(settings) oidcOAuthEnabled.value = settings.oidc_oauth_enabled oidcOAuthProviderName.value = settings.oidc_oauth_provider_name || 'OIDC' registrationEmailSuffixWhitelist.value = normalizeRegistrationEmailSuffixWhitelist( diff --git a/frontend/src/views/auth/WechatCallbackView.vue b/frontend/src/views/auth/WechatCallbackView.vue index e5942141..2bcc1c3d 100644 --- a/frontend/src/views/auth/WechatCallbackView.vue +++ b/frontend/src/views/auth/WechatCallbackView.vue @@ -504,6 +504,8 @@ function resolveWeChatOAuthUnavailableMessage(): string { return t('auth.oauthFlow.wechatSystemBrowserOnly') case 'wechat_browser_required': return t('auth.oauthFlow.wechatBrowserOnly') + case 'native_app_required': + return 'This WeChat sign-in flow is only available from the native mobile app.' case 'not_configured': return t('auth.oauthFlow.wechatNotConfigured') default: