fix(settings): preserve oauth config compatibility on upgrade
This commit is contained in:
@ -70,6 +70,7 @@ type Config struct {
|
||||
JWT JWTConfig `mapstructure:"jwt"`
|
||||
Totp TotpConfig `mapstructure:"totp"`
|
||||
LinuxDo LinuxDoConnectConfig `mapstructure:"linuxdo_connect"`
|
||||
WeChat WeChatConnectConfig `mapstructure:"wechat_connect"`
|
||||
OIDC OIDCConnectConfig `mapstructure:"oidc_connect"`
|
||||
Default DefaultConfig `mapstructure:"default"`
|
||||
RateLimit RateLimitConfig `mapstructure:"rate_limit"`
|
||||
@ -190,6 +191,25 @@ type LinuxDoConnectConfig struct {
|
||||
UserInfoUsernamePath string `mapstructure:"userinfo_username_path"`
|
||||
}
|
||||
|
||||
type WeChatConnectConfig struct {
|
||||
Enabled bool `mapstructure:"enabled"`
|
||||
AppID string `mapstructure:"app_id"`
|
||||
AppSecret string `mapstructure:"app_secret"`
|
||||
OpenAppID string `mapstructure:"open_app_id"`
|
||||
OpenAppSecret string `mapstructure:"open_app_secret"`
|
||||
MPAppID string `mapstructure:"mp_app_id"`
|
||||
MPAppSecret string `mapstructure:"mp_app_secret"`
|
||||
MobileAppID string `mapstructure:"mobile_app_id"`
|
||||
MobileAppSecret string `mapstructure:"mobile_app_secret"`
|
||||
OpenEnabled bool `mapstructure:"open_enabled"`
|
||||
MPEnabled bool `mapstructure:"mp_enabled"`
|
||||
MobileEnabled bool `mapstructure:"mobile_enabled"`
|
||||
Mode string `mapstructure:"mode"`
|
||||
Scopes string `mapstructure:"scopes"`
|
||||
RedirectURL string `mapstructure:"redirect_url"`
|
||||
FrontendRedirectURL string `mapstructure:"frontend_redirect_url"`
|
||||
}
|
||||
|
||||
type OIDCConnectConfig struct {
|
||||
Enabled bool `mapstructure:"enabled"`
|
||||
ProviderName string `mapstructure:"provider_name"` // 显示名: "Keycloak" 等
|
||||
@ -218,6 +238,217 @@ type OIDCConnectConfig struct {
|
||||
UserInfoUsernamePath string `mapstructure:"userinfo_username_path"`
|
||||
}
|
||||
|
||||
const (
|
||||
defaultWeChatConnectMode = "open"
|
||||
defaultWeChatConnectScopes = "snsapi_login"
|
||||
defaultWeChatConnectFrontendRedirect = "/auth/wechat/callback"
|
||||
)
|
||||
|
||||
func firstNonEmptyString(values ...string) string {
|
||||
for _, value := range values {
|
||||
if trimmed := strings.TrimSpace(value); trimmed != "" {
|
||||
return trimmed
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func normalizeWeChatConnectMode(raw string) string {
|
||||
switch strings.ToLower(strings.TrimSpace(raw)) {
|
||||
case "mp":
|
||||
return "mp"
|
||||
case "mobile":
|
||||
return "mobile"
|
||||
default:
|
||||
return defaultWeChatConnectMode
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeWeChatConnectStoredMode(openEnabled, mpEnabled, mobileEnabled bool, mode string) string {
|
||||
mode = normalizeWeChatConnectMode(mode)
|
||||
switch mode {
|
||||
case "open":
|
||||
if openEnabled {
|
||||
return "open"
|
||||
}
|
||||
case "mp":
|
||||
if mpEnabled {
|
||||
return "mp"
|
||||
}
|
||||
case "mobile":
|
||||
if mobileEnabled {
|
||||
return "mobile"
|
||||
}
|
||||
}
|
||||
switch {
|
||||
case openEnabled:
|
||||
return "open"
|
||||
case mpEnabled:
|
||||
return "mp"
|
||||
case mobileEnabled:
|
||||
return "mobile"
|
||||
default:
|
||||
return mode
|
||||
}
|
||||
}
|
||||
|
||||
func defaultWeChatConnectScopesForMode(mode string) string {
|
||||
switch normalizeWeChatConnectMode(mode) {
|
||||
case "mp":
|
||||
return "snsapi_userinfo"
|
||||
case "mobile":
|
||||
return ""
|
||||
default:
|
||||
return defaultWeChatConnectScopes
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeWeChatConnectScopes(raw, mode string) string {
|
||||
switch normalizeWeChatConnectMode(mode) {
|
||||
case "mp":
|
||||
switch strings.TrimSpace(raw) {
|
||||
case "snsapi_base":
|
||||
return "snsapi_base"
|
||||
case "snsapi_userinfo":
|
||||
return "snsapi_userinfo"
|
||||
default:
|
||||
return defaultWeChatConnectScopesForMode(mode)
|
||||
}
|
||||
case "mobile":
|
||||
return ""
|
||||
default:
|
||||
return defaultWeChatConnectScopes
|
||||
}
|
||||
}
|
||||
|
||||
func shouldApplyLegacyWeChatEnv(configKey, envKey string) bool {
|
||||
if viper.InConfig(configKey) {
|
||||
return false
|
||||
}
|
||||
_, hasNewEnv := os.LookupEnv(envKey)
|
||||
return !hasNewEnv
|
||||
}
|
||||
|
||||
func applyLegacyWeChatConnectEnvCompatibility(cfg *WeChatConnectConfig) {
|
||||
if cfg == nil {
|
||||
return
|
||||
}
|
||||
|
||||
legacyOpenAppID := ""
|
||||
if shouldApplyLegacyWeChatEnv("wechat_connect.open_app_id", "WECHAT_CONNECT_OPEN_APP_ID") &&
|
||||
shouldApplyLegacyWeChatEnv("wechat_connect.app_id", "WECHAT_CONNECT_APP_ID") {
|
||||
legacyOpenAppID = strings.TrimSpace(os.Getenv("WECHAT_OAUTH_OPEN_APP_ID"))
|
||||
if legacyOpenAppID != "" {
|
||||
cfg.OpenAppID = legacyOpenAppID
|
||||
}
|
||||
}
|
||||
|
||||
legacyOpenAppSecret := ""
|
||||
if shouldApplyLegacyWeChatEnv("wechat_connect.open_app_secret", "WECHAT_CONNECT_OPEN_APP_SECRET") &&
|
||||
shouldApplyLegacyWeChatEnv("wechat_connect.app_secret", "WECHAT_CONNECT_APP_SECRET") {
|
||||
legacyOpenAppSecret = strings.TrimSpace(os.Getenv("WECHAT_OAUTH_OPEN_APP_SECRET"))
|
||||
if legacyOpenAppSecret != "" {
|
||||
cfg.OpenAppSecret = legacyOpenAppSecret
|
||||
}
|
||||
}
|
||||
|
||||
legacyMPAppID := ""
|
||||
if shouldApplyLegacyWeChatEnv("wechat_connect.mp_app_id", "WECHAT_CONNECT_MP_APP_ID") &&
|
||||
shouldApplyLegacyWeChatEnv("wechat_connect.app_id", "WECHAT_CONNECT_APP_ID") {
|
||||
legacyMPAppID = strings.TrimSpace(os.Getenv("WECHAT_OAUTH_MP_APP_ID"))
|
||||
if legacyMPAppID != "" {
|
||||
cfg.MPAppID = legacyMPAppID
|
||||
}
|
||||
}
|
||||
|
||||
legacyMPAppSecret := ""
|
||||
if shouldApplyLegacyWeChatEnv("wechat_connect.mp_app_secret", "WECHAT_CONNECT_MP_APP_SECRET") &&
|
||||
shouldApplyLegacyWeChatEnv("wechat_connect.app_secret", "WECHAT_CONNECT_APP_SECRET") {
|
||||
legacyMPAppSecret = strings.TrimSpace(os.Getenv("WECHAT_OAUTH_MP_APP_SECRET"))
|
||||
if legacyMPAppSecret != "" {
|
||||
cfg.MPAppSecret = legacyMPAppSecret
|
||||
}
|
||||
}
|
||||
|
||||
if shouldApplyLegacyWeChatEnv("wechat_connect.frontend_redirect_url", "WECHAT_CONNECT_FRONTEND_REDIRECT_URL") {
|
||||
if legacyFrontend := strings.TrimSpace(os.Getenv("WECHAT_OAUTH_FRONTEND_REDIRECT_URL")); legacyFrontend != "" {
|
||||
cfg.FrontendRedirectURL = legacyFrontend
|
||||
}
|
||||
}
|
||||
|
||||
hasLegacyOpen := legacyOpenAppID != "" && legacyOpenAppSecret != ""
|
||||
hasLegacyMP := legacyMPAppID != "" && legacyMPAppSecret != ""
|
||||
|
||||
if shouldApplyLegacyWeChatEnv("wechat_connect.enabled", "WECHAT_CONNECT_ENABLED") && (hasLegacyOpen || hasLegacyMP) {
|
||||
cfg.Enabled = true
|
||||
}
|
||||
if shouldApplyLegacyWeChatEnv("wechat_connect.open_enabled", "WECHAT_CONNECT_OPEN_ENABLED") && hasLegacyOpen {
|
||||
cfg.OpenEnabled = true
|
||||
}
|
||||
if shouldApplyLegacyWeChatEnv("wechat_connect.mp_enabled", "WECHAT_CONNECT_MP_ENABLED") && hasLegacyMP {
|
||||
cfg.MPEnabled = true
|
||||
}
|
||||
if shouldApplyLegacyWeChatEnv("wechat_connect.mode", "WECHAT_CONNECT_MODE") {
|
||||
switch {
|
||||
case hasLegacyMP && !hasLegacyOpen:
|
||||
cfg.Mode = "mp"
|
||||
case hasLegacyOpen:
|
||||
cfg.Mode = "open"
|
||||
}
|
||||
}
|
||||
if shouldApplyLegacyWeChatEnv("wechat_connect.scopes", "WECHAT_CONNECT_SCOPES") {
|
||||
switch {
|
||||
case hasLegacyMP && !hasLegacyOpen:
|
||||
cfg.Scopes = defaultWeChatConnectScopesForMode("mp")
|
||||
case hasLegacyOpen:
|
||||
cfg.Scopes = defaultWeChatConnectScopesForMode("open")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeWeChatConnectConfig(cfg *WeChatConnectConfig) {
|
||||
if cfg == nil {
|
||||
return
|
||||
}
|
||||
|
||||
cfg.AppID = strings.TrimSpace(cfg.AppID)
|
||||
cfg.AppSecret = strings.TrimSpace(cfg.AppSecret)
|
||||
cfg.OpenAppID = strings.TrimSpace(cfg.OpenAppID)
|
||||
cfg.OpenAppSecret = strings.TrimSpace(cfg.OpenAppSecret)
|
||||
cfg.MPAppID = strings.TrimSpace(cfg.MPAppID)
|
||||
cfg.MPAppSecret = strings.TrimSpace(cfg.MPAppSecret)
|
||||
cfg.MobileAppID = strings.TrimSpace(cfg.MobileAppID)
|
||||
cfg.MobileAppSecret = strings.TrimSpace(cfg.MobileAppSecret)
|
||||
cfg.Mode = normalizeWeChatConnectMode(cfg.Mode)
|
||||
cfg.RedirectURL = strings.TrimSpace(cfg.RedirectURL)
|
||||
cfg.FrontendRedirectURL = strings.TrimSpace(cfg.FrontendRedirectURL)
|
||||
|
||||
cfg.AppID = firstNonEmptyString(cfg.AppID, cfg.OpenAppID, cfg.MPAppID, cfg.MobileAppID)
|
||||
cfg.AppSecret = firstNonEmptyString(cfg.AppSecret, cfg.OpenAppSecret, cfg.MPAppSecret, cfg.MobileAppSecret)
|
||||
cfg.OpenAppID = firstNonEmptyString(cfg.OpenAppID, cfg.AppID)
|
||||
cfg.OpenAppSecret = firstNonEmptyString(cfg.OpenAppSecret, cfg.AppSecret)
|
||||
cfg.MPAppID = firstNonEmptyString(cfg.MPAppID, cfg.AppID)
|
||||
cfg.MPAppSecret = firstNonEmptyString(cfg.MPAppSecret, cfg.AppSecret)
|
||||
cfg.MobileAppID = firstNonEmptyString(cfg.MobileAppID, cfg.AppID)
|
||||
cfg.MobileAppSecret = firstNonEmptyString(cfg.MobileAppSecret, cfg.AppSecret)
|
||||
|
||||
if !cfg.OpenEnabled && !cfg.MPEnabled && !cfg.MobileEnabled && cfg.Enabled {
|
||||
switch cfg.Mode {
|
||||
case "mp":
|
||||
cfg.MPEnabled = true
|
||||
case "mobile":
|
||||
cfg.MobileEnabled = true
|
||||
default:
|
||||
cfg.OpenEnabled = true
|
||||
}
|
||||
}
|
||||
cfg.Mode = normalizeWeChatConnectStoredMode(cfg.OpenEnabled, cfg.MPEnabled, cfg.MobileEnabled, cfg.Mode)
|
||||
cfg.Scopes = normalizeWeChatConnectScopes(cfg.Scopes, cfg.Mode)
|
||||
if cfg.FrontendRedirectURL == "" {
|
||||
cfg.FrontendRedirectURL = defaultWeChatConnectFrontendRedirect
|
||||
}
|
||||
}
|
||||
|
||||
// TokenRefreshConfig OAuth token自动刷新配置
|
||||
type TokenRefreshConfig struct {
|
||||
// 是否启用自动刷新
|
||||
@ -1012,6 +1243,8 @@ func load(allowMissingJWTSecret bool) (*Config, error) {
|
||||
cfg.LinuxDo.UserInfoEmailPath = strings.TrimSpace(cfg.LinuxDo.UserInfoEmailPath)
|
||||
cfg.LinuxDo.UserInfoIDPath = strings.TrimSpace(cfg.LinuxDo.UserInfoIDPath)
|
||||
cfg.LinuxDo.UserInfoUsernamePath = strings.TrimSpace(cfg.LinuxDo.UserInfoUsernamePath)
|
||||
applyLegacyWeChatConnectEnvCompatibility(&cfg.WeChat)
|
||||
normalizeWeChatConnectConfig(&cfg.WeChat)
|
||||
cfg.OIDC.ProviderName = strings.TrimSpace(cfg.OIDC.ProviderName)
|
||||
cfg.OIDC.ClientID = strings.TrimSpace(cfg.OIDC.ClientID)
|
||||
cfg.OIDC.ClientSecret = strings.TrimSpace(cfg.OIDC.ClientSecret)
|
||||
@ -1207,6 +1440,24 @@ func setDefaults() {
|
||||
viper.SetDefault("linuxdo_connect.userinfo_id_path", "")
|
||||
viper.SetDefault("linuxdo_connect.userinfo_username_path", "")
|
||||
|
||||
// WeChat Connect OAuth 登录
|
||||
viper.SetDefault("wechat_connect.enabled", false)
|
||||
viper.SetDefault("wechat_connect.app_id", "")
|
||||
viper.SetDefault("wechat_connect.app_secret", "")
|
||||
viper.SetDefault("wechat_connect.open_app_id", "")
|
||||
viper.SetDefault("wechat_connect.open_app_secret", "")
|
||||
viper.SetDefault("wechat_connect.mp_app_id", "")
|
||||
viper.SetDefault("wechat_connect.mp_app_secret", "")
|
||||
viper.SetDefault("wechat_connect.mobile_app_id", "")
|
||||
viper.SetDefault("wechat_connect.mobile_app_secret", "")
|
||||
viper.SetDefault("wechat_connect.open_enabled", false)
|
||||
viper.SetDefault("wechat_connect.mp_enabled", false)
|
||||
viper.SetDefault("wechat_connect.mobile_enabled", false)
|
||||
viper.SetDefault("wechat_connect.mode", defaultWeChatConnectMode)
|
||||
viper.SetDefault("wechat_connect.scopes", defaultWeChatConnectScopes)
|
||||
viper.SetDefault("wechat_connect.redirect_url", "")
|
||||
viper.SetDefault("wechat_connect.frontend_redirect_url", defaultWeChatConnectFrontendRedirect)
|
||||
|
||||
// Generic OIDC OAuth 登录
|
||||
viper.SetDefault("oidc_connect.enabled", false)
|
||||
viper.SetDefault("oidc_connect.provider_name", "OIDC")
|
||||
@ -1222,8 +1473,8 @@ func setDefaults() {
|
||||
viper.SetDefault("oidc_connect.redirect_url", "")
|
||||
viper.SetDefault("oidc_connect.frontend_redirect_url", "/auth/oidc/callback")
|
||||
viper.SetDefault("oidc_connect.token_auth_method", "client_secret_post")
|
||||
viper.SetDefault("oidc_connect.use_pkce", false)
|
||||
viper.SetDefault("oidc_connect.validate_id_token", false)
|
||||
viper.SetDefault("oidc_connect.use_pkce", true)
|
||||
viper.SetDefault("oidc_connect.validate_id_token", true)
|
||||
viper.SetDefault("oidc_connect.allowed_signing_algs", "RS256,ES256,PS256")
|
||||
viper.SetDefault("oidc_connect.clock_skew_seconds", 120)
|
||||
viper.SetDefault("oidc_connect.require_email_verified", false)
|
||||
@ -1664,6 +1915,45 @@ func (c *Config) Validate() error {
|
||||
warnIfInsecureURL("linuxdo_connect.redirect_url", c.LinuxDo.RedirectURL)
|
||||
warnIfInsecureURL("linuxdo_connect.frontend_redirect_url", c.LinuxDo.FrontendRedirectURL)
|
||||
}
|
||||
if c.WeChat.Enabled {
|
||||
weChat := c.WeChat
|
||||
normalizeWeChatConnectConfig(&weChat)
|
||||
|
||||
if weChat.OpenEnabled {
|
||||
if strings.TrimSpace(weChat.OpenAppID) == "" {
|
||||
return fmt.Errorf("wechat_connect.open_app_id is required when wechat_connect.open_enabled=true")
|
||||
}
|
||||
if strings.TrimSpace(weChat.OpenAppSecret) == "" {
|
||||
return fmt.Errorf("wechat_connect.open_app_secret is required when wechat_connect.open_enabled=true")
|
||||
}
|
||||
}
|
||||
if weChat.MPEnabled {
|
||||
if strings.TrimSpace(weChat.MPAppID) == "" {
|
||||
return fmt.Errorf("wechat_connect.mp_app_id is required when wechat_connect.mp_enabled=true")
|
||||
}
|
||||
if strings.TrimSpace(weChat.MPAppSecret) == "" {
|
||||
return fmt.Errorf("wechat_connect.mp_app_secret is required when wechat_connect.mp_enabled=true")
|
||||
}
|
||||
}
|
||||
if weChat.MobileEnabled {
|
||||
if strings.TrimSpace(weChat.MobileAppID) == "" {
|
||||
return fmt.Errorf("wechat_connect.mobile_app_id is required when wechat_connect.mobile_enabled=true")
|
||||
}
|
||||
if strings.TrimSpace(weChat.MobileAppSecret) == "" {
|
||||
return fmt.Errorf("wechat_connect.mobile_app_secret is required when wechat_connect.mobile_enabled=true")
|
||||
}
|
||||
}
|
||||
if v := strings.TrimSpace(weChat.RedirectURL); v != "" {
|
||||
if err := ValidateAbsoluteHTTPURL(v); err != nil {
|
||||
return fmt.Errorf("wechat_connect.redirect_url invalid: %w", err)
|
||||
}
|
||||
warnIfInsecureURL("wechat_connect.redirect_url", v)
|
||||
}
|
||||
if err := ValidateFrontendRedirectURL(weChat.FrontendRedirectURL); err != nil {
|
||||
return fmt.Errorf("wechat_connect.frontend_redirect_url invalid: %w", err)
|
||||
}
|
||||
warnIfInsecureURL("wechat_connect.frontend_redirect_url", weChat.FrontendRedirectURL)
|
||||
}
|
||||
if c.OIDC.Enabled {
|
||||
if strings.TrimSpace(c.OIDC.ClientID) == "" {
|
||||
return fmt.Errorf("oidc_connect.client_id is required when oidc_connect.enabled=true")
|
||||
|
||||
@ -225,6 +225,37 @@ func TestLoadSchedulingConfigFromEnv(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadWeChatConnectConfigFromLegacyEnv(t *testing.T) {
|
||||
resetViperWithJWTSecret(t)
|
||||
t.Setenv("WECHAT_OAUTH_OPEN_APP_ID", "wx-open-app")
|
||||
t.Setenv("WECHAT_OAUTH_OPEN_APP_SECRET", "wx-open-secret")
|
||||
t.Setenv("WECHAT_OAUTH_MP_APP_ID", "wx-mp-app")
|
||||
t.Setenv("WECHAT_OAUTH_MP_APP_SECRET", "wx-mp-secret")
|
||||
t.Setenv("WECHAT_OAUTH_FRONTEND_REDIRECT_URL", "/auth/wechat/legacy-callback")
|
||||
|
||||
cfg, err := Load()
|
||||
require.NoError(t, err)
|
||||
require.True(t, cfg.WeChat.Enabled)
|
||||
require.True(t, cfg.WeChat.OpenEnabled)
|
||||
require.True(t, cfg.WeChat.MPEnabled)
|
||||
require.False(t, cfg.WeChat.MobileEnabled)
|
||||
require.Equal(t, "open", cfg.WeChat.Mode)
|
||||
require.Equal(t, "wx-open-app", cfg.WeChat.OpenAppID)
|
||||
require.Equal(t, "wx-open-secret", cfg.WeChat.OpenAppSecret)
|
||||
require.Equal(t, "wx-mp-app", cfg.WeChat.MPAppID)
|
||||
require.Equal(t, "wx-mp-secret", cfg.WeChat.MPAppSecret)
|
||||
require.Equal(t, "/auth/wechat/legacy-callback", cfg.WeChat.FrontendRedirectURL)
|
||||
}
|
||||
|
||||
func TestLoadDefaultOIDCSecurityDefaults(t *testing.T) {
|
||||
resetViperWithJWTSecret(t)
|
||||
|
||||
cfg, err := Load()
|
||||
require.NoError(t, err)
|
||||
require.True(t, cfg.OIDC.UsePKCE)
|
||||
require.True(t, cfg.OIDC.ValidateIDToken)
|
||||
}
|
||||
|
||||
func TestLoadForcedCodexInstructionsTemplate(t *testing.T) {
|
||||
resetViperWithJWTSecret(t)
|
||||
|
||||
@ -424,7 +455,7 @@ func TestValidateOIDCAllowsIssuerOnlyEndpointsWithDiscoveryFallback(t *testing.T
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateOIDCAllowsDisablingPKCEAndIDTokenValidation(t *testing.T) {
|
||||
func TestValidateOIDCAllowsExplicitCompatibilityOverridesForPKCEAndIDTokenValidation(t *testing.T) {
|
||||
resetViperWithJWTSecret(t)
|
||||
|
||||
cfg, err := Load()
|
||||
|
||||
@ -565,6 +565,15 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
|
||||
req.WeChatConnectScopes = strings.TrimSpace(req.WeChatConnectScopes)
|
||||
req.WeChatConnectRedirectURL = strings.TrimSpace(req.WeChatConnectRedirectURL)
|
||||
req.WeChatConnectFrontendRedirectURL = strings.TrimSpace(req.WeChatConnectFrontendRedirectURL)
|
||||
req.WeChatConnectAppID = strings.TrimSpace(firstNonEmpty(req.WeChatConnectAppID, previousSettings.WeChatConnectAppID))
|
||||
req.WeChatConnectRedirectURL = strings.TrimSpace(firstNonEmpty(req.WeChatConnectRedirectURL, previousSettings.WeChatConnectRedirectURL))
|
||||
req.WeChatConnectFrontendRedirectURL = strings.TrimSpace(firstNonEmpty(req.WeChatConnectFrontendRedirectURL, previousSettings.WeChatConnectFrontendRedirectURL))
|
||||
if req.WeChatConnectMode == "" {
|
||||
req.WeChatConnectMode = strings.ToLower(strings.TrimSpace(previousSettings.WeChatConnectMode))
|
||||
}
|
||||
if req.WeChatConnectScopes == "" {
|
||||
req.WeChatConnectScopes = strings.TrimSpace(previousSettings.WeChatConnectScopes)
|
||||
}
|
||||
|
||||
if req.WeChatConnectMPEnabled && req.WeChatConnectMobileEnabled {
|
||||
response.BadRequest(c, "WeChat Official Account and Mobile App cannot be enabled at the same time")
|
||||
@ -598,9 +607,9 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
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))
|
||||
req.WeChatConnectOpenAppID = strings.TrimSpace(firstNonEmpty(req.WeChatConnectOpenAppID, req.WeChatConnectAppID, previousSettings.WeChatConnectOpenAppID, previousSettings.WeChatConnectAppID))
|
||||
req.WeChatConnectMPAppID = strings.TrimSpace(firstNonEmpty(req.WeChatConnectMPAppID, req.WeChatConnectAppID, previousSettings.WeChatConnectMPAppID, previousSettings.WeChatConnectAppID))
|
||||
req.WeChatConnectMobileAppID = strings.TrimSpace(firstNonEmpty(req.WeChatConnectMobileAppID, req.WeChatConnectAppID, previousSettings.WeChatConnectMobileAppID, previousSettings.WeChatConnectAppID))
|
||||
|
||||
if req.WeChatConnectOpenAppSecret == "" {
|
||||
req.WeChatConnectOpenAppSecret = strings.TrimSpace(firstNonEmpty(previousSettings.WeChatConnectOpenAppSecret, previousSettings.WeChatConnectAppSecret, req.WeChatConnectAppSecret))
|
||||
@ -691,10 +700,35 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
|
||||
req.OIDCConnectUserInfoEmailPath = strings.TrimSpace(req.OIDCConnectUserInfoEmailPath)
|
||||
req.OIDCConnectUserInfoIDPath = strings.TrimSpace(req.OIDCConnectUserInfoIDPath)
|
||||
req.OIDCConnectUserInfoUsernamePath = strings.TrimSpace(req.OIDCConnectUserInfoUsernamePath)
|
||||
|
||||
if req.OIDCConnectProviderName == "" {
|
||||
req.OIDCConnectProviderName = "OIDC"
|
||||
req.OIDCConnectProviderName = strings.TrimSpace(firstNonEmpty(req.OIDCConnectProviderName, previousSettings.OIDCConnectProviderName, "OIDC"))
|
||||
req.OIDCConnectClientID = strings.TrimSpace(firstNonEmpty(req.OIDCConnectClientID, previousSettings.OIDCConnectClientID))
|
||||
req.OIDCConnectIssuerURL = strings.TrimSpace(firstNonEmpty(req.OIDCConnectIssuerURL, previousSettings.OIDCConnectIssuerURL))
|
||||
req.OIDCConnectDiscoveryURL = strings.TrimSpace(firstNonEmpty(req.OIDCConnectDiscoveryURL, previousSettings.OIDCConnectDiscoveryURL))
|
||||
req.OIDCConnectAuthorizeURL = strings.TrimSpace(firstNonEmpty(req.OIDCConnectAuthorizeURL, previousSettings.OIDCConnectAuthorizeURL))
|
||||
req.OIDCConnectTokenURL = strings.TrimSpace(firstNonEmpty(req.OIDCConnectTokenURL, previousSettings.OIDCConnectTokenURL))
|
||||
req.OIDCConnectUserInfoURL = strings.TrimSpace(firstNonEmpty(req.OIDCConnectUserInfoURL, previousSettings.OIDCConnectUserInfoURL))
|
||||
req.OIDCConnectJWKSURL = strings.TrimSpace(firstNonEmpty(req.OIDCConnectJWKSURL, previousSettings.OIDCConnectJWKSURL))
|
||||
req.OIDCConnectScopes = strings.TrimSpace(firstNonEmpty(req.OIDCConnectScopes, previousSettings.OIDCConnectScopes, "openid email profile"))
|
||||
req.OIDCConnectRedirectURL = strings.TrimSpace(firstNonEmpty(req.OIDCConnectRedirectURL, previousSettings.OIDCConnectRedirectURL))
|
||||
req.OIDCConnectFrontendRedirectURL = strings.TrimSpace(firstNonEmpty(req.OIDCConnectFrontendRedirectURL, previousSettings.OIDCConnectFrontendRedirectURL, "/auth/oidc/callback"))
|
||||
req.OIDCConnectTokenAuthMethod = strings.ToLower(strings.TrimSpace(firstNonEmpty(req.OIDCConnectTokenAuthMethod, previousSettings.OIDCConnectTokenAuthMethod, "client_secret_post")))
|
||||
req.OIDCConnectAllowedSigningAlgs = strings.TrimSpace(firstNonEmpty(req.OIDCConnectAllowedSigningAlgs, previousSettings.OIDCConnectAllowedSigningAlgs, "RS256,ES256,PS256"))
|
||||
req.OIDCConnectUserInfoEmailPath = strings.TrimSpace(firstNonEmpty(req.OIDCConnectUserInfoEmailPath, previousSettings.OIDCConnectUserInfoEmailPath))
|
||||
req.OIDCConnectUserInfoIDPath = strings.TrimSpace(firstNonEmpty(req.OIDCConnectUserInfoIDPath, previousSettings.OIDCConnectUserInfoIDPath))
|
||||
req.OIDCConnectUserInfoUsernamePath = strings.TrimSpace(firstNonEmpty(req.OIDCConnectUserInfoUsernamePath, previousSettings.OIDCConnectUserInfoUsernamePath))
|
||||
if !req.OIDCConnectUsePKCE {
|
||||
req.OIDCConnectUsePKCE = previousSettings.OIDCConnectUsePKCE
|
||||
}
|
||||
if !req.OIDCConnectValidateIDToken {
|
||||
req.OIDCConnectValidateIDToken = previousSettings.OIDCConnectValidateIDToken
|
||||
}
|
||||
if req.OIDCConnectClockSkewSeconds == 0 {
|
||||
req.OIDCConnectClockSkewSeconds = previousSettings.OIDCConnectClockSkewSeconds
|
||||
if req.OIDCConnectClockSkewSeconds == 0 {
|
||||
req.OIDCConnectClockSkewSeconds = 120
|
||||
}
|
||||
}
|
||||
|
||||
if req.OIDCConnectClientID == "" {
|
||||
response.BadRequest(c, "OIDC Client ID is required when enabled")
|
||||
return
|
||||
|
||||
@ -784,6 +784,198 @@ func TestAPIContracts(t *testing.T) {
|
||||
}
|
||||
}`,
|
||||
},
|
||||
{
|
||||
name: "GET /api/v1/admin/settings falls back to config oauth defaults",
|
||||
setup: func(t *testing.T, deps *contractDeps) {
|
||||
t.Helper()
|
||||
deps.cfg.OIDC = config.OIDCConnectConfig{
|
||||
Enabled: true,
|
||||
ProviderName: "ConfigOIDC",
|
||||
ClientID: "oidc-config-client",
|
||||
ClientSecret: "oidc-config-secret",
|
||||
IssuerURL: "https://issuer.example.com",
|
||||
RedirectURL: "https://api.example.com/api/v1/auth/oauth/oidc/callback",
|
||||
FrontendRedirectURL: "/auth/oidc/callback",
|
||||
Scopes: "openid email profile",
|
||||
TokenAuthMethod: "client_secret_post",
|
||||
UsePKCE: true,
|
||||
ValidateIDToken: true,
|
||||
AllowedSigningAlgs: "RS256,ES256,PS256",
|
||||
ClockSkewSeconds: 120,
|
||||
}
|
||||
deps.cfg.WeChat = config.WeChatConnectConfig{
|
||||
Enabled: true,
|
||||
OpenEnabled: true,
|
||||
OpenAppID: "wx-open-config",
|
||||
OpenAppSecret: "wx-open-secret",
|
||||
Mode: "open",
|
||||
Scopes: "snsapi_login",
|
||||
FrontendRedirectURL: "/auth/wechat/callback",
|
||||
}
|
||||
deps.settingRepo.SetAll(map[string]string{
|
||||
service.SettingKeyRegistrationEnabled: "true",
|
||||
service.SettingKeyEmailVerifyEnabled: "false",
|
||||
service.SettingKeyRegistrationEmailSuffixWhitelist: "[]",
|
||||
})
|
||||
},
|
||||
method: http.MethodGet,
|
||||
path: "/api/v1/admin/settings",
|
||||
wantStatus: http.StatusOK,
|
||||
wantJSON: `{
|
||||
"code": 0,
|
||||
"message": "success",
|
||||
"data": {
|
||||
"registration_enabled": true,
|
||||
"email_verify_enabled": false,
|
||||
"registration_email_suffix_whitelist": [],
|
||||
"promo_code_enabled": true,
|
||||
"password_reset_enabled": false,
|
||||
"frontend_url": "",
|
||||
"invitation_code_enabled": false,
|
||||
"totp_enabled": false,
|
||||
"totp_encryption_key_configured": false,
|
||||
"smtp_host": "",
|
||||
"smtp_port": 587,
|
||||
"smtp_username": "",
|
||||
"smtp_password_configured": false,
|
||||
"smtp_from_email": "",
|
||||
"smtp_from_name": "",
|
||||
"smtp_use_tls": false,
|
||||
"turnstile_enabled": false,
|
||||
"turnstile_site_key": "",
|
||||
"turnstile_secret_key_configured": false,
|
||||
"linuxdo_connect_enabled": false,
|
||||
"linuxdo_connect_client_id": "",
|
||||
"linuxdo_connect_client_secret_configured": false,
|
||||
"linuxdo_connect_redirect_url": "",
|
||||
"oidc_connect_enabled": true,
|
||||
"oidc_connect_provider_name": "ConfigOIDC",
|
||||
"oidc_connect_client_id": "oidc-config-client",
|
||||
"oidc_connect_client_secret_configured": true,
|
||||
"oidc_connect_issuer_url": "https://issuer.example.com",
|
||||
"oidc_connect_discovery_url": "",
|
||||
"oidc_connect_authorize_url": "",
|
||||
"oidc_connect_token_url": "",
|
||||
"oidc_connect_userinfo_url": "",
|
||||
"oidc_connect_jwks_url": "",
|
||||
"oidc_connect_scopes": "openid email profile",
|
||||
"oidc_connect_redirect_url": "https://api.example.com/api/v1/auth/oauth/oidc/callback",
|
||||
"oidc_connect_frontend_redirect_url": "/auth/oidc/callback",
|
||||
"oidc_connect_token_auth_method": "client_secret_post",
|
||||
"oidc_connect_use_pkce": true,
|
||||
"oidc_connect_validate_id_token": true,
|
||||
"oidc_connect_allowed_signing_algs": "RS256,ES256,PS256",
|
||||
"oidc_connect_clock_skew_seconds": 120,
|
||||
"oidc_connect_require_email_verified": false,
|
||||
"oidc_connect_userinfo_email_path": "",
|
||||
"oidc_connect_userinfo_id_path": "",
|
||||
"oidc_connect_userinfo_username_path": "",
|
||||
"site_name": "Sub2API",
|
||||
"site_logo": "",
|
||||
"site_subtitle": "Subscription to API Conversion Platform",
|
||||
"api_base_url": "",
|
||||
"contact_info": "",
|
||||
"doc_url": "",
|
||||
"home_content": "",
|
||||
"hide_ccs_import_button": false,
|
||||
"purchase_subscription_enabled": false,
|
||||
"purchase_subscription_url": "",
|
||||
"table_default_page_size": 20,
|
||||
"table_page_size_options": [10, 20, 50],
|
||||
"custom_menu_items": [],
|
||||
"custom_endpoints": [],
|
||||
"default_concurrency": 0,
|
||||
"default_balance": 0,
|
||||
"default_subscriptions": [],
|
||||
"enable_model_fallback": false,
|
||||
"fallback_model_anthropic": "claude-3-5-sonnet-20241022",
|
||||
"fallback_model_openai": "gpt-4o",
|
||||
"fallback_model_gemini": "gemini-2.5-pro",
|
||||
"fallback_model_antigravity": "gemini-2.5-pro",
|
||||
"enable_identity_patch": true,
|
||||
"identity_patch_prompt": "",
|
||||
"ops_monitoring_enabled": false,
|
||||
"ops_realtime_monitoring_enabled": true,
|
||||
"ops_query_mode_default": "auto",
|
||||
"ops_metrics_interval_seconds": 60,
|
||||
"min_claude_code_version": "",
|
||||
"max_claude_code_version": "",
|
||||
"allow_ungrouped_key_scheduling": false,
|
||||
"backend_mode_enabled": false,
|
||||
"enable_fingerprint_unification": true,
|
||||
"enable_metadata_passthrough": false,
|
||||
"enable_cch_signing": false,
|
||||
"web_search_emulation_enabled": false,
|
||||
"payment_visible_method_alipay_source": "",
|
||||
"payment_visible_method_wxpay_source": "",
|
||||
"payment_visible_method_alipay_enabled": false,
|
||||
"payment_visible_method_wxpay_enabled": false,
|
||||
"openai_advanced_scheduler_enabled": false,
|
||||
"payment_enabled": false,
|
||||
"payment_min_amount": 0,
|
||||
"payment_max_amount": 0,
|
||||
"payment_daily_limit": 0,
|
||||
"payment_order_timeout_minutes": 0,
|
||||
"payment_max_pending_orders": 0,
|
||||
"payment_enabled_types": null,
|
||||
"payment_balance_disabled": false,
|
||||
"payment_balance_recharge_multiplier": 0,
|
||||
"payment_recharge_fee_rate": 0,
|
||||
"payment_load_balance_strategy": "",
|
||||
"payment_product_name_prefix": "",
|
||||
"payment_product_name_suffix": "",
|
||||
"payment_help_image_url": "",
|
||||
"payment_help_text": "",
|
||||
"payment_cancel_rate_limit_enabled": false,
|
||||
"payment_cancel_rate_limit_max": 0,
|
||||
"payment_cancel_rate_limit_window": 0,
|
||||
"payment_cancel_rate_limit_unit": "",
|
||||
"payment_cancel_rate_limit_window_mode": "",
|
||||
"balance_low_notify_enabled": false,
|
||||
"account_quota_notify_enabled": false,
|
||||
"balance_low_notify_threshold": 0,
|
||||
"balance_low_notify_recharge_url": "",
|
||||
"account_quota_notify_emails": [],
|
||||
"wechat_connect_enabled": true,
|
||||
"wechat_connect_app_id": "wx-open-config",
|
||||
"wechat_connect_app_secret_configured": true,
|
||||
"wechat_connect_mode": "open",
|
||||
"wechat_connect_open_enabled": true,
|
||||
"wechat_connect_open_app_id": "wx-open-config",
|
||||
"wechat_connect_open_app_secret_configured": true,
|
||||
"wechat_connect_mp_enabled": false,
|
||||
"wechat_connect_mp_app_id": "wx-open-config",
|
||||
"wechat_connect_mp_app_secret_configured": true,
|
||||
"wechat_connect_mobile_enabled": false,
|
||||
"wechat_connect_mobile_app_id": "wx-open-config",
|
||||
"wechat_connect_mobile_app_secret_configured": true,
|
||||
"wechat_connect_redirect_url": "",
|
||||
"wechat_connect_frontend_redirect_url": "/auth/wechat/callback",
|
||||
"wechat_connect_scopes": "snsapi_login",
|
||||
"auth_source_default_email_balance": 0,
|
||||
"auth_source_default_email_concurrency": 5,
|
||||
"auth_source_default_email_subscriptions": [],
|
||||
"auth_source_default_email_grant_on_signup": false,
|
||||
"auth_source_default_email_grant_on_first_bind": false,
|
||||
"auth_source_default_linuxdo_balance": 0,
|
||||
"auth_source_default_linuxdo_concurrency": 5,
|
||||
"auth_source_default_linuxdo_subscriptions": [],
|
||||
"auth_source_default_linuxdo_grant_on_signup": false,
|
||||
"auth_source_default_linuxdo_grant_on_first_bind": false,
|
||||
"auth_source_default_oidc_balance": 0,
|
||||
"auth_source_default_oidc_concurrency": 5,
|
||||
"auth_source_default_oidc_subscriptions": [],
|
||||
"auth_source_default_oidc_grant_on_signup": false,
|
||||
"auth_source_default_oidc_grant_on_first_bind": false,
|
||||
"auth_source_default_wechat_balance": 0,
|
||||
"auth_source_default_wechat_concurrency": 5,
|
||||
"auth_source_default_wechat_subscriptions": [],
|
||||
"auth_source_default_wechat_grant_on_signup": false,
|
||||
"auth_source_default_wechat_grant_on_first_bind": false,
|
||||
"force_email_on_third_party_signup": false
|
||||
}
|
||||
}`,
|
||||
},
|
||||
{
|
||||
name: "POST /api/v1/admin/accounts/bulk-update",
|
||||
method: http.MethodPost,
|
||||
@ -827,6 +1019,7 @@ func TestAPIContracts(t *testing.T) {
|
||||
type contractDeps struct {
|
||||
now time.Time
|
||||
router http.Handler
|
||||
cfg *config.Config
|
||||
apiKeyRepo *stubApiKeyRepo
|
||||
groupRepo *stubGroupRepo
|
||||
userSubRepo *stubUserSubscriptionRepo
|
||||
@ -947,6 +1140,7 @@ func newContractDeps(t *testing.T) *contractDeps {
|
||||
return &contractDeps{
|
||||
now: now,
|
||||
router: r,
|
||||
cfg: cfg,
|
||||
apiKeyRepo: apiKeyRepo,
|
||||
groupRepo: groupRepo,
|
||||
userSubRepo: userSubRepo,
|
||||
|
||||
@ -245,15 +245,107 @@ func parseWeChatConnectCapabilitySettings(settings map[string]string, enabled bo
|
||||
}
|
||||
|
||||
func normalizeWeChatConnectStoredMode(openEnabled, mpEnabled, mobileEnabled bool, mode string) string {
|
||||
mode = normalizeWeChatConnectModeSetting(mode)
|
||||
switch mode {
|
||||
case "open":
|
||||
if openEnabled {
|
||||
return "open"
|
||||
}
|
||||
case "mp":
|
||||
if mpEnabled {
|
||||
return "mp"
|
||||
}
|
||||
case "mobile":
|
||||
if mobileEnabled {
|
||||
return "mobile"
|
||||
}
|
||||
}
|
||||
switch {
|
||||
case openEnabled:
|
||||
return "open"
|
||||
case mpEnabled:
|
||||
return "mp"
|
||||
case mobileEnabled:
|
||||
return "mobile"
|
||||
case openEnabled:
|
||||
return "open"
|
||||
default:
|
||||
return normalizeWeChatConnectModeSetting(mode)
|
||||
return mode
|
||||
}
|
||||
}
|
||||
|
||||
func mergeWeChatConnectCapabilitySettings(settings map[string]string, base config.WeChatConnectConfig, enabled bool, mode string) (bool, bool, bool) {
|
||||
mode = normalizeWeChatConnectModeSetting(firstNonEmpty(mode, base.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 || mobileConfigured {
|
||||
return parseWeChatConnectCapabilitySettings(settings, enabled, mode)
|
||||
}
|
||||
if !enabled {
|
||||
return false, false, false
|
||||
}
|
||||
if base.OpenEnabled || base.MPEnabled || base.MobileEnabled {
|
||||
return base.OpenEnabled, base.MPEnabled, base.MobileEnabled
|
||||
}
|
||||
return parseWeChatConnectCapabilitySettings(settings, enabled, mode)
|
||||
}
|
||||
|
||||
func (s *SettingService) effectiveWeChatConnectOAuthConfig(settings map[string]string) WeChatConnectOAuthConfig {
|
||||
base := config.WeChatConnectConfig{}
|
||||
if s != nil && s.cfg != nil {
|
||||
base = s.cfg.WeChat
|
||||
}
|
||||
|
||||
enabled := base.Enabled
|
||||
if raw, ok := settings[SettingKeyWeChatConnectEnabled]; ok {
|
||||
enabled = strings.TrimSpace(raw) == "true"
|
||||
}
|
||||
|
||||
legacyAppID := strings.TrimSpace(firstNonEmpty(
|
||||
settings[SettingKeyWeChatConnectAppID],
|
||||
base.AppID,
|
||||
base.OpenAppID,
|
||||
base.MPAppID,
|
||||
base.MobileAppID,
|
||||
))
|
||||
legacyAppSecret := strings.TrimSpace(firstNonEmpty(
|
||||
settings[SettingKeyWeChatConnectAppSecret],
|
||||
base.AppSecret,
|
||||
base.OpenAppSecret,
|
||||
base.MPAppSecret,
|
||||
base.MobileAppSecret,
|
||||
))
|
||||
openAppID := strings.TrimSpace(firstNonEmpty(settings[SettingKeyWeChatConnectOpenAppID], base.OpenAppID, legacyAppID))
|
||||
openAppSecret := strings.TrimSpace(firstNonEmpty(settings[SettingKeyWeChatConnectOpenAppSecret], base.OpenAppSecret, legacyAppSecret))
|
||||
mpAppID := strings.TrimSpace(firstNonEmpty(settings[SettingKeyWeChatConnectMPAppID], base.MPAppID, legacyAppID))
|
||||
mpAppSecret := strings.TrimSpace(firstNonEmpty(settings[SettingKeyWeChatConnectMPAppSecret], base.MPAppSecret, legacyAppSecret))
|
||||
mobileAppID := strings.TrimSpace(firstNonEmpty(settings[SettingKeyWeChatConnectMobileAppID], base.MobileAppID, legacyAppID))
|
||||
mobileAppSecret := strings.TrimSpace(firstNonEmpty(settings[SettingKeyWeChatConnectMobileAppSecret], base.MobileAppSecret, legacyAppSecret))
|
||||
|
||||
modeRaw := firstNonEmpty(settings[SettingKeyWeChatConnectMode], base.Mode)
|
||||
openEnabled, mpEnabled, mobileEnabled := mergeWeChatConnectCapabilitySettings(settings, base, enabled, modeRaw)
|
||||
mode := normalizeWeChatConnectStoredMode(openEnabled, mpEnabled, mobileEnabled, modeRaw)
|
||||
|
||||
return WeChatConnectOAuthConfig{
|
||||
Enabled: enabled,
|
||||
LegacyAppID: legacyAppID,
|
||||
LegacyAppSecret: legacyAppSecret,
|
||||
OpenAppID: openAppID,
|
||||
OpenAppSecret: openAppSecret,
|
||||
MPAppID: mpAppID,
|
||||
MPAppSecret: mpAppSecret,
|
||||
MobileAppID: mobileAppID,
|
||||
MobileAppSecret: mobileAppSecret,
|
||||
OpenEnabled: openEnabled,
|
||||
MPEnabled: mpEnabled,
|
||||
MobileEnabled: mobileEnabled,
|
||||
Mode: mode,
|
||||
Scopes: normalizeWeChatConnectScopeSetting(firstNonEmpty(settings[SettingKeyWeChatConnectScopes], base.Scopes), mode),
|
||||
RedirectURL: strings.TrimSpace(firstNonEmpty(settings[SettingKeyWeChatConnectRedirectURL], base.RedirectURL)),
|
||||
FrontendRedirectURL: strings.TrimSpace(firstNonEmpty(settings[SettingKeyWeChatConnectFrontendRedirectURL], base.FrontendRedirectURL, defaultWeChatConnectFrontend)),
|
||||
}
|
||||
}
|
||||
|
||||
@ -535,32 +627,7 @@ 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, mobileEnabled := parseWeChatConnectCapabilitySettings(settings, enabled, mode)
|
||||
mode = normalizeWeChatConnectStoredMode(openEnabled, mpEnabled, mobileEnabled, mode)
|
||||
|
||||
cfg := WeChatConnectOAuthConfig{
|
||||
Enabled: enabled,
|
||||
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]),
|
||||
FrontendRedirectURL: strings.TrimSpace(settings[SettingKeyWeChatConnectFrontendRedirectURL]),
|
||||
}
|
||||
if cfg.FrontendRedirectURL == "" {
|
||||
cfg.FrontendRedirectURL = defaultWeChatConnectFrontend
|
||||
}
|
||||
cfg := s.effectiveWeChatConnectOAuthConfig(settings)
|
||||
|
||||
if !cfg.Enabled || (!cfg.OpenEnabled && !cfg.MPEnabled) {
|
||||
return WeChatConnectOAuthConfig{}, infraerrors.NotFound("OAUTH_DISABLED", "wechat oauth is disabled")
|
||||
@ -589,14 +656,10 @@ func (s *SettingService) parseWeChatConnectOAuthConfig(settings map[string]strin
|
||||
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")
|
||||
}
|
||||
if cfg.FrontendRedirectURL == "" {
|
||||
return WeChatConnectOAuthConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "wechat oauth frontend redirect url not configured")
|
||||
}
|
||||
if err := config.ValidateAbsoluteHTTPURL(cfg.RedirectURL); err != nil {
|
||||
return WeChatConnectOAuthConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "wechat oauth redirect url invalid")
|
||||
if v := strings.TrimSpace(cfg.RedirectURL); v != "" {
|
||||
if err := config.ValidateAbsoluteHTTPURL(v); err != nil {
|
||||
return WeChatConnectOAuthConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "wechat oauth redirect url invalid")
|
||||
}
|
||||
}
|
||||
if err := config.ValidateFrontendRedirectURL(cfg.FrontendRedirectURL); err != nil {
|
||||
return WeChatConnectOAuthConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "wechat oauth frontend redirect url invalid")
|
||||
@ -605,31 +668,14 @@ func (s *SettingService) parseWeChatConnectOAuthConfig(settings map[string]strin
|
||||
}
|
||||
|
||||
func (s *SettingService) weChatOAuthCapabilitiesFromSettings(settings map[string]string) (bool, bool, bool, bool) {
|
||||
if settings[SettingKeyWeChatConnectEnabled] != "true" {
|
||||
cfg := s.effectiveWeChatConnectOAuthConfig(settings)
|
||||
if !cfg.Enabled {
|
||||
return false, false, false, false
|
||||
}
|
||||
|
||||
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 != ""
|
||||
openReady := cfg.OpenEnabled && cfg.AppIDForMode("open") != "" && cfg.AppSecretForMode("open") != ""
|
||||
mpReady := cfg.MPEnabled && cfg.AppIDForMode("mp") != "" && cfg.AppSecretForMode("mp") != ""
|
||||
mobileReady := cfg.MobileEnabled && cfg.AppIDForMode("mobile") != "" && cfg.AppSecretForMode("mobile") != ""
|
||||
|
||||
return openReady || mpReady, openReady, mpReady, mobileReady
|
||||
}
|
||||
@ -1436,6 +1482,8 @@ func (s *SettingService) InitializeDefaultSettings(ctx context.Context) error {
|
||||
SettingKeyCustomMenuItems: "[]",
|
||||
SettingKeyCustomEndpoints: "[]",
|
||||
SettingKeyWeChatConnectEnabled: "false",
|
||||
SettingKeyWeChatConnectAppID: "",
|
||||
SettingKeyWeChatConnectAppSecret: "",
|
||||
SettingKeyWeChatConnectOpenAppID: "",
|
||||
SettingKeyWeChatConnectOpenAppSecret: "",
|
||||
SettingKeyWeChatConnectMPAppID: "",
|
||||
@ -1447,9 +1495,30 @@ func (s *SettingService) InitializeDefaultSettings(ctx context.Context) error {
|
||||
SettingKeyWeChatConnectMobileEnabled: "false",
|
||||
SettingKeyWeChatConnectMode: "open",
|
||||
SettingKeyWeChatConnectScopes: "snsapi_login",
|
||||
SettingKeyWeChatConnectRedirectURL: "",
|
||||
SettingKeyWeChatConnectFrontendRedirectURL: defaultWeChatConnectFrontend,
|
||||
SettingKeyOIDCConnectEnabled: "false",
|
||||
SettingKeyOIDCConnectProviderName: "OIDC",
|
||||
SettingKeyOIDCConnectClientID: "",
|
||||
SettingKeyOIDCConnectClientSecret: "",
|
||||
SettingKeyOIDCConnectIssuerURL: "",
|
||||
SettingKeyOIDCConnectDiscoveryURL: "",
|
||||
SettingKeyOIDCConnectAuthorizeURL: "",
|
||||
SettingKeyOIDCConnectTokenURL: "",
|
||||
SettingKeyOIDCConnectUserInfoURL: "",
|
||||
SettingKeyOIDCConnectJWKSURL: "",
|
||||
SettingKeyOIDCConnectScopes: "openid email profile",
|
||||
SettingKeyOIDCConnectRedirectURL: "",
|
||||
SettingKeyOIDCConnectFrontendRedirectURL: "/auth/oidc/callback",
|
||||
SettingKeyOIDCConnectTokenAuthMethod: "client_secret_post",
|
||||
SettingKeyOIDCConnectUsePKCE: "true",
|
||||
SettingKeyOIDCConnectValidateIDToken: "true",
|
||||
SettingKeyOIDCConnectAllowedSigningAlgs: "RS256,ES256,PS256",
|
||||
SettingKeyOIDCConnectClockSkewSeconds: "120",
|
||||
SettingKeyOIDCConnectRequireEmailVerified: "false",
|
||||
SettingKeyOIDCConnectUserInfoEmailPath: "",
|
||||
SettingKeyOIDCConnectUserInfoIDPath: "",
|
||||
SettingKeyOIDCConnectUserInfoUsernamePath: "",
|
||||
SettingKeyDefaultConcurrency: strconv.Itoa(s.cfg.Default.UserConcurrency),
|
||||
SettingKeyDefaultBalance: strconv.FormatFloat(s.cfg.Default.UserBalance, 'f', 8, 64),
|
||||
SettingKeyDefaultSubscriptions: "[]",
|
||||
@ -1737,37 +1806,30 @@ func (s *SettingService) parseSettings(settings map[string]string) *SystemSettin
|
||||
}
|
||||
result.OIDCConnectClientSecretConfigured = result.OIDCConnectClientSecret != ""
|
||||
|
||||
// WeChat Connect 设置:完全以 DB 系统设置为准。
|
||||
result.WeChatConnectEnabled = settings[SettingKeyWeChatConnectEnabled] == "true"
|
||||
result.WeChatConnectAppID = strings.TrimSpace(settings[SettingKeyWeChatConnectAppID])
|
||||
result.WeChatConnectAppSecret = strings.TrimSpace(settings[SettingKeyWeChatConnectAppSecret])
|
||||
result.WeChatConnectAppSecretConfigured = result.WeChatConnectAppSecret != ""
|
||||
result.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],
|
||||
)
|
||||
result.WeChatConnectMode = normalizeWeChatConnectStoredMode(
|
||||
result.WeChatConnectOpenEnabled,
|
||||
result.WeChatConnectMPEnabled,
|
||||
result.WeChatConnectMobileEnabled,
|
||||
settings[SettingKeyWeChatConnectMode],
|
||||
)
|
||||
result.WeChatConnectScopes = normalizeWeChatConnectScopeSetting(settings[SettingKeyWeChatConnectScopes], result.WeChatConnectMode)
|
||||
result.WeChatConnectRedirectURL = strings.TrimSpace(settings[SettingKeyWeChatConnectRedirectURL])
|
||||
result.WeChatConnectFrontendRedirectURL = strings.TrimSpace(settings[SettingKeyWeChatConnectFrontendRedirectURL])
|
||||
if result.WeChatConnectFrontendRedirectURL == "" {
|
||||
result.WeChatConnectFrontendRedirectURL = defaultWeChatConnectFrontend
|
||||
}
|
||||
// WeChat Connect 设置:
|
||||
// - 优先读取 DB 系统设置
|
||||
// - 缺失时回退到 config/env,保持升级兼容
|
||||
weChatEffective := s.effectiveWeChatConnectOAuthConfig(settings)
|
||||
result.WeChatConnectEnabled = weChatEffective.Enabled
|
||||
result.WeChatConnectAppID = weChatEffective.LegacyAppID
|
||||
result.WeChatConnectAppSecret = weChatEffective.LegacyAppSecret
|
||||
result.WeChatConnectAppSecretConfigured = weChatEffective.LegacyAppSecret != ""
|
||||
result.WeChatConnectOpenAppID = weChatEffective.OpenAppID
|
||||
result.WeChatConnectOpenAppSecret = weChatEffective.OpenAppSecret
|
||||
result.WeChatConnectOpenAppSecretConfigured = weChatEffective.OpenAppSecret != ""
|
||||
result.WeChatConnectMPAppID = weChatEffective.MPAppID
|
||||
result.WeChatConnectMPAppSecret = weChatEffective.MPAppSecret
|
||||
result.WeChatConnectMPAppSecretConfigured = weChatEffective.MPAppSecret != ""
|
||||
result.WeChatConnectMobileAppID = weChatEffective.MobileAppID
|
||||
result.WeChatConnectMobileAppSecret = weChatEffective.MobileAppSecret
|
||||
result.WeChatConnectMobileAppSecretConfigured = weChatEffective.MobileAppSecret != ""
|
||||
result.WeChatConnectOpenEnabled = weChatEffective.OpenEnabled
|
||||
result.WeChatConnectMPEnabled = weChatEffective.MPEnabled
|
||||
result.WeChatConnectMobileEnabled = weChatEffective.MobileEnabled
|
||||
result.WeChatConnectMode = weChatEffective.Mode
|
||||
result.WeChatConnectScopes = weChatEffective.Scopes
|
||||
result.WeChatConnectRedirectURL = weChatEffective.RedirectURL
|
||||
result.WeChatConnectFrontendRedirectURL = weChatEffective.FrontendRedirectURL
|
||||
|
||||
// Model fallback settings
|
||||
result.EnableModelFallback = settings[SettingKeyEnableModelFallback] == "true"
|
||||
|
||||
@ -115,6 +115,22 @@ func TestSettingService_ParseSettings_PreservesOptionalOIDCCompatibilityFlags(t
|
||||
require.False(t, got.OIDCConnectValidateIDToken)
|
||||
}
|
||||
|
||||
func TestSettingService_ParseSettings_DefaultsOIDCSecurityFlagsToSafeConfigValues(t *testing.T) {
|
||||
svc := NewSettingService(&settingOIDCRepoStub{values: map[string]string{}}, &config.Config{
|
||||
OIDC: config.OIDCConnectConfig{
|
||||
UsePKCE: true,
|
||||
ValidateIDToken: true,
|
||||
},
|
||||
})
|
||||
|
||||
got := svc.parseSettings(map[string]string{
|
||||
SettingKeyOIDCConnectEnabled: "true",
|
||||
})
|
||||
|
||||
require.True(t, got.OIDCConnectUsePKCE)
|
||||
require.True(t, got.OIDCConnectValidateIDToken)
|
||||
}
|
||||
|
||||
func TestGetOIDCConnectOAuthConfig_AllowsCompatibilityFlagsToDisablePKCEAndIDTokenValidation(t *testing.T) {
|
||||
cfg := &config.Config{
|
||||
OIDC: config.OIDCConnectConfig{
|
||||
@ -145,3 +161,37 @@ func TestGetOIDCConnectOAuthConfig_AllowsCompatibilityFlagsToDisablePKCEAndIDTok
|
||||
require.False(t, got.UsePKCE)
|
||||
require.False(t, got.ValidateIDToken)
|
||||
}
|
||||
|
||||
func TestGetOIDCConnectOAuthConfig_DefaultsToSecureFlagsWhenSettingsMissing(t *testing.T) {
|
||||
cfg := &config.Config{
|
||||
OIDC: config.OIDCConnectConfig{
|
||||
Enabled: true,
|
||||
ProviderName: "OIDC",
|
||||
ClientID: "oidc-client",
|
||||
ClientSecret: "oidc-secret",
|
||||
IssuerURL: "https://issuer.example.com",
|
||||
AuthorizeURL: "https://issuer.example.com/auth",
|
||||
TokenURL: "https://issuer.example.com/token",
|
||||
UserInfoURL: "https://issuer.example.com/userinfo",
|
||||
JWKSURL: "https://issuer.example.com/jwks",
|
||||
RedirectURL: "https://example.com/api/v1/auth/oauth/oidc/callback",
|
||||
FrontendRedirectURL: "/auth/oidc/callback",
|
||||
Scopes: "openid email profile",
|
||||
TokenAuthMethod: "client_secret_post",
|
||||
UsePKCE: true,
|
||||
ValidateIDToken: true,
|
||||
AllowedSigningAlgs: "RS256",
|
||||
ClockSkewSeconds: 120,
|
||||
},
|
||||
}
|
||||
|
||||
repo := &settingOIDCRepoStub{values: map[string]string{
|
||||
SettingKeyOIDCConnectEnabled: "true",
|
||||
}}
|
||||
svc := NewSettingService(repo, cfg)
|
||||
|
||||
got, err := svc.GetOIDCConnectOAuthConfig(context.Background())
|
||||
require.NoError(t, err)
|
||||
require.True(t, got.UsePKCE)
|
||||
require.True(t, got.ValidateIDToken)
|
||||
}
|
||||
|
||||
@ -132,3 +132,22 @@ func TestSettingService_GetPublicSettings_DoesNotExposeMobileOnlyWeChatAsWebOAut
|
||||
require.False(t, settings.WeChatOAuthMPEnabled)
|
||||
require.True(t, settings.WeChatOAuthMobileEnabled)
|
||||
}
|
||||
|
||||
func TestSettingService_GetPublicSettings_FallsBackToConfigForWeChatOAuthCapabilities(t *testing.T) {
|
||||
svc := NewSettingService(&settingPublicRepoStub{values: map[string]string{}}, &config.Config{
|
||||
WeChat: config.WeChatConnectConfig{
|
||||
Enabled: true,
|
||||
OpenEnabled: true,
|
||||
OpenAppID: "wx-open-config",
|
||||
OpenAppSecret: "wx-open-secret",
|
||||
FrontendRedirectURL: "/auth/wechat/config-callback",
|
||||
},
|
||||
})
|
||||
|
||||
settings, err := svc.GetPublicSettings(context.Background())
|
||||
require.NoError(t, err)
|
||||
require.True(t, settings.WeChatOAuthEnabled)
|
||||
require.True(t, settings.WeChatOAuthOpenEnabled)
|
||||
require.False(t, settings.WeChatOAuthMPEnabled)
|
||||
require.False(t, settings.WeChatOAuthMobileEnabled)
|
||||
}
|
||||
|
||||
@ -79,3 +79,54 @@ func TestSettingService_GetWeChatConnectOAuthConfig_UsesDatabaseOverrides(t *tes
|
||||
require.Equal(t, "https://api.example.com/api/v1/auth/oauth/wechat/callback", got.RedirectURL)
|
||||
require.Equal(t, "/auth/wechat/callback", got.FrontendRedirectURL)
|
||||
}
|
||||
|
||||
func TestSettingService_GetWeChatConnectOAuthConfig_FallsBackToConfigWhenDatabaseEmpty(t *testing.T) {
|
||||
repo := &settingWeChatRepoStub{values: map[string]string{}}
|
||||
svc := NewSettingService(repo, &config.Config{
|
||||
WeChat: config.WeChatConnectConfig{
|
||||
Enabled: true,
|
||||
OpenEnabled: true,
|
||||
MPEnabled: true,
|
||||
Mode: "open",
|
||||
OpenAppID: "wx-open-config",
|
||||
OpenAppSecret: "wx-open-secret",
|
||||
MPAppID: "wx-mp-config",
|
||||
MPAppSecret: "wx-mp-secret",
|
||||
FrontendRedirectURL: "/auth/wechat/config-callback",
|
||||
},
|
||||
})
|
||||
|
||||
got, err := svc.GetWeChatConnectOAuthConfig(context.Background())
|
||||
require.NoError(t, err)
|
||||
require.True(t, got.Enabled)
|
||||
require.True(t, got.OpenEnabled)
|
||||
require.True(t, got.MPEnabled)
|
||||
require.Equal(t, "wx-open-config", got.AppIDForMode("open"))
|
||||
require.Equal(t, "wx-open-secret", got.AppSecretForMode("open"))
|
||||
require.Equal(t, "wx-mp-config", got.AppIDForMode("mp"))
|
||||
require.Equal(t, "wx-mp-secret", got.AppSecretForMode("mp"))
|
||||
require.Equal(t, "/auth/wechat/config-callback", got.FrontendRedirectURL)
|
||||
require.Empty(t, got.RedirectURL)
|
||||
}
|
||||
|
||||
func TestSettingService_ParseSettings_FallsBackToConfigForWeChatAdminView(t *testing.T) {
|
||||
svc := NewSettingService(&settingWeChatRepoStub{values: map[string]string{}}, &config.Config{
|
||||
WeChat: config.WeChatConnectConfig{
|
||||
Enabled: true,
|
||||
OpenEnabled: true,
|
||||
Mode: "open",
|
||||
OpenAppID: "wx-open-config",
|
||||
OpenAppSecret: "wx-open-secret",
|
||||
FrontendRedirectURL: "/auth/wechat/config-callback",
|
||||
},
|
||||
})
|
||||
|
||||
got := svc.parseSettings(map[string]string{})
|
||||
require.True(t, got.WeChatConnectEnabled)
|
||||
require.True(t, got.WeChatConnectOpenEnabled)
|
||||
require.Equal(t, "wx-open-config", got.WeChatConnectOpenAppID)
|
||||
require.True(t, got.WeChatConnectOpenAppSecretConfigured)
|
||||
require.Equal(t, "/auth/wechat/config-callback", got.WeChatConnectFrontendRedirectURL)
|
||||
require.Equal(t, "open", got.WeChatConnectMode)
|
||||
require.Equal(t, "snsapi_login", got.WeChatConnectScopes)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user