"backend/cmd/vscode:/vscode.git/clone" did not exist on "9ba42aa5566653e2eb57b1761a38f1e532e55bfb"
Unverified Commit ddf80f5e authored by Wesley Liddick's avatar Wesley Liddick Committed by GitHub
Browse files

Merge pull request #1799 from IanShaw027/rebuild/auth-identity-foundation

fix(auth,payment,profile): 修复认证身份和支付系统的后续问题
parents 4d0483f5 c048ca80
...@@ -245,15 +245,119 @@ func parseWeChatConnectCapabilitySettings(settings map[string]string, enabled bo ...@@ -245,15 +245,119 @@ func parseWeChatConnectCapabilitySettings(settings map[string]string, enabled bo
} }
func normalizeWeChatConnectStoredMode(openEnabled, mpEnabled, mobileEnabled bool, mode string) string { 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 { switch {
case openEnabled:
return "open"
case mpEnabled: case mpEnabled:
return "mp" return "mp"
case mobileEnabled: case mobileEnabled:
return "mobile" return "mobile"
case openEnabled:
return "open"
default: 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 {
openEnabled := strings.TrimSpace(rawOpen) == "true"
mpEnabled := strings.TrimSpace(rawMP) == "true"
mobileEnabled := strings.TrimSpace(rawMobile) == "true"
_, enabledConfigured := settings[SettingKeyWeChatConnectEnabled]
if !enabledConfigured &&
enabled &&
!openEnabled &&
!mpEnabled &&
!mobileEnabled &&
(base.OpenEnabled || base.MPEnabled || base.MobileEnabled) {
return base.OpenEnabled, base.MPEnabled, base.MobileEnabled
}
return openEnabled, mpEnabled, mobileEnabled
}
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 +639,7 @@ func DefaultWeChatConnectScopesForMode(mode string) string { ...@@ -535,32 +639,7 @@ func DefaultWeChatConnectScopesForMode(mode string) string {
} }
func (s *SettingService) parseWeChatConnectOAuthConfig(settings map[string]string) (WeChatConnectOAuthConfig, error) { func (s *SettingService) parseWeChatConnectOAuthConfig(settings map[string]string) (WeChatConnectOAuthConfig, error) {
enabled := settings[SettingKeyWeChatConnectEnabled] == "true" cfg := s.effectiveWeChatConnectOAuthConfig(settings)
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
}
if !cfg.Enabled || (!cfg.OpenEnabled && !cfg.MPEnabled) { if !cfg.Enabled || (!cfg.OpenEnabled && !cfg.MPEnabled) {
return WeChatConnectOAuthConfig{}, infraerrors.NotFound("OAUTH_DISABLED", "wechat oauth is disabled") return WeChatConnectOAuthConfig{}, infraerrors.NotFound("OAUTH_DISABLED", "wechat oauth is disabled")
...@@ -589,14 +668,10 @@ func (s *SettingService) parseWeChatConnectOAuthConfig(settings map[string]strin ...@@ -589,14 +668,10 @@ func (s *SettingService) parseWeChatConnectOAuthConfig(settings map[string]strin
return WeChatConnectOAuthConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "wechat oauth mobile app secret not configured") return WeChatConnectOAuthConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "wechat oauth mobile app secret not configured")
} }
} }
if cfg.RedirectURL == "" { if v := strings.TrimSpace(cfg.RedirectURL); v != "" {
return WeChatConnectOAuthConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "wechat oauth redirect url not configured") if err := config.ValidateAbsoluteHTTPURL(v); err != nil {
} return WeChatConnectOAuthConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "wechat oauth redirect url invalid")
if cfg.FrontendRedirectURL == "" { }
return WeChatConnectOAuthConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "wechat oauth frontend redirect url not configured")
}
if err := config.ValidateAbsoluteHTTPURL(cfg.RedirectURL); err != nil {
return WeChatConnectOAuthConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "wechat oauth redirect url invalid")
} }
if err := config.ValidateFrontendRedirectURL(cfg.FrontendRedirectURL); err != nil { if err := config.ValidateFrontendRedirectURL(cfg.FrontendRedirectURL); err != nil {
return WeChatConnectOAuthConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "wechat oauth frontend redirect url invalid") return WeChatConnectOAuthConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "wechat oauth frontend redirect url invalid")
...@@ -605,33 +680,16 @@ func (s *SettingService) parseWeChatConnectOAuthConfig(settings map[string]strin ...@@ -605,33 +680,16 @@ func (s *SettingService) parseWeChatConnectOAuthConfig(settings map[string]strin
} }
func (s *SettingService) weChatOAuthCapabilitiesFromSettings(settings map[string]string) (bool, bool, bool, bool) { 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 return false, false, false, false
} }
mode := normalizeWeChatConnectModeSetting(settings[SettingKeyWeChatConnectMode]) openReady := cfg.OpenEnabled && cfg.AppIDForMode("open") != "" && cfg.AppSecretForMode("open") != ""
openEnabled, mpEnabled, mobileEnabled := parseWeChatConnectCapabilitySettings(settings, true, mode) mpReady := cfg.MPEnabled && cfg.AppIDForMode("mp") != "" && cfg.AppSecretForMode("mp") != ""
redirectURL := strings.TrimSpace(settings[SettingKeyWeChatConnectRedirectURL]) mobileReady := cfg.MobileEnabled && cfg.AppIDForMode("mobile") != "" && cfg.AppSecretForMode("mobile") != ""
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 return openReady || mpReady, openReady, mpReady, mobileReady
} }
// filterUserVisibleMenuItems filters out admin-only menu items from a raw JSON // filterUserVisibleMenuItems filters out admin-only menu items from a raw JSON
...@@ -756,6 +814,30 @@ func parseCustomMenuItemURLs(raw string) []string { ...@@ -756,6 +814,30 @@ func parseCustomMenuItemURLs(raw string) []string {
return urls return urls
} }
func oidcUsePKCECompatibilityDefault(base config.OIDCConnectConfig) bool {
if base.UsePKCEExplicit {
return base.UsePKCE
}
return true
}
func oidcValidateIDTokenCompatibilityDefault(base config.OIDCConnectConfig) bool {
if base.ValidateIDTokenExplicit {
return base.ValidateIDToken
}
return true
}
func oidcCompatibilityWriteDefault(base config.OIDCConnectConfig, configured bool, raw string, explicit bool, explicitValue bool) bool {
if configured {
return strings.TrimSpace(raw) == "true"
}
if explicit {
return explicitValue
}
return false
}
// UpdateSettings 更新系统设置 // UpdateSettings 更新系统设置
func (s *SettingService) UpdateSettings(ctx context.Context, settings *SystemSettings) error { func (s *SettingService) UpdateSettings(ctx context.Context, settings *SystemSettings) error {
updates, err := s.buildSystemSettingsUpdates(ctx, settings) updates, err := s.buildSystemSettingsUpdates(ctx, settings)
...@@ -770,6 +852,28 @@ func (s *SettingService) UpdateSettings(ctx context.Context, settings *SystemSet ...@@ -770,6 +852,28 @@ func (s *SettingService) UpdateSettings(ctx context.Context, settings *SystemSet
return err return err
} }
func (s *SettingService) OIDCSecurityWriteDefaults(ctx context.Context) (bool, bool, error) {
rawSettings, err := s.settingRepo.GetMultiple(ctx, []string{
SettingKeyOIDCConnectUsePKCE,
SettingKeyOIDCConnectValidateIDToken,
})
if err != nil {
return false, false, fmt.Errorf("get oidc security write defaults: %w", err)
}
base := config.OIDCConnectConfig{}
if s != nil && s.cfg != nil {
base = s.cfg.OIDC
}
rawUsePKCE, hasUsePKCE := rawSettings[SettingKeyOIDCConnectUsePKCE]
rawValidateIDToken, hasValidateIDToken := rawSettings[SettingKeyOIDCConnectValidateIDToken]
return oidcCompatibilityWriteDefault(base, hasUsePKCE, rawUsePKCE, base.UsePKCEExplicit, base.UsePKCE),
oidcCompatibilityWriteDefault(base, hasValidateIDToken, rawValidateIDToken, base.ValidateIDTokenExplicit, base.ValidateIDToken),
nil
}
// UpdateSettingsWithAuthSourceDefaults persists system settings and auth-source defaults in a single write. // UpdateSettingsWithAuthSourceDefaults persists system settings and auth-source defaults in a single write.
func (s *SettingService) UpdateSettingsWithAuthSourceDefaults(ctx context.Context, settings *SystemSettings, authDefaults *AuthSourceDefaultSettings) error { func (s *SettingService) UpdateSettingsWithAuthSourceDefaults(ctx context.Context, settings *SystemSettings, authDefaults *AuthSourceDefaultSettings) error {
updates, err := s.buildSystemSettingsUpdates(ctx, settings) updates, err := s.buildSystemSettingsUpdates(ctx, settings)
...@@ -1421,6 +1525,17 @@ func (s *SettingService) InitializeDefaultSettings(ctx context.Context) error { ...@@ -1421,6 +1525,17 @@ func (s *SettingService) InitializeDefaultSettings(ctx context.Context) error {
return fmt.Errorf("check existing settings: %w", err) return fmt.Errorf("check existing settings: %w", err)
} }
oidcUsePKCEDefault := true
oidcValidateIDTokenDefault := true
if s != nil && s.cfg != nil {
if s.cfg.OIDC.UsePKCEExplicit {
oidcUsePKCEDefault = s.cfg.OIDC.UsePKCE
}
if s.cfg.OIDC.ValidateIDTokenExplicit {
oidcValidateIDTokenDefault = s.cfg.OIDC.ValidateIDToken
}
}
// 初始化默认设置 // 初始化默认设置
defaults := map[string]string{ defaults := map[string]string{
SettingKeyRegistrationEnabled: "true", SettingKeyRegistrationEnabled: "true",
...@@ -1436,6 +1551,8 @@ func (s *SettingService) InitializeDefaultSettings(ctx context.Context) error { ...@@ -1436,6 +1551,8 @@ func (s *SettingService) InitializeDefaultSettings(ctx context.Context) error {
SettingKeyCustomMenuItems: "[]", SettingKeyCustomMenuItems: "[]",
SettingKeyCustomEndpoints: "[]", SettingKeyCustomEndpoints: "[]",
SettingKeyWeChatConnectEnabled: "false", SettingKeyWeChatConnectEnabled: "false",
SettingKeyWeChatConnectAppID: "",
SettingKeyWeChatConnectAppSecret: "",
SettingKeyWeChatConnectOpenAppID: "", SettingKeyWeChatConnectOpenAppID: "",
SettingKeyWeChatConnectOpenAppSecret: "", SettingKeyWeChatConnectOpenAppSecret: "",
SettingKeyWeChatConnectMPAppID: "", SettingKeyWeChatConnectMPAppID: "",
...@@ -1447,9 +1564,30 @@ func (s *SettingService) InitializeDefaultSettings(ctx context.Context) error { ...@@ -1447,9 +1564,30 @@ func (s *SettingService) InitializeDefaultSettings(ctx context.Context) error {
SettingKeyWeChatConnectMobileEnabled: "false", SettingKeyWeChatConnectMobileEnabled: "false",
SettingKeyWeChatConnectMode: "open", SettingKeyWeChatConnectMode: "open",
SettingKeyWeChatConnectScopes: "snsapi_login", SettingKeyWeChatConnectScopes: "snsapi_login",
SettingKeyWeChatConnectRedirectURL: "",
SettingKeyWeChatConnectFrontendRedirectURL: defaultWeChatConnectFrontend, SettingKeyWeChatConnectFrontendRedirectURL: defaultWeChatConnectFrontend,
SettingKeyOIDCConnectEnabled: "false", SettingKeyOIDCConnectEnabled: "false",
SettingKeyOIDCConnectProviderName: "OIDC", SettingKeyOIDCConnectProviderName: "OIDC",
SettingKeyOIDCConnectClientID: "",
SettingKeyOIDCConnectClientSecret: "",
SettingKeyOIDCConnectIssuerURL: "",
SettingKeyOIDCConnectDiscoveryURL: "",
SettingKeyOIDCConnectAuthorizeURL: "",
SettingKeyOIDCConnectTokenURL: "",
SettingKeyOIDCConnectUserInfoURL: "",
SettingKeyOIDCConnectJWKSURL: "",
SettingKeyOIDCConnectScopes: "openid email profile",
SettingKeyOIDCConnectRedirectURL: "",
SettingKeyOIDCConnectFrontendRedirectURL: "/auth/oidc/callback",
SettingKeyOIDCConnectTokenAuthMethod: "client_secret_post",
SettingKeyOIDCConnectUsePKCE: strconv.FormatBool(oidcUsePKCEDefault),
SettingKeyOIDCConnectValidateIDToken: strconv.FormatBool(oidcValidateIDTokenDefault),
SettingKeyOIDCConnectAllowedSigningAlgs: "RS256,ES256,PS256",
SettingKeyOIDCConnectClockSkewSeconds: "120",
SettingKeyOIDCConnectRequireEmailVerified: "false",
SettingKeyOIDCConnectUserInfoEmailPath: "",
SettingKeyOIDCConnectUserInfoIDPath: "",
SettingKeyOIDCConnectUserInfoUsernamePath: "",
SettingKeyDefaultConcurrency: strconv.Itoa(s.cfg.Default.UserConcurrency), SettingKeyDefaultConcurrency: strconv.Itoa(s.cfg.Default.UserConcurrency),
SettingKeyDefaultBalance: strconv.FormatFloat(s.cfg.Default.UserBalance, 'f', 8, 64), SettingKeyDefaultBalance: strconv.FormatFloat(s.cfg.Default.UserBalance, 'f', 8, 64),
SettingKeyDefaultSubscriptions: "[]", SettingKeyDefaultSubscriptions: "[]",
...@@ -1686,15 +1824,13 @@ func (s *SettingService) parseSettings(settings map[string]string) *SystemSettin ...@@ -1686,15 +1824,13 @@ func (s *SettingService) parseSettings(settings map[string]string) *SystemSettin
if raw, ok := settings[SettingKeyOIDCConnectUsePKCE]; ok { if raw, ok := settings[SettingKeyOIDCConnectUsePKCE]; ok {
result.OIDCConnectUsePKCE = raw == "true" result.OIDCConnectUsePKCE = raw == "true"
} else { } else {
result.OIDCConnectUsePKCE = oidcBase.UsePKCE result.OIDCConnectUsePKCE = oidcUsePKCECompatibilityDefault(oidcBase)
} }
if raw, ok := settings[SettingKeyOIDCConnectValidateIDToken]; ok { if raw, ok := settings[SettingKeyOIDCConnectValidateIDToken]; ok {
result.OIDCConnectValidateIDToken = raw == "true" result.OIDCConnectValidateIDToken = raw == "true"
} else { } else {
result.OIDCConnectValidateIDToken = oidcBase.ValidateIDToken result.OIDCConnectValidateIDToken = oidcValidateIDTokenCompatibilityDefault(oidcBase)
} }
result.OIDCConnectUsePKCE = true
result.OIDCConnectValidateIDToken = true
if v, ok := settings[SettingKeyOIDCConnectAllowedSigningAlgs]; ok && strings.TrimSpace(v) != "" { if v, ok := settings[SettingKeyOIDCConnectAllowedSigningAlgs]; ok && strings.TrimSpace(v) != "" {
result.OIDCConnectAllowedSigningAlgs = strings.TrimSpace(v) result.OIDCConnectAllowedSigningAlgs = strings.TrimSpace(v)
} else { } else {
...@@ -1739,37 +1875,30 @@ func (s *SettingService) parseSettings(settings map[string]string) *SystemSettin ...@@ -1739,37 +1875,30 @@ func (s *SettingService) parseSettings(settings map[string]string) *SystemSettin
} }
result.OIDCConnectClientSecretConfigured = result.OIDCConnectClientSecret != "" result.OIDCConnectClientSecretConfigured = result.OIDCConnectClientSecret != ""
// WeChat Connect 设置:完全以 DB 系统设置为准。 // WeChat Connect 设置:
result.WeChatConnectEnabled = settings[SettingKeyWeChatConnectEnabled] == "true" // - 优先读取 DB 系统设置
result.WeChatConnectAppID = strings.TrimSpace(settings[SettingKeyWeChatConnectAppID]) // - 缺失时回退到 config/env,保持升级兼容
result.WeChatConnectAppSecret = strings.TrimSpace(settings[SettingKeyWeChatConnectAppSecret]) weChatEffective := s.effectiveWeChatConnectOAuthConfig(settings)
result.WeChatConnectAppSecretConfigured = result.WeChatConnectAppSecret != "" result.WeChatConnectEnabled = weChatEffective.Enabled
result.WeChatConnectOpenAppID = strings.TrimSpace(firstNonEmpty(settings[SettingKeyWeChatConnectOpenAppID], result.WeChatConnectAppID)) result.WeChatConnectAppID = weChatEffective.LegacyAppID
result.WeChatConnectOpenAppSecret = strings.TrimSpace(firstNonEmpty(settings[SettingKeyWeChatConnectOpenAppSecret], result.WeChatConnectAppSecret)) result.WeChatConnectAppSecret = weChatEffective.LegacyAppSecret
result.WeChatConnectOpenAppSecretConfigured = result.WeChatConnectOpenAppSecret != "" result.WeChatConnectAppSecretConfigured = weChatEffective.LegacyAppSecret != ""
result.WeChatConnectMPAppID = strings.TrimSpace(firstNonEmpty(settings[SettingKeyWeChatConnectMPAppID], result.WeChatConnectAppID)) result.WeChatConnectOpenAppID = weChatEffective.OpenAppID
result.WeChatConnectMPAppSecret = strings.TrimSpace(firstNonEmpty(settings[SettingKeyWeChatConnectMPAppSecret], result.WeChatConnectAppSecret)) result.WeChatConnectOpenAppSecret = weChatEffective.OpenAppSecret
result.WeChatConnectMPAppSecretConfigured = result.WeChatConnectMPAppSecret != "" result.WeChatConnectOpenAppSecretConfigured = weChatEffective.OpenAppSecret != ""
result.WeChatConnectMobileAppID = strings.TrimSpace(firstNonEmpty(settings[SettingKeyWeChatConnectMobileAppID], result.WeChatConnectAppID)) result.WeChatConnectMPAppID = weChatEffective.MPAppID
result.WeChatConnectMobileAppSecret = strings.TrimSpace(firstNonEmpty(settings[SettingKeyWeChatConnectMobileAppSecret], result.WeChatConnectAppSecret)) result.WeChatConnectMPAppSecret = weChatEffective.MPAppSecret
result.WeChatConnectMobileAppSecretConfigured = result.WeChatConnectMobileAppSecret != "" result.WeChatConnectMPAppSecretConfigured = weChatEffective.MPAppSecret != ""
result.WeChatConnectOpenEnabled, result.WeChatConnectMPEnabled, result.WeChatConnectMobileEnabled = parseWeChatConnectCapabilitySettings( result.WeChatConnectMobileAppID = weChatEffective.MobileAppID
settings, result.WeChatConnectMobileAppSecret = weChatEffective.MobileAppSecret
result.WeChatConnectEnabled, result.WeChatConnectMobileAppSecretConfigured = weChatEffective.MobileAppSecret != ""
settings[SettingKeyWeChatConnectMode], result.WeChatConnectOpenEnabled = weChatEffective.OpenEnabled
) result.WeChatConnectMPEnabled = weChatEffective.MPEnabled
result.WeChatConnectMode = normalizeWeChatConnectStoredMode( result.WeChatConnectMobileEnabled = weChatEffective.MobileEnabled
result.WeChatConnectOpenEnabled, result.WeChatConnectMode = weChatEffective.Mode
result.WeChatConnectMPEnabled, result.WeChatConnectScopes = weChatEffective.Scopes
result.WeChatConnectMobileEnabled, result.WeChatConnectRedirectURL = weChatEffective.RedirectURL
settings[SettingKeyWeChatConnectMode], result.WeChatConnectFrontendRedirectURL = weChatEffective.FrontendRedirectURL
)
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
}
// Model fallback settings // Model fallback settings
result.EnableModelFallback = settings[SettingKeyEnableModelFallback] == "true" result.EnableModelFallback = settings[SettingKeyEnableModelFallback] == "true"
...@@ -1861,14 +1990,9 @@ func isFalseSettingValue(value string) bool { ...@@ -1861,14 +1990,9 @@ func isFalseSettingValue(value string) bool {
} }
func normalizeVisibleMethodSettingSource(method, source string, enabled bool) (string, error) { func normalizeVisibleMethodSettingSource(method, source string, enabled bool) (string, error) {
_ = enabled
source = strings.TrimSpace(source) source = strings.TrimSpace(source)
if source == "" { if source == "" {
if enabled {
return "", infraerrors.BadRequest(
"INVALID_PAYMENT_VISIBLE_METHOD_SOURCE",
fmt.Sprintf("%s source is required when the visible method is enabled", method),
)
}
return "", nil return "", nil
} }
...@@ -2196,8 +2320,6 @@ func (s *SettingService) GetLinuxDoConnectOAuthConfig(ctx context.Context) (conf ...@@ -2196,8 +2320,6 @@ func (s *SettingService) GetLinuxDoConnectOAuthConfig(ctx context.Context) (conf
if v, ok := settings[SettingKeyLinuxDoConnectRedirectURL]; ok && strings.TrimSpace(v) != "" { if v, ok := settings[SettingKeyLinuxDoConnectRedirectURL]; ok && strings.TrimSpace(v) != "" {
effective.RedirectURL = strings.TrimSpace(v) effective.RedirectURL = strings.TrimSpace(v)
} }
effective.UsePKCE = true
if !effective.Enabled { if !effective.Enabled {
return config.LinuxDoConnectConfig{}, infraerrors.NotFound("OAUTH_DISABLED", "oauth login is disabled") return config.LinuxDoConnectConfig{}, infraerrors.NotFound("OAUTH_DISABLED", "oauth login is disabled")
} }
...@@ -2417,12 +2539,14 @@ func (s *SettingService) GetOIDCConnectOAuthConfig(ctx context.Context) (config. ...@@ -2417,12 +2539,14 @@ func (s *SettingService) GetOIDCConnectOAuthConfig(ctx context.Context) (config.
} }
if raw, ok := settings[SettingKeyOIDCConnectUsePKCE]; ok { if raw, ok := settings[SettingKeyOIDCConnectUsePKCE]; ok {
effective.UsePKCE = raw == "true" effective.UsePKCE = raw == "true"
} else {
effective.UsePKCE = oidcUsePKCECompatibilityDefault(effective)
} }
if raw, ok := settings[SettingKeyOIDCConnectValidateIDToken]; ok { if raw, ok := settings[SettingKeyOIDCConnectValidateIDToken]; ok {
effective.ValidateIDToken = raw == "true" effective.ValidateIDToken = raw == "true"
} else {
effective.ValidateIDToken = oidcValidateIDTokenCompatibilityDefault(effective)
} }
effective.UsePKCE = true
effective.ValidateIDToken = true
if v, ok := settings[SettingKeyOIDCConnectAllowedSigningAlgs]; ok && strings.TrimSpace(v) != "" { if v, ok := settings[SettingKeyOIDCConnectAllowedSigningAlgs]; ok && strings.TrimSpace(v) != "" {
effective.AllowedSigningAlgs = strings.TrimSpace(v) effective.AllowedSigningAlgs = strings.TrimSpace(v)
} }
......
...@@ -101,3 +101,151 @@ func TestGetOIDCConnectOAuthConfig_ResolvesEndpointsFromIssuerDiscovery(t *testi ...@@ -101,3 +101,151 @@ func TestGetOIDCConnectOAuthConfig_ResolvesEndpointsFromIssuerDiscovery(t *testi
require.Equal(t, srv.URL+"/issuer/protocol/openid-connect/userinfo", got.UserInfoURL) require.Equal(t, srv.URL+"/issuer/protocol/openid-connect/userinfo", got.UserInfoURL)
require.Equal(t, srv.URL+"/issuer/protocol/openid-connect/certs", got.JWKSURL) require.Equal(t, srv.URL+"/issuer/protocol/openid-connect/certs", got.JWKSURL)
} }
func TestSettingService_ParseSettings_PreservesOptionalOIDCCompatibilityFlags(t *testing.T) {
svc := NewSettingService(&settingOIDCRepoStub{values: map[string]string{}}, &config.Config{})
got := svc.parseSettings(map[string]string{
SettingKeyOIDCConnectEnabled: "true",
SettingKeyOIDCConnectUsePKCE: "false",
SettingKeyOIDCConnectValidateIDToken: "false",
})
require.False(t, got.OIDCConnectUsePKCE)
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,
UsePKCEExplicit: true,
ValidateIDToken: true,
ValidateIDTokenExplicit: true,
},
})
got := svc.parseSettings(map[string]string{
SettingKeyOIDCConnectEnabled: "true",
})
require.True(t, got.OIDCConnectUsePKCE)
require.True(t, got.OIDCConnectValidateIDToken)
}
func TestSettingService_ParseSettings_DefaultsOIDCCompatibilityFlagsToSafeDefaultsWhenSettingsMissing(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{
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",
RedirectURL: "https://example.com/api/v1/auth/oauth/oidc/callback",
FrontendRedirectURL: "/auth/oidc/callback",
Scopes: "openid email profile",
TokenAuthMethod: "client_secret_post",
},
}
repo := &settingOIDCRepoStub{values: map[string]string{
SettingKeyOIDCConnectEnabled: "true",
SettingKeyOIDCConnectUsePKCE: "false",
SettingKeyOIDCConnectValidateIDToken: "false",
}}
svc := NewSettingService(repo, cfg)
got, err := svc.GetOIDCConnectOAuthConfig(context.Background())
require.NoError(t, err)
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,
UsePKCEExplicit: true,
ValidateIDToken: true,
ValidateIDTokenExplicit: 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)
}
func TestGetOIDCConnectOAuthConfig_DefaultsCompatibilityFlagsToSafeValuesWhenSettingsMissing(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)
}
...@@ -112,3 +112,42 @@ func TestSettingService_GetPublicSettings_ExposesWeChatOAuthModeCapabilities(t * ...@@ -112,3 +112,42 @@ func TestSettingService_GetPublicSettings_ExposesWeChatOAuthModeCapabilities(t *
require.True(t, settings.WeChatOAuthOpenEnabled) require.True(t, settings.WeChatOAuthOpenEnabled)
require.True(t, settings.WeChatOAuthMPEnabled) require.True(t, settings.WeChatOAuthMPEnabled)
} }
func TestSettingService_GetPublicSettings_DoesNotExposeMobileOnlyWeChatAsWebOAuthAvailable(t *testing.T) {
svc := NewSettingService(&settingPublicRepoStub{
values: map[string]string{
SettingKeyWeChatConnectEnabled: "true",
SettingKeyWeChatConnectMobileEnabled: "true",
SettingKeyWeChatConnectMode: "mobile",
SettingKeyWeChatConnectMobileAppID: "wx-mobile-app",
SettingKeyWeChatConnectMobileAppSecret: "wx-mobile-secret",
SettingKeyWeChatConnectFrontendRedirectURL: "/auth/wechat/callback",
},
}, &config.Config{})
settings, err := svc.GetPublicSettings(context.Background())
require.NoError(t, err)
require.False(t, settings.WeChatOAuthEnabled)
require.False(t, settings.WeChatOAuthOpenEnabled)
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,84 @@ func TestSettingService_GetWeChatConnectOAuthConfig_UsesDatabaseOverrides(t *tes ...@@ -79,3 +79,84 @@ func TestSettingService_GetWeChatConnectOAuthConfig_UsesDatabaseOverrides(t *tes
require.Equal(t, "https://api.example.com/api/v1/auth/oauth/wechat/callback", got.RedirectURL) require.Equal(t, "https://api.example.com/api/v1/auth/oauth/wechat/callback", got.RedirectURL)
require.Equal(t, "/auth/wechat/callback", got.FrontendRedirectURL) 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_GetWeChatConnectOAuthConfig_IgnoresSyntheticDisabledCapabilitiesFromMigration118(t *testing.T) {
repo := &settingWeChatRepoStub{
values: map[string]string{
SettingKeyWeChatConnectOpenEnabled: "false",
SettingKeyWeChatConnectMPEnabled: "false",
},
}
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-mp-config", got.AppIDForMode("mp"))
}
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)
}
...@@ -23,12 +23,15 @@ type User struct { ...@@ -23,12 +23,15 @@ type User struct {
Status string Status string
AllowedGroups []int64 AllowedGroups []int64
TokenVersion int64 // Incremented on password change to invalidate existing tokens TokenVersion int64 // Incremented on password change to invalidate existing tokens
SignupSource string // TokenVersionResolved indicates TokenVersion already contains the fingerprint-derived
LastLoginAt *time.Time // value expected in JWT claims and refresh-token state.
LastActiveAt *time.Time TokenVersionResolved bool
LastUsedAt *time.Time SignupSource string
CreatedAt time.Time LastLoginAt *time.Time
UpdatedAt time.Time LastActiveAt *time.Time
LastUsedAt *time.Time
CreatedAt time.Time
UpdatedAt time.Time
// GroupRates 用户专属分组倍率配置 // GroupRates 用户专属分组倍率配置
// map[groupID]rateMultiplier // map[groupID]rateMultiplier
......
...@@ -127,6 +127,7 @@ type UserIdentitySummary struct { ...@@ -127,6 +127,7 @@ type UserIdentitySummary struct {
Bound bool `json:"bound"` Bound bool `json:"bound"`
BoundCount int `json:"bound_count"` BoundCount int `json:"bound_count"`
DisplayName string `json:"display_name,omitempty"` DisplayName string `json:"display_name,omitempty"`
AvatarURL string `json:"-"`
SubjectHint string `json:"subject_hint,omitempty"` SubjectHint string `json:"subject_hint,omitempty"`
ProviderKey string `json:"provider_key,omitempty"` ProviderKey string `json:"provider_key,omitempty"`
VerifiedAt *time.Time `json:"verified_at,omitempty"` VerifiedAt *time.Time `json:"verified_at,omitempty"`
...@@ -228,6 +229,7 @@ func (s *UserService) GetProfile(ctx context.Context, userID int64) (*User, erro ...@@ -228,6 +229,7 @@ func (s *UserService) GetProfile(ctx context.Context, userID int64) (*User, erro
if err != nil { if err != nil {
return nil, fmt.Errorf("get user: %w", err) return nil, fmt.Errorf("get user: %w", err)
} }
normalizeLoadedUserTokenVersion(user)
if err := s.hydrateUserAvatar(ctx, user); err != nil { if err := s.hydrateUserAvatar(ctx, user); err != nil {
return nil, fmt.Errorf("get user avatar: %w", err) return nil, fmt.Errorf("get user avatar: %w", err)
} }
...@@ -248,12 +250,59 @@ func (s *UserService) GetProfileIdentitySummaries(ctx context.Context, userID in ...@@ -248,12 +250,59 @@ func (s *UserService) GetProfileIdentitySummaries(ctx context.Context, userID in
return UserIdentitySummarySet{}, err return UserIdentitySummarySet{}, err
} }
return UserIdentitySummarySet{ summaries := UserIdentitySummarySet{
Email: s.buildEmailIdentitySummary(user, records), Email: s.buildEmailIdentitySummary(user, records),
LinuxDo: s.buildProviderIdentitySummary("linuxdo", user, records), LinuxDo: s.buildProviderIdentitySummary("linuxdo", user, records),
OIDC: s.buildProviderIdentitySummary("oidc", user, records), OIDC: s.buildProviderIdentitySummary("oidc", user, records),
WeChat: s.buildProviderIdentitySummary("wechat", user, records), WeChat: s.buildProviderIdentitySummary("wechat", user, records),
}, nil }
s.applyExplicitProviderAvailability(ctx, &summaries)
return summaries, nil
}
func (s *UserService) applyExplicitProviderAvailability(ctx context.Context, summaries *UserIdentitySummarySet) {
if s == nil || summaries == nil || s.settingRepo == nil {
return
}
settings, err := s.settingRepo.GetMultiple(ctx, []string{
SettingKeyLinuxDoConnectEnabled,
SettingKeyOIDCConnectEnabled,
SettingKeyWeChatConnectEnabled,
SettingKeyWeChatConnectOpenEnabled,
SettingKeyWeChatConnectMPEnabled,
SettingKeyWeChatConnectMobileEnabled,
SettingKeyWeChatConnectMode,
})
if err != nil {
return
}
if raw, ok := settings[SettingKeyLinuxDoConnectEnabled]; ok && strings.TrimSpace(raw) != "" && raw != "true" {
disableIdentityBindAction(&summaries.LinuxDo)
}
if raw, ok := settings[SettingKeyOIDCConnectEnabled]; ok && strings.TrimSpace(raw) != "" && raw != "true" {
disableIdentityBindAction(&summaries.OIDC)
}
if raw, ok := settings[SettingKeyWeChatConnectEnabled]; ok && strings.TrimSpace(raw) != "" {
if raw != "true" {
disableIdentityBindAction(&summaries.WeChat)
return
}
openEnabled, mpEnabled, _ := parseWeChatConnectCapabilitySettings(settings, true, settings[SettingKeyWeChatConnectMode])
if !openEnabled && !mpEnabled {
disableIdentityBindAction(&summaries.WeChat)
}
}
}
func disableIdentityBindAction(summary *UserIdentitySummary) {
if summary == nil || summary.Bound {
return
}
summary.CanBind = false
summary.BindStartPath = ""
} }
func (s *UserService) PrepareIdentityBindingStart(_ context.Context, req StartUserIdentityBindingRequest) (*StartUserIdentityBindingResult, error) { func (s *UserService) PrepareIdentityBindingStart(_ context.Context, req StartUserIdentityBindingRequest) (*StartUserIdentityBindingResult, error) {
...@@ -276,29 +325,34 @@ func (s *UserService) PrepareIdentityBindingStart(_ context.Context, req StartUs ...@@ -276,29 +325,34 @@ func (s *UserService) PrepareIdentityBindingStart(_ context.Context, req StartUs
} }
func (s *UserService) UnbindUserAuthProvider(ctx context.Context, userID int64, provider string) (*User, error) { func (s *UserService) UnbindUserAuthProvider(ctx context.Context, userID int64, provider string) (*User, error) {
user, _, err := s.UnbindUserAuthProviderWithResult(ctx, userID, provider)
return user, err
}
func (s *UserService) UnbindUserAuthProviderWithResult(ctx context.Context, userID int64, provider string) (*User, bool, error) {
provider = normalizeUserIdentityProvider(provider) provider = normalizeUserIdentityProvider(provider)
if provider == "" || provider == "email" { if provider == "" || provider == "email" {
return nil, ErrIdentityProviderInvalid return nil, false, ErrIdentityProviderInvalid
} }
user, err := s.userRepo.GetByID(ctx, userID) user, err := s.userRepo.GetByID(ctx, userID)
if err != nil { if err != nil {
return nil, fmt.Errorf("get user: %w", err) return nil, false, fmt.Errorf("get user: %w", err)
} }
records, err := s.listUserAuthIdentities(ctx, userID) records, err := s.listUserAuthIdentities(ctx, userID)
if err != nil { if err != nil {
return nil, err return nil, false, err
} }
if len(filterUserAuthIdentities(records, provider)) == 0 { if len(filterUserAuthIdentities(records, provider)) == 0 {
return user, nil return user, false, nil
} }
if !s.canUnbindProvider(provider, user, records) { if !s.canUnbindProvider(provider, user, records) {
return nil, ErrIdentityUnbindLastMethod return nil, false, ErrIdentityUnbindLastMethod
} }
if err := s.userRepo.UnbindUserAuthProvider(ctx, userID, provider); err != nil { if err := s.userRepo.UnbindUserAuthProvider(ctx, userID, provider); err != nil {
return nil, err return nil, false, err
} }
if s.authCacheInvalidator != nil { if s.authCacheInvalidator != nil {
s.authCacheInvalidator.InvalidateAuthCacheByUserID(ctx, userID) s.authCacheInvalidator.InvalidateAuthCacheByUserID(ctx, userID)
...@@ -306,9 +360,9 @@ func (s *UserService) UnbindUserAuthProvider(ctx context.Context, userID int64, ...@@ -306,9 +360,9 @@ func (s *UserService) UnbindUserAuthProvider(ctx context.Context, userID int64,
updatedUser, err := s.GetProfile(ctx, userID) updatedUser, err := s.GetProfile(ctx, userID)
if err != nil { if err != nil {
return nil, err return nil, false, err
} }
return updatedUser, nil return updatedUser, true, nil
} }
// UpdateProfile 更新用户资料 // UpdateProfile 更新用户资料
...@@ -608,6 +662,7 @@ func (s *UserService) buildProviderIdentitySummary(provider string, user *User, ...@@ -608,6 +662,7 @@ func (s *UserService) buildProviderIdentitySummary(provider string, user *User,
summary.Bound = true summary.Bound = true
summary.BoundCount = len(filtered) summary.BoundCount = len(filtered)
summary.DisplayName = userAuthIdentityDisplayName(primary) summary.DisplayName = userAuthIdentityDisplayName(primary)
summary.AvatarURL = strings.TrimSpace(firstStringIdentityValue(primary.Metadata, "avatar_url", "suggested_avatar_url", "headimgurl"))
summary.SubjectHint = maskOpaqueIdentity(primary.ProviderSubject) summary.SubjectHint = maskOpaqueIdentity(primary.ProviderSubject)
summary.ProviderKey = strings.TrimSpace(primary.ProviderKey) summary.ProviderKey = strings.TrimSpace(primary.ProviderKey)
summary.VerifiedAt = primary.VerifiedAt summary.VerifiedAt = primary.VerifiedAt
...@@ -625,7 +680,7 @@ func (s *UserService) canUnbindProvider(provider string, user *User, records []U ...@@ -625,7 +680,7 @@ func (s *UserService) canUnbindProvider(provider string, user *User, records []U
return false return false
} }
if s.buildEmailIdentitySummary(user, records).Bound { if s.canUseEmailAsSignInMethod(user, records) {
return true return true
} }
...@@ -641,6 +696,44 @@ func (s *UserService) canUnbindProvider(provider string, user *User, records []U ...@@ -641,6 +696,44 @@ func (s *UserService) canUnbindProvider(provider string, user *User, records []U
return false return false
} }
func (s *UserService) canUseEmailAsSignInMethod(user *User, records []UserAuthIdentityRecord) bool {
if user == nil {
return false
}
email := strings.ToLower(strings.TrimSpace(user.Email))
if email == "" || isReservedEmail(email) {
return false
}
if emailSignupSourceAllowsLogin(user.SignupSource) {
return true
}
for _, record := range filterUserAuthIdentities(records, "email") {
if emailIdentitySupportsSignIn(record) {
return true
}
}
return false
}
func emailSignupSourceAllowsLogin(signupSource string) bool {
signupSource = strings.ToLower(strings.TrimSpace(signupSource))
return signupSource == "" || signupSource == "email"
}
func emailIdentitySupportsSignIn(record UserAuthIdentityRecord) bool {
source := strings.TrimSpace(firstStringIdentityValue(record.Metadata, "source"))
switch source {
case "auth_service_email_bind", "auth_service_login_backfill", "auth_service_dual_write":
return true
default:
return false
}
}
func (s *UserService) listUserAuthIdentities(ctx context.Context, userID int64) ([]UserAuthIdentityRecord, error) { func (s *UserService) listUserAuthIdentities(ctx context.Context, userID int64) ([]UserAuthIdentityRecord, error) {
if userID <= 0 || s == nil || s.userRepo == nil { if userID <= 0 || s == nil || s.userRepo == nil {
return nil, nil return nil, nil
...@@ -662,11 +755,11 @@ func buildUserIdentityBindAuthorizeURL(provider, redirectTo string) (string, err ...@@ -662,11 +755,11 @@ func buildUserIdentityBindAuthorizeURL(provider, redirectTo string) (string, err
path := "" path := ""
switch provider { switch provider {
case "linuxdo": case "linuxdo":
path = "/api/v1/auth/oauth/linuxdo/start" path = "/api/v1/auth/oauth/linuxdo/bind/start"
case "oidc": case "oidc":
path = "/api/v1/auth/oauth/oidc/start" path = "/api/v1/auth/oauth/oidc/bind/start"
case "wechat": case "wechat":
path = "/api/v1/auth/oauth/wechat/start" path = "/api/v1/auth/oauth/wechat/bind/start"
default: default:
return "", ErrIdentityProviderInvalid return "", ErrIdentityProviderInvalid
} }
...@@ -842,12 +935,21 @@ func (s *UserService) GetByID(ctx context.Context, id int64) (*User, error) { ...@@ -842,12 +935,21 @@ func (s *UserService) GetByID(ctx context.Context, id int64) (*User, error) {
if err != nil { if err != nil {
return nil, fmt.Errorf("get user: %w", err) return nil, fmt.Errorf("get user: %w", err)
} }
normalizeLoadedUserTokenVersion(user)
if err := s.hydrateUserAvatar(ctx, user); err != nil { if err := s.hydrateUserAvatar(ctx, user); err != nil {
return nil, fmt.Errorf("get user avatar: %w", err) return nil, fmt.Errorf("get user avatar: %w", err)
} }
return user, nil return user, nil
} }
func normalizeLoadedUserTokenVersion(user *User) {
if user == nil || user.TokenVersionResolved {
return
}
user.TokenVersion = resolvedTokenVersion(user)
user.TokenVersionResolved = true
}
// TouchLastActive 通过防抖更新 users.last_active_at,减少鉴权热路径写放大。 // TouchLastActive 通过防抖更新 users.last_active_at,减少鉴权热路径写放大。
// 该操作为尽力而为,不应中断正常请求。 // 该操作为尽力而为,不应中断正常请求。
func (s *UserService) TouchLastActive(ctx context.Context, userID int64) { func (s *UserService) TouchLastActive(ctx context.Context, userID int64) {
......
...@@ -51,6 +51,44 @@ type mockUserRepoTxState struct { ...@@ -51,6 +51,44 @@ type mockUserRepoTxState struct {
deleteAvatarIDs []int64 deleteAvatarIDs []int64
} }
type mockUserSettingRepo struct {
values map[string]string
}
func (m *mockUserSettingRepo) Get(context.Context, string) (*Setting, error) {
panic("unexpected Get call")
}
func (m *mockUserSettingRepo) GetValue(context.Context, string) (string, error) {
panic("unexpected GetValue call")
}
func (m *mockUserSettingRepo) Set(context.Context, string, string) error {
panic("unexpected Set call")
}
func (m *mockUserSettingRepo) GetMultiple(_ context.Context, keys []string) (map[string]string, error) {
out := make(map[string]string, len(keys))
for _, key := range keys {
if value, ok := m.values[key]; ok {
out[key] = value
}
}
return out, nil
}
func (m *mockUserSettingRepo) SetMultiple(context.Context, map[string]string) error {
panic("unexpected SetMultiple call")
}
func (m *mockUserSettingRepo) GetAll(context.Context) (map[string]string, error) {
panic("unexpected GetAll call")
}
func (m *mockUserSettingRepo) Delete(context.Context, string) error {
panic("unexpected Delete call")
}
func (m *mockUserRepo) Create(context.Context, *User) error { return nil } func (m *mockUserRepo) Create(context.Context, *User) error { return nil }
func (m *mockUserRepo) GetByID(ctx context.Context, _ int64) (*User, error) { func (m *mockUserRepo) GetByID(ctx context.Context, _ int64) (*User, error) {
if m.getByIDErr != nil { if m.getByIDErr != nil {
...@@ -349,6 +387,70 @@ func TestUnbindUserAuthProviderRejectsLastRemainingLoginMethod(t *testing.T) { ...@@ -349,6 +387,70 @@ func TestUnbindUserAuthProviderRejectsLastRemainingLoginMethod(t *testing.T) {
require.Empty(t, repo.unboundProviders) require.Empty(t, repo.unboundProviders)
} }
func TestGetProfileIdentitySummaries_DoesNotTreatOAuthOnlyCompatEmailAsAlternativeLoginMethod(t *testing.T) {
repo := &mockUserRepo{
getByIDUser: &User{
ID: 10,
Email: "oauth-only@example.com",
SignupSource: "oidc",
},
identities: []UserAuthIdentityRecord{
{
ProviderType: "oidc",
ProviderKey: "https://issuer.example.com",
ProviderSubject: "oidc-only-subject",
},
},
}
svc := NewUserService(repo, nil, nil, nil)
summaries, err := svc.GetProfileIdentitySummaries(context.Background(), 10, repo.getByIDUser)
require.NoError(t, err)
require.False(t, summaries.OIDC.CanUnbind)
_, err = svc.UnbindUserAuthProvider(context.Background(), 10, "oidc")
require.ErrorIs(t, err, ErrIdentityUnbindLastMethod)
require.Empty(t, repo.unboundProviders)
}
func TestGetProfileIdentitySummaries_DoesNotTreatCompatBackfilledEmailIdentityAsAlternativeLoginMethod(t *testing.T) {
repo := &mockUserRepo{
getByIDUser: &User{
ID: 11,
Email: "oauth-only@example.com",
SignupSource: "wechat",
},
identities: []UserAuthIdentityRecord{
{
ProviderType: "email",
ProviderKey: "email",
ProviderSubject: "oauth-only@example.com",
Metadata: map[string]any{
"backfill_source": "users.email",
"migration": "109_auth_identity_compat_backfill",
},
},
{
ProviderType: "wechat",
ProviderKey: "wechat",
ProviderSubject: "wechat-only-subject",
},
},
}
svc := NewUserService(repo, nil, nil, nil)
summaries, err := svc.GetProfileIdentitySummaries(context.Background(), 11, repo.getByIDUser)
require.NoError(t, err)
require.True(t, summaries.Email.Bound)
require.False(t, summaries.WeChat.CanUnbind)
_, err = svc.UnbindUserAuthProvider(context.Background(), 11, "wechat")
require.ErrorIs(t, err, ErrIdentityUnbindLastMethod)
require.Empty(t, repo.unboundProviders)
}
func TestUnbindUserAuthProviderRemovesProviderAndReturnsUpdatedProfile(t *testing.T) { func TestUnbindUserAuthProviderRemovesProviderAndReturnsUpdatedProfile(t *testing.T) {
repo := &mockUserRepo{ repo := &mockUserRepo{
getByIDUser: &User{ getByIDUser: &User{
...@@ -368,13 +470,15 @@ func TestUnbindUserAuthProviderRemovesProviderAndReturnsUpdatedProfile(t *testin ...@@ -368,13 +470,15 @@ func TestUnbindUserAuthProviderRemovesProviderAndReturnsUpdatedProfile(t *testin
}, },
}, },
} }
svc := NewUserService(repo, nil, nil, nil) invalidator := &mockAuthCacheInvalidator{}
svc := NewUserService(repo, nil, invalidator, nil)
user, err := svc.UnbindUserAuthProvider(context.Background(), 12, "linuxdo") user, err := svc.UnbindUserAuthProvider(context.Background(), 12, "linuxdo")
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, []string{"linuxdo"}, repo.unboundProviders) require.Equal(t, []string{"linuxdo"}, repo.unboundProviders)
require.Equal(t, int64(12), user.ID) require.Equal(t, int64(12), user.ID)
require.Equal(t, []int64{12}, invalidator.invalidatedUserIDs)
summaries, err := svc.GetProfileIdentitySummaries(context.Background(), 12, user) summaries, err := svc.GetProfileIdentitySummaries(context.Background(), 12, user)
require.NoError(t, err) require.NoError(t, err)
...@@ -382,6 +486,71 @@ func TestUnbindUserAuthProviderRemovesProviderAndReturnsUpdatedProfile(t *testin ...@@ -382,6 +486,71 @@ func TestUnbindUserAuthProviderRemovesProviderAndReturnsUpdatedProfile(t *testin
require.True(t, summaries.LinuxDo.CanBind) require.True(t, summaries.LinuxDo.CanBind)
} }
func TestGetProfileIdentitySummaries_HidesBindActionWhenProviderExplicitlyDisabled(t *testing.T) {
repo := &mockUserRepo{
getByIDUser: &User{
ID: 15,
Email: "alice@example.com",
},
identities: []UserAuthIdentityRecord{
{
ProviderType: "email",
ProviderKey: "email",
ProviderSubject: "alice@example.com",
},
},
}
settingRepo := &mockUserSettingRepo{
values: map[string]string{
SettingKeyLinuxDoConnectEnabled: "false",
},
}
svc := NewUserService(repo, settingRepo, nil, nil)
summaries, err := svc.GetProfileIdentitySummaries(context.Background(), 15, repo.getByIDUser)
require.NoError(t, err)
require.False(t, summaries.LinuxDo.Bound)
require.False(t, summaries.LinuxDo.CanBind)
require.Empty(t, summaries.LinuxDo.BindStartPath)
}
func TestGetProfileIdentitySummaries_UsesBindStartRoute(t *testing.T) {
repo := &mockUserRepo{
getByIDUser: &User{
ID: 16,
Email: "alice@example.com",
},
identities: []UserAuthIdentityRecord{
{
ProviderType: "email",
ProviderKey: "email",
ProviderSubject: "alice@example.com",
},
},
}
svc := NewUserService(repo, nil, nil, nil)
summaries, err := svc.GetProfileIdentitySummaries(context.Background(), 16, repo.getByIDUser)
require.NoError(t, err)
require.Equal(
t,
"/api/v1/auth/oauth/linuxdo/bind/start?intent=bind_current_user&redirect=%2Fsettings%2Fprofile",
summaries.LinuxDo.BindStartPath,
)
require.Equal(
t,
"/api/v1/auth/oauth/oidc/bind/start?intent=bind_current_user&redirect=%2Fsettings%2Fprofile",
summaries.OIDC.BindStartPath,
)
require.Equal(
t,
"/api/v1/auth/oauth/wechat/bind/start?intent=bind_current_user&redirect=%2Fsettings%2Fprofile",
summaries.WeChat.BindStartPath,
)
}
func TestUpdateBalance_NilBillingCache_NoPanic(t *testing.T) { func TestUpdateBalance_NilBillingCache_NoPanic(t *testing.T) {
repo := &mockUserRepo{} repo := &mockUserRepo{}
svc := NewUserService(repo, nil, nil, nil) // billingCache = nil svc := NewUserService(repo, nil, nil, nil) // billingCache = nil
......
DO $$
BEGIN
IF EXISTS (
SELECT 1
FROM information_schema.columns
WHERE table_schema = 'public'
AND table_name = 'auth_identity_migration_reports'
AND column_name = 'report_type'
AND COALESCE(character_maximum_length, 0) < 80
) THEN
ALTER TABLE auth_identity_migration_reports
ALTER COLUMN report_type TYPE VARCHAR(80);
END IF;
END $$;
ALTER TABLE auth_identity_migration_reports
ALTER COLUMN report_type TYPE VARCHAR(80);
INSERT INTO auth_identities ( INSERT INTO auth_identities (
user_id, user_id,
provider_type, provider_type,
......
...@@ -38,23 +38,22 @@ VALUES ...@@ -38,23 +38,22 @@ VALUES
('auth_source_default_email_balance', '0'), ('auth_source_default_email_balance', '0'),
('auth_source_default_email_concurrency', '5'), ('auth_source_default_email_concurrency', '5'),
('auth_source_default_email_subscriptions', '[]'), ('auth_source_default_email_subscriptions', '[]'),
('auth_source_default_email_grant_on_signup', 'true'), ('auth_source_default_email_grant_on_signup', 'false'),
('auth_source_default_email_grant_on_first_bind', 'false'), ('auth_source_default_email_grant_on_first_bind', 'false'),
('auth_source_default_linuxdo_balance', '0'), ('auth_source_default_linuxdo_balance', '0'),
('auth_source_default_linuxdo_concurrency', '5'), ('auth_source_default_linuxdo_concurrency', '5'),
('auth_source_default_linuxdo_subscriptions', '[]'), ('auth_source_default_linuxdo_subscriptions', '[]'),
('auth_source_default_linuxdo_grant_on_signup', 'true'), ('auth_source_default_linuxdo_grant_on_signup', 'false'),
('auth_source_default_linuxdo_grant_on_first_bind', 'false'), ('auth_source_default_linuxdo_grant_on_first_bind', 'false'),
('auth_source_default_oidc_balance', '0'), ('auth_source_default_oidc_balance', '0'),
('auth_source_default_oidc_concurrency', '5'), ('auth_source_default_oidc_concurrency', '5'),
('auth_source_default_oidc_subscriptions', '[]'), ('auth_source_default_oidc_subscriptions', '[]'),
('auth_source_default_oidc_grant_on_signup', 'true'), ('auth_source_default_oidc_grant_on_signup', 'false'),
('auth_source_default_oidc_grant_on_first_bind', 'false'), ('auth_source_default_oidc_grant_on_first_bind', 'false'),
('auth_source_default_wechat_balance', '0'), ('auth_source_default_wechat_balance', '0'),
('auth_source_default_wechat_concurrency', '5'), ('auth_source_default_wechat_concurrency', '5'),
('auth_source_default_wechat_subscriptions', '[]'), ('auth_source_default_wechat_subscriptions', '[]'),
('auth_source_default_wechat_grant_on_signup', 'true'), ('auth_source_default_wechat_grant_on_signup', 'false'),
('auth_source_default_wechat_grant_on_first_bind', 'false'), ('auth_source_default_wechat_grant_on_first_bind', 'false'),
('force_email_on_third_party_signup', 'false') ('force_email_on_third_party_signup', 'false')
ON CONFLICT (key) DO NOTHING; ON CONFLICT (key) DO NOTHING;
ALTER TABLE payment_orders ADD COLUMN provider_key VARCHAR(30); ALTER TABLE payment_orders ADD COLUMN IF NOT EXISTS provider_key VARCHAR(30);
UPDATE payment_orders UPDATE payment_orders
SET provider_key = ( SET provider_key = (
......
...@@ -31,6 +31,41 @@ BEGIN ...@@ -31,6 +31,41 @@ BEGIN
END IF; END IF;
EXECUTE $sql$ EXECUTE $sql$
WITH legacy AS (
SELECT
uei.id,
uei.user_id,
BTRIM(uei.provider_user_id) AS provider_user_id,
BTRIM(uei.provider_username) AS provider_username,
BTRIM(uei.display_name) AS display_name,
public.__migration_115_safe_legacy_metadata_jsonb(uei.metadata) AS metadata_json,
uei.created_at,
uei.updated_at
FROM user_external_identities AS uei
JOIN users AS u ON u.id = uei.user_id
WHERE u.deleted_at IS NULL
AND LOWER(BTRIM(COALESCE(uei.provider, ''))) = 'linuxdo'
AND BTRIM(COALESCE(uei.provider_user_id, '')) <> ''
),
legacy_subjects AS (
SELECT
provider_user_id AS provider_subject,
COUNT(DISTINCT user_id) AS distinct_user_count
FROM legacy
GROUP BY provider_user_id
),
canonical_legacy AS (
SELECT
legacy.*,
ROW_NUMBER() OVER (
PARTITION BY legacy.provider_user_id
ORDER BY COALESCE(legacy.updated_at, legacy.created_at, NOW()) DESC, legacy.id DESC
) AS canonical_row_num
FROM legacy
JOIN legacy_subjects AS subjects
ON subjects.provider_subject = legacy.provider_user_id
AND subjects.distinct_user_count = 1
)
INSERT INTO auth_identities ( INSERT INTO auth_identities (
user_id, user_id,
provider_type, provider_type,
...@@ -52,11 +87,18 @@ SELECT ...@@ -52,11 +87,18 @@ SELECT
'display_name', legacy.display_name, 'display_name', legacy.display_name,
'migration', '115_auth_identity_legacy_external_backfill' 'migration', '115_auth_identity_legacy_external_backfill'
) )
FROM ( FROM canonical_legacy AS legacy
WHERE legacy.canonical_row_num = 1
ON CONFLICT (provider_type, provider_key, provider_subject) DO NOTHING;
$sql$;
EXECUTE $sql$
WITH legacy AS (
SELECT SELECT
uei.id, uei.id,
uei.user_id, uei.user_id,
BTRIM(uei.provider_user_id) AS provider_user_id, BTRIM(uei.provider_user_id) AS provider_user_id,
BTRIM(uei.provider_union_id) AS provider_union_id,
BTRIM(uei.provider_username) AS provider_username, BTRIM(uei.provider_username) AS provider_username,
BTRIM(uei.display_name) AS display_name, BTRIM(uei.display_name) AS display_name,
public.__migration_115_safe_legacy_metadata_jsonb(uei.metadata) AS metadata_json, public.__migration_115_safe_legacy_metadata_jsonb(uei.metadata) AS metadata_json,
...@@ -65,13 +107,28 @@ FROM ( ...@@ -65,13 +107,28 @@ FROM (
FROM user_external_identities AS uei FROM user_external_identities AS uei
JOIN users AS u ON u.id = uei.user_id JOIN users AS u ON u.id = uei.user_id
WHERE u.deleted_at IS NULL WHERE u.deleted_at IS NULL
AND LOWER(BTRIM(COALESCE(uei.provider, ''))) = 'linuxdo' AND LOWER(BTRIM(COALESCE(uei.provider, ''))) = 'wechat'
AND BTRIM(COALESCE(uei.provider_user_id, '')) <> '' AND BTRIM(COALESCE(uei.provider_union_id, '')) <> ''
) AS legacy ),
ON CONFLICT (provider_type, provider_key, provider_subject) DO NOTHING; legacy_subjects AS (
$sql$; SELECT
provider_union_id AS provider_subject,
EXECUTE $sql$ COUNT(DISTINCT user_id) AS distinct_user_count
FROM legacy
GROUP BY provider_union_id
),
canonical_legacy AS (
SELECT
legacy.*,
ROW_NUMBER() OVER (
PARTITION BY legacy.provider_union_id
ORDER BY COALESCE(legacy.updated_at, legacy.created_at, NOW()) DESC, legacy.id DESC
) AS canonical_row_num
FROM legacy
JOIN legacy_subjects AS subjects
ON subjects.provider_subject = legacy.provider_union_id
AND subjects.distinct_user_count = 1
)
INSERT INTO auth_identities ( INSERT INTO auth_identities (
user_id, user_id,
provider_type, provider_type,
...@@ -96,27 +153,36 @@ SELECT ...@@ -96,27 +153,36 @@ SELECT
'display_name', legacy.display_name, 'display_name', legacy.display_name,
'migration', '115_auth_identity_legacy_external_backfill' 'migration', '115_auth_identity_legacy_external_backfill'
) )
FROM ( FROM canonical_legacy AS legacy
WHERE legacy.canonical_row_num = 1
ON CONFLICT (provider_type, provider_key, provider_subject) DO NOTHING;
$sql$;
EXECUTE $sql$
WITH legacy AS (
SELECT SELECT
uei.id,
uei.user_id, uei.user_id,
BTRIM(uei.provider_user_id) AS provider_user_id, BTRIM(uei.provider_user_id) AS provider_user_id,
BTRIM(uei.provider_union_id) AS provider_union_id, BTRIM(uei.provider_union_id) AS provider_union_id,
BTRIM(uei.provider_username) AS provider_username, BTRIM(COALESCE(meta.metadata_json ->> 'channel', '')) AS channel,
BTRIM(uei.display_name) AS display_name, BTRIM(COALESCE(meta.metadata_json ->> 'channel_app_id', meta.metadata_json ->> 'appid', meta.metadata_json ->> 'app_id', '')) AS channel_app_id,
public.__migration_115_safe_legacy_metadata_jsonb(uei.metadata) AS metadata_json, meta.metadata_json
uei.created_at,
uei.updated_at
FROM user_external_identities AS uei FROM user_external_identities AS uei
JOIN users AS u ON u.id = uei.user_id JOIN users AS u ON u.id = uei.user_id
CROSS JOIN LATERAL (
SELECT public.__migration_115_safe_legacy_metadata_jsonb(uei.metadata) AS metadata_json
) AS meta
WHERE u.deleted_at IS NULL WHERE u.deleted_at IS NULL
AND LOWER(BTRIM(COALESCE(uei.provider, ''))) = 'wechat' AND LOWER(BTRIM(COALESCE(uei.provider, ''))) = 'wechat'
AND BTRIM(COALESCE(uei.provider_union_id, '')) <> '' AND BTRIM(COALESCE(uei.provider_union_id, '')) <> ''
) AS legacy ),
ON CONFLICT (provider_type, provider_key, provider_subject) DO NOTHING; legacy_subjects AS (
$sql$; SELECT
provider_union_id AS provider_subject,
EXECUTE $sql$ COUNT(DISTINCT user_id) AS distinct_user_count
FROM legacy
GROUP BY provider_union_id
)
INSERT INTO auth_identity_channels ( INSERT INTO auth_identity_channels (
identity_id, identity_id,
provider_type, provider_type,
...@@ -138,23 +204,10 @@ SELECT ...@@ -138,23 +204,10 @@ SELECT
'unionid', legacy.provider_union_id, 'unionid', legacy.provider_union_id,
'migration', '115_auth_identity_legacy_external_backfill' 'migration', '115_auth_identity_legacy_external_backfill'
) )
FROM ( FROM legacy
SELECT JOIN legacy_subjects AS subjects
uei.user_id, ON subjects.provider_subject = legacy.provider_union_id
BTRIM(uei.provider_user_id) AS provider_user_id, AND subjects.distinct_user_count = 1
BTRIM(uei.provider_union_id) AS provider_union_id,
BTRIM(COALESCE(meta.metadata_json ->> 'channel', '')) AS channel,
BTRIM(COALESCE(meta.metadata_json ->> 'channel_app_id', meta.metadata_json ->> 'appid', meta.metadata_json ->> 'app_id', '')) AS channel_app_id,
meta.metadata_json
FROM user_external_identities AS uei
JOIN users AS u ON u.id = uei.user_id
CROSS JOIN LATERAL (
SELECT public.__migration_115_safe_legacy_metadata_jsonb(uei.metadata) AS metadata_json
) AS meta
WHERE u.deleted_at IS NULL
AND LOWER(BTRIM(COALESCE(uei.provider, ''))) = 'wechat'
AND BTRIM(COALESCE(uei.provider_union_id, '')) <> ''
) AS legacy
JOIN auth_identities AS ai JOIN auth_identities AS ai
ON ai.user_id = legacy.user_id ON ai.user_id = legacy.user_id
AND ai.provider_type = 'wechat' AND ai.provider_type = 'wechat'
......
...@@ -74,6 +74,82 @@ $sql$; ...@@ -74,6 +74,82 @@ $sql$;
EXECUTE $sql$ EXECUTE $sql$
INSERT INTO auth_identity_migration_reports (report_type, report_key, details) INSERT INTO auth_identity_migration_reports (report_type, report_key, details)
SELECT
'legacy_external_identity_conflict',
'legacy_external_identity:' || legacy.id::text,
legacy.metadata_json || jsonb_build_object(
'legacy_identity_id', legacy.id,
'legacy_user_id', legacy.user_id,
'provider_type', legacy.provider_type,
'provider_key', legacy.provider_key,
'provider_subject', legacy.provider_subject,
'conflicting_legacy_user_ids', ambiguous.conflicting_legacy_user_ids,
'reason', 'legacy canonical identity subject belongs to multiple legacy users and cannot be auto-resolved',
'migration', '116_auth_identity_legacy_external_safety_reports'
)
FROM (
SELECT
uei.id,
uei.user_id,
LOWER(BTRIM(COALESCE(uei.provider, ''))) AS provider_type,
CASE
WHEN LOWER(BTRIM(COALESCE(uei.provider, ''))) = 'wechat' THEN 'wechat-main'
ELSE 'linuxdo'
END AS provider_key,
CASE
WHEN LOWER(BTRIM(COALESCE(uei.provider, ''))) = 'wechat' THEN BTRIM(COALESCE(uei.provider_union_id, ''))
ELSE BTRIM(COALESCE(uei.provider_user_id, ''))
END AS provider_subject,
public.__migration_116_safe_legacy_metadata_jsonb(uei.metadata) AS metadata_json
FROM user_external_identities AS uei
JOIN users AS u ON u.id = uei.user_id
WHERE u.deleted_at IS NULL
AND LOWER(BTRIM(COALESCE(uei.provider, ''))) IN ('linuxdo', 'wechat')
AND (
(LOWER(BTRIM(COALESCE(uei.provider, ''))) = 'linuxdo' AND BTRIM(COALESCE(uei.provider_user_id, '')) <> '')
OR
(LOWER(BTRIM(COALESCE(uei.provider, ''))) = 'wechat' AND BTRIM(COALESCE(uei.provider_union_id, '')) <> '')
)
) AS legacy
JOIN (
SELECT
provider_type,
provider_key,
provider_subject,
to_jsonb(array_agg(DISTINCT user_id ORDER BY user_id)) AS conflicting_legacy_user_ids
FROM (
SELECT
uei.user_id,
LOWER(BTRIM(COALESCE(uei.provider, ''))) AS provider_type,
CASE
WHEN LOWER(BTRIM(COALESCE(uei.provider, ''))) = 'wechat' THEN 'wechat-main'
ELSE 'linuxdo'
END AS provider_key,
CASE
WHEN LOWER(BTRIM(COALESCE(uei.provider, ''))) = 'wechat' THEN BTRIM(COALESCE(uei.provider_union_id, ''))
ELSE BTRIM(COALESCE(uei.provider_user_id, ''))
END AS provider_subject
FROM user_external_identities AS uei
JOIN users AS u ON u.id = uei.user_id
WHERE u.deleted_at IS NULL
AND LOWER(BTRIM(COALESCE(uei.provider, ''))) IN ('linuxdo', 'wechat')
AND (
(LOWER(BTRIM(COALESCE(uei.provider, ''))) = 'linuxdo' AND BTRIM(COALESCE(uei.provider_user_id, '')) <> '')
OR
(LOWER(BTRIM(COALESCE(uei.provider, ''))) = 'wechat' AND BTRIM(COALESCE(uei.provider_union_id, '')) <> '')
)
) AS legacy_subjects
GROUP BY provider_type, provider_key, provider_subject
HAVING COUNT(DISTINCT user_id) > 1
) AS ambiguous
ON ambiguous.provider_type = legacy.provider_type
AND ambiguous.provider_key = legacy.provider_key
AND ambiguous.provider_subject = legacy.provider_subject
ON CONFLICT (report_type, report_key) DO NOTHING;
$sql$;
EXECUTE $sql$
INSERT INTO auth_identity_migration_reports (report_type, report_key, details)
SELECT SELECT
'legacy_external_identity_conflict', 'legacy_external_identity_conflict',
'legacy_external_identity:' || legacy.id::text, 'legacy_external_identity:' || legacy.id::text,
...@@ -116,6 +192,39 @@ FROM ( ...@@ -116,6 +192,39 @@ FROM (
(LOWER(BTRIM(COALESCE(uei.provider, ''))) = 'wechat' AND BTRIM(COALESCE(uei.provider_union_id, '')) <> '') (LOWER(BTRIM(COALESCE(uei.provider, ''))) = 'wechat' AND BTRIM(COALESCE(uei.provider_union_id, '')) <> '')
) )
) AS legacy ) AS legacy
JOIN (
SELECT
provider_type,
provider_key,
provider_subject
FROM (
SELECT
uei.user_id,
LOWER(BTRIM(COALESCE(uei.provider, ''))) AS provider_type,
CASE
WHEN LOWER(BTRIM(COALESCE(uei.provider, ''))) = 'wechat' THEN 'wechat-main'
ELSE 'linuxdo'
END AS provider_key,
CASE
WHEN LOWER(BTRIM(COALESCE(uei.provider, ''))) = 'wechat' THEN BTRIM(COALESCE(uei.provider_union_id, ''))
ELSE BTRIM(COALESCE(uei.provider_user_id, ''))
END AS provider_subject
FROM user_external_identities AS uei
JOIN users AS u ON u.id = uei.user_id
WHERE u.deleted_at IS NULL
AND LOWER(BTRIM(COALESCE(uei.provider, ''))) IN ('linuxdo', 'wechat')
AND (
(LOWER(BTRIM(COALESCE(uei.provider, ''))) = 'linuxdo' AND BTRIM(COALESCE(uei.provider_user_id, '')) <> '')
OR
(LOWER(BTRIM(COALESCE(uei.provider, ''))) = 'wechat' AND BTRIM(COALESCE(uei.provider_union_id, '')) <> '')
)
) AS legacy_subjects
GROUP BY provider_type, provider_key, provider_subject
HAVING COUNT(DISTINCT user_id) = 1
) AS clear_subjects
ON clear_subjects.provider_type = legacy.provider_type
AND clear_subjects.provider_key = legacy.provider_key
AND clear_subjects.provider_subject = legacy.provider_subject
JOIN auth_identities AS ai JOIN auth_identities AS ai
ON ai.provider_type = legacy.provider_type ON ai.provider_type = legacy.provider_type
AND ai.provider_key = legacy.provider_key AND ai.provider_key = legacy.provider_key
...@@ -125,29 +234,7 @@ ON CONFLICT (report_type, report_key) DO NOTHING; ...@@ -125,29 +234,7 @@ ON CONFLICT (report_type, report_key) DO NOTHING;
$sql$; $sql$;
EXECUTE $sql$ EXECUTE $sql$
INSERT INTO auth_identities ( WITH legacy AS (
user_id,
provider_type,
provider_key,
provider_subject,
verified_at,
metadata
)
SELECT
legacy.user_id,
legacy.provider_type,
legacy.provider_key,
legacy.provider_subject,
legacy.verified_at,
legacy.metadata_json || jsonb_build_object(
'legacy_identity_id', legacy.id,
'provider_user_id', legacy.provider_user_id,
'provider_union_id', NULLIF(legacy.provider_union_id, ''),
'provider_username', legacy.provider_username,
'display_name', legacy.display_name,
'migration', '116_auth_identity_legacy_external_safety_reports'
)
FROM (
SELECT SELECT
uei.id, uei.id,
uei.user_id, uei.user_id,
...@@ -175,12 +262,58 @@ FROM ( ...@@ -175,12 +262,58 @@ FROM (
OR OR
(LOWER(BTRIM(COALESCE(uei.provider, ''))) = 'wechat' AND BTRIM(COALESCE(uei.provider_union_id, '')) <> '') (LOWER(BTRIM(COALESCE(uei.provider, ''))) = 'wechat' AND BTRIM(COALESCE(uei.provider_union_id, '')) <> '')
) )
) AS legacy ),
clear_subjects AS (
SELECT
provider_type,
provider_key,
provider_subject
FROM legacy
GROUP BY provider_type, provider_key, provider_subject
HAVING COUNT(DISTINCT user_id) = 1
),
canonical_legacy AS (
SELECT
legacy.*,
ROW_NUMBER() OVER (
PARTITION BY legacy.provider_type, legacy.provider_key, legacy.provider_subject
ORDER BY legacy.verified_at DESC, legacy.id DESC
) AS canonical_row_num
FROM legacy
JOIN clear_subjects
ON clear_subjects.provider_type = legacy.provider_type
AND clear_subjects.provider_key = legacy.provider_key
AND clear_subjects.provider_subject = legacy.provider_subject
)
INSERT INTO auth_identities (
user_id,
provider_type,
provider_key,
provider_subject,
verified_at,
metadata
)
SELECT
legacy.user_id,
legacy.provider_type,
legacy.provider_key,
legacy.provider_subject,
legacy.verified_at,
legacy.metadata_json || jsonb_build_object(
'legacy_identity_id', legacy.id,
'provider_user_id', legacy.provider_user_id,
'provider_union_id', NULLIF(legacy.provider_union_id, ''),
'provider_username', legacy.provider_username,
'display_name', legacy.display_name,
'migration', '116_auth_identity_legacy_external_safety_reports'
)
FROM canonical_legacy AS legacy
LEFT JOIN auth_identities AS ai LEFT JOIN auth_identities AS ai
ON ai.provider_type = legacy.provider_type ON ai.provider_type = legacy.provider_type
AND ai.provider_key = legacy.provider_key AND ai.provider_key = legacy.provider_key
AND ai.provider_subject = legacy.provider_subject AND ai.provider_subject = legacy.provider_subject
WHERE ai.id IS NULL WHERE legacy.canonical_row_num = 1
AND ai.id IS NULL
ON CONFLICT (provider_type, provider_key, provider_subject) DO NOTHING; ON CONFLICT (provider_type, provider_key, provider_subject) DO NOTHING;
$sql$; $sql$;
...@@ -225,6 +358,19 @@ FROM ( ...@@ -225,6 +358,19 @@ FROM (
AND BTRIM(COALESCE(uei.provider_union_id, '')) <> '' AND BTRIM(COALESCE(uei.provider_union_id, '')) <> ''
AND BTRIM(COALESCE(uei.provider_user_id, '')) <> '' AND BTRIM(COALESCE(uei.provider_user_id, '')) <> ''
) AS legacy ) AS legacy
JOIN (
SELECT
BTRIM(COALESCE(uei.provider_union_id, '')) AS provider_subject
FROM user_external_identities AS uei
JOIN users AS u ON u.id = uei.user_id
WHERE u.deleted_at IS NULL
AND LOWER(BTRIM(COALESCE(uei.provider, ''))) = 'wechat'
AND BTRIM(COALESCE(uei.provider_union_id, '')) <> ''
AND BTRIM(COALESCE(uei.provider_user_id, '')) <> ''
GROUP BY BTRIM(COALESCE(uei.provider_union_id, ''))
HAVING COUNT(DISTINCT uei.user_id) = 1
) AS clear_subjects
ON clear_subjects.provider_subject = legacy.provider_union_id
JOIN auth_identities AS legacy_ai JOIN auth_identities AS legacy_ai
ON legacy_ai.user_id = legacy.user_id ON legacy_ai.user_id = legacy.user_id
AND legacy_ai.provider_type = 'wechat' AND legacy_ai.provider_type = 'wechat'
...@@ -245,6 +391,33 @@ ON CONFLICT (report_type, report_key) DO NOTHING; ...@@ -245,6 +391,33 @@ ON CONFLICT (report_type, report_key) DO NOTHING;
$sql$; $sql$;
EXECUTE $sql$ EXECUTE $sql$
WITH legacy AS (
SELECT
uei.user_id,
BTRIM(COALESCE(uei.provider_user_id, '')) AS provider_user_id,
BTRIM(COALESCE(uei.provider_union_id, '')) AS provider_union_id,
public.__migration_116_safe_legacy_metadata_jsonb(uei.metadata) AS metadata_json,
BTRIM(COALESCE(public.__migration_116_safe_legacy_metadata_jsonb(uei.metadata) ->> 'channel', '')) AS channel,
BTRIM(COALESCE(
public.__migration_116_safe_legacy_metadata_jsonb(uei.metadata) ->> 'channel_app_id',
public.__migration_116_safe_legacy_metadata_jsonb(uei.metadata) ->> 'appid',
public.__migration_116_safe_legacy_metadata_jsonb(uei.metadata) ->> 'app_id',
''
)) AS channel_app_id
FROM user_external_identities AS uei
JOIN users AS u ON u.id = uei.user_id
WHERE u.deleted_at IS NULL
AND LOWER(BTRIM(COALESCE(uei.provider, ''))) = 'wechat'
AND BTRIM(COALESCE(uei.provider_union_id, '')) <> ''
AND BTRIM(COALESCE(uei.provider_user_id, '')) <> ''
),
clear_subjects AS (
SELECT
provider_union_id AS provider_subject
FROM legacy
GROUP BY provider_union_id
HAVING COUNT(DISTINCT user_id) = 1
)
INSERT INTO auth_identity_channels ( INSERT INTO auth_identity_channels (
identity_id, identity_id,
provider_type, provider_type,
...@@ -266,26 +439,9 @@ SELECT ...@@ -266,26 +439,9 @@ SELECT
'unionid', legacy.provider_union_id, 'unionid', legacy.provider_union_id,
'migration', '116_auth_identity_legacy_external_safety_reports' 'migration', '116_auth_identity_legacy_external_safety_reports'
) )
FROM ( FROM legacy
SELECT JOIN clear_subjects
uei.user_id, ON clear_subjects.provider_subject = legacy.provider_union_id
BTRIM(COALESCE(uei.provider_user_id, '')) AS provider_user_id,
BTRIM(COALESCE(uei.provider_union_id, '')) AS provider_union_id,
public.__migration_116_safe_legacy_metadata_jsonb(uei.metadata) AS metadata_json,
BTRIM(COALESCE(public.__migration_116_safe_legacy_metadata_jsonb(uei.metadata) ->> 'channel', '')) AS channel,
BTRIM(COALESCE(
public.__migration_116_safe_legacy_metadata_jsonb(uei.metadata) ->> 'channel_app_id',
public.__migration_116_safe_legacy_metadata_jsonb(uei.metadata) ->> 'appid',
public.__migration_116_safe_legacy_metadata_jsonb(uei.metadata) ->> 'app_id',
''
)) AS channel_app_id
FROM user_external_identities AS uei
JOIN users AS u ON u.id = uei.user_id
WHERE u.deleted_at IS NULL
AND LOWER(BTRIM(COALESCE(uei.provider, ''))) = 'wechat'
AND BTRIM(COALESCE(uei.provider_union_id, '')) <> ''
AND BTRIM(COALESCE(uei.provider_user_id, '')) <> ''
) AS legacy
JOIN auth_identities AS legacy_ai JOIN auth_identities AS legacy_ai
ON legacy_ai.user_id = legacy.user_id ON legacy_ai.user_id = legacy.user_id
AND legacy_ai.provider_type = 'wechat' AND legacy_ai.provider_type = 'wechat'
......
...@@ -3,6 +3,7 @@ VALUES ...@@ -3,6 +3,7 @@ VALUES
( (
'wechat_connect_open_enabled', 'wechat_connect_open_enabled',
CASE CASE
WHEN NOT EXISTS (SELECT 1 FROM settings WHERE key = 'wechat_connect_enabled') THEN ''
WHEN COALESCE((SELECT value FROM settings WHERE key = 'wechat_connect_enabled'), 'false') <> 'true' THEN 'false' WHEN COALESCE((SELECT value FROM settings WHERE key = 'wechat_connect_enabled'), 'false') <> 'true' THEN 'false'
WHEN LOWER(TRIM(COALESCE((SELECT value FROM settings WHERE key = 'wechat_connect_mode'), 'open'))) = 'mp' THEN 'false' WHEN LOWER(TRIM(COALESCE((SELECT value FROM settings WHERE key = 'wechat_connect_mode'), 'open'))) = 'mp' THEN 'false'
ELSE 'true' ELSE 'true'
...@@ -11,6 +12,7 @@ VALUES ...@@ -11,6 +12,7 @@ VALUES
( (
'wechat_connect_mp_enabled', 'wechat_connect_mp_enabled',
CASE CASE
WHEN NOT EXISTS (SELECT 1 FROM settings WHERE key = 'wechat_connect_enabled') THEN ''
WHEN COALESCE((SELECT value FROM settings WHERE key = 'wechat_connect_enabled'), 'false') <> 'true' THEN 'false' WHEN COALESCE((SELECT value FROM settings WHERE key = 'wechat_connect_enabled'), 'false') <> 'true' THEN 'false'
WHEN LOWER(TRIM(COALESCE((SELECT value FROM settings WHERE key = 'wechat_connect_mode'), 'open'))) = 'mp' THEN 'true' WHEN LOWER(TRIM(COALESCE((SELECT value FROM settings WHERE key = 'wechat_connect_mode'), 'open'))) = 'mp' THEN 'true'
ELSE 'false' ELSE 'false'
...@@ -21,12 +23,3 @@ VALUES ...@@ -21,12 +23,3 @@ VALUES
('auth_source_default_oidc_grant_on_signup', 'false'), ('auth_source_default_oidc_grant_on_signup', 'false'),
('auth_source_default_wechat_grant_on_signup', 'false') ('auth_source_default_wechat_grant_on_signup', 'false')
ON CONFLICT (key) DO NOTHING; ON CONFLICT (key) DO NOTHING;
UPDATE settings
SET value = 'false'
WHERE key IN (
'auth_source_default_email_grant_on_signup',
'auth_source_default_linuxdo_grant_on_signup',
'auth_source_default_oidc_grant_on_signup',
'auth_source_default_wechat_grant_on_signup'
);
-- Intentionally left as a no-op.
-- The online index rollout lives in 120_enforce_payment_orders_out_trade_no_unique_notx.sql
DO $$
BEGIN
NULL;
END $$;
-- Build the payment order uniqueness guarantee online.
-- The migration runner performs an explicit duplicate out_trade_no precheck and
-- drops any stale invalid paymentorder_out_trade_no_unique index before retrying.
-- Create the new partial unique index concurrently first so writes keep flowing,
-- then remove the legacy index name once the replacement is ready.
CREATE UNIQUE INDEX CONCURRENTLY IF NOT EXISTS paymentorder_out_trade_no_unique
ON payment_orders (out_trade_no)
WHERE out_trade_no <> '';
DROP INDEX CONCURRENTLY IF EXISTS paymentorder_out_trade_no;
DO $$
BEGIN
IF EXISTS (
SELECT 1
FROM pg_indexes
WHERE schemaname = 'public'
AND tablename = 'payment_orders'
AND indexname = 'paymentorder_out_trade_no_unique'
) THEN
IF EXISTS (
SELECT 1
FROM pg_indexes
WHERE schemaname = 'public'
AND tablename = 'payment_orders'
AND indexname = 'paymentorder_out_trade_no'
) THEN
EXECUTE 'DROP INDEX IF EXISTS paymentorder_out_trade_no';
END IF;
EXECUTE 'ALTER INDEX paymentorder_out_trade_no_unique RENAME TO paymentorder_out_trade_no';
END IF;
END $$;
ALTER TABLE auth_identity_migration_reports
ALTER COLUMN report_type TYPE VARCHAR(80);
UPDATE pending_auth_sessions
SET
local_flow_state = jsonb_set(
local_flow_state,
'{completion_response}',
((local_flow_state -> 'completion_response') - 'access_token' - 'refresh_token' - 'expires_in' - 'token_type'),
true
)
WHERE jsonb_typeof(local_flow_state -> 'completion_response') = 'object'
AND (
(local_flow_state -> 'completion_response') ? 'access_token'
OR (local_flow_state -> 'completion_response') ? 'refresh_token'
OR (local_flow_state -> 'completion_response') ? 'expires_in'
OR (local_flow_state -> 'completion_response') ? 'token_type'
);
-- Auto-backfill untouched migration 110 signup-grant defaults to the corrected false value.
-- Rows still matching the migration-110 default payload and timestamp window are treated as
-- untouched legacy defaults; any remaining legacy true values are reported for manual review.
WITH migration_110 AS (
SELECT applied_at
FROM schema_migrations
WHERE filename = '110_pending_auth_and_provider_default_grants.sql'
),
providers AS (
SELECT provider_type
FROM (
VALUES ('email'), ('linuxdo'), ('oidc'), ('wechat')
) AS providers(provider_type)
),
legacy_provider_defaults AS (
SELECT providers.provider_type
FROM providers
CROSS JOIN migration_110
JOIN settings balance
ON balance.key = 'auth_source_default_' || providers.provider_type || '_balance'
JOIN settings concurrency
ON concurrency.key = 'auth_source_default_' || providers.provider_type || '_concurrency'
JOIN settings subscriptions
ON subscriptions.key = 'auth_source_default_' || providers.provider_type || '_subscriptions'
JOIN settings grant_on_signup
ON grant_on_signup.key = 'auth_source_default_' || providers.provider_type || '_grant_on_signup'
JOIN settings grant_on_first_bind
ON grant_on_first_bind.key = 'auth_source_default_' || providers.provider_type || '_grant_on_first_bind'
WHERE balance.value = '0'
AND concurrency.value = '5'
AND subscriptions.value = '[]'
AND grant_on_signup.value = 'true'
AND grant_on_first_bind.value = 'false'
AND balance.updated_at BETWEEN migration_110.applied_at - INTERVAL '1 minute' AND migration_110.applied_at + INTERVAL '1 minute'
AND concurrency.updated_at BETWEEN migration_110.applied_at - INTERVAL '1 minute' AND migration_110.applied_at + INTERVAL '1 minute'
AND subscriptions.updated_at BETWEEN migration_110.applied_at - INTERVAL '1 minute' AND migration_110.applied_at + INTERVAL '1 minute'
AND grant_on_signup.updated_at BETWEEN migration_110.applied_at - INTERVAL '1 minute' AND migration_110.applied_at + INTERVAL '1 minute'
AND grant_on_first_bind.updated_at BETWEEN migration_110.applied_at - INTERVAL '1 minute' AND migration_110.applied_at + INTERVAL '1 minute'
),
updated_signup_grants AS (
UPDATE settings
SET
value = 'false',
updated_at = NOW()
FROM legacy_provider_defaults
WHERE settings.key = 'auth_source_default_' || legacy_provider_defaults.provider_type || '_grant_on_signup'
AND settings.value = 'true'
RETURNING legacy_provider_defaults.provider_type
)
INSERT INTO auth_identity_migration_reports (report_type, report_key, details)
SELECT
'legacy_auth_source_signup_grant_review',
providers.provider_type,
jsonb_build_object(
'provider_type', providers.provider_type,
'current_value', grant_on_signup.value,
'auto_backfilled', FALSE,
'reason', 'legacy_true_default_not_auto_backfilled'
)
FROM providers
JOIN settings grant_on_signup
ON grant_on_signup.key = 'auth_source_default_' || providers.provider_type || '_grant_on_signup'
LEFT JOIN updated_signup_grants
ON updated_signup_grants.provider_type = providers.provider_type
WHERE grant_on_signup.value = 'true'
AND updated_signup_grants.provider_type IS NULL
ON CONFLICT (report_type, report_key) DO NOTHING;
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment