Skip to content
GitLab
Menu
Projects
Groups
Snippets
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in / Register
Toggle navigation
Menu
Open sidebar
陈曦
sub2api
Commits
ddf80f5e
Unverified
Commit
ddf80f5e
authored
Apr 22, 2026
by
Wesley Liddick
Committed by
GitHub
Apr 22, 2026
Browse files
Merge pull request #1799 from IanShaw027/rebuild/auth-identity-foundation
fix(auth,payment,profile): 修复认证身份和支付系统的后续问题
parents
4d0483f5
c048ca80
Changes
140
Show whitespace changes
Inline
Side-by-side
backend/internal/service/setting_service.go
View file @
ddf80f5e
...
...
@@ -245,15 +245,119 @@ 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
{
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 {
}
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,15 +668,11 @@ 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
{
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,33 +680,16 @@ 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
||
mobileReady
,
openReady
,
mpReady
,
mobileReady
return
openReady
||
mpReady
,
openReady
,
mpReady
,
mobileReady
}
// filterUserVisibleMenuItems filters out admin-only menu items from a raw JSON
...
...
@@ -756,6 +814,30 @@ func parseCustomMenuItemURLs(raw string) []string {
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 更新系统设置
func
(
s
*
SettingService
)
UpdateSettings
(
ctx
context
.
Context
,
settings
*
SystemSettings
)
error
{
updates
,
err
:=
s
.
buildSystemSettingsUpdates
(
ctx
,
settings
)
...
...
@@ -770,6 +852,28 @@ func (s *SettingService) UpdateSettings(ctx context.Context, settings *SystemSet
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.
func
(
s
*
SettingService
)
UpdateSettingsWithAuthSourceDefaults
(
ctx
context
.
Context
,
settings
*
SystemSettings
,
authDefaults
*
AuthSourceDefaultSettings
)
error
{
updates
,
err
:=
s
.
buildSystemSettingsUpdates
(
ctx
,
settings
)
...
...
@@ -1421,6 +1525,17 @@ func (s *SettingService) InitializeDefaultSettings(ctx context.Context) error {
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
{
SettingKeyRegistrationEnabled
:
"true"
,
...
...
@@ -1436,6 +1551,8 @@ func (s *SettingService) InitializeDefaultSettings(ctx context.Context) error {
SettingKeyCustomMenuItems
:
"[]"
,
SettingKeyCustomEndpoints
:
"[]"
,
SettingKeyWeChatConnectEnabled
:
"false"
,
SettingKeyWeChatConnectAppID
:
""
,
SettingKeyWeChatConnectAppSecret
:
""
,
SettingKeyWeChatConnectOpenAppID
:
""
,
SettingKeyWeChatConnectOpenAppSecret
:
""
,
SettingKeyWeChatConnectMPAppID
:
""
,
...
...
@@ -1447,9 +1564,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
:
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
),
SettingKeyDefaultBalance
:
strconv
.
FormatFloat
(
s
.
cfg
.
Default
.
UserBalance
,
'f'
,
8
,
64
),
SettingKeyDefaultSubscriptions
:
"[]"
,
...
...
@@ -1686,15 +1824,13 @@ func (s *SettingService) parseSettings(settings map[string]string) *SystemSettin
if
raw
,
ok
:=
settings
[
SettingKeyOIDCConnectUsePKCE
];
ok
{
result
.
OIDCConnectUsePKCE
=
raw
==
"true"
}
else
{
result
.
OIDCConnectUsePKCE
=
oidc
Base
.
UsePKCE
result
.
OIDCConnectUsePKCE
=
oidcUsePKCE
CompatibilityDefault
(
oidcBase
)
}
if
raw
,
ok
:=
settings
[
SettingKeyOIDCConnectValidateIDToken
];
ok
{
result
.
OIDCConnectValidateIDToken
=
raw
==
"true"
}
else
{
result
.
OIDCConnectValidateIDToken
=
oidc
Base
.
ValidateIDToken
result
.
OIDCConnectValidateIDToken
=
oidcValidateIDToken
CompatibilityDefault
(
oidcBase
)
}
result
.
OIDCConnectUsePKCE
=
true
result
.
OIDCConnectValidateIDToken
=
true
if
v
,
ok
:=
settings
[
SettingKeyOIDCConnectAllowedSigningAlgs
];
ok
&&
strings
.
TrimSpace
(
v
)
!=
""
{
result
.
OIDCConnectAllowedSigningAlgs
=
strings
.
TrimSpace
(
v
)
}
else
{
...
...
@@ -1739,37 +1875,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"
...
...
@@ -1861,14 +1990,9 @@ func isFalseSettingValue(value string) bool {
}
func
normalizeVisibleMethodSettingSource
(
method
,
source
string
,
enabled
bool
)
(
string
,
error
)
{
_
=
enabled
source
=
strings
.
TrimSpace
(
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
}
...
...
@@ -2196,8 +2320,6 @@ func (s *SettingService) GetLinuxDoConnectOAuthConfig(ctx context.Context) (conf
if
v
,
ok
:=
settings
[
SettingKeyLinuxDoConnectRedirectURL
];
ok
&&
strings
.
TrimSpace
(
v
)
!=
""
{
effective
.
RedirectURL
=
strings
.
TrimSpace
(
v
)
}
effective
.
UsePKCE
=
true
if
!
effective
.
Enabled
{
return
config
.
LinuxDoConnectConfig
{},
infraerrors
.
NotFound
(
"OAUTH_DISABLED"
,
"oauth login is disabled"
)
}
...
...
@@ -2417,12 +2539,14 @@ func (s *SettingService) GetOIDCConnectOAuthConfig(ctx context.Context) (config.
}
if
raw
,
ok
:=
settings
[
SettingKeyOIDCConnectUsePKCE
];
ok
{
effective
.
UsePKCE
=
raw
==
"true"
}
else
{
effective
.
UsePKCE
=
oidcUsePKCECompatibilityDefault
(
effective
)
}
if
raw
,
ok
:=
settings
[
SettingKeyOIDCConnectValidateIDToken
];
ok
{
effective
.
ValidateIDToken
=
raw
==
"true"
}
else
{
effective
.
ValidateIDToken
=
oidcValidateIDTokenCompatibilityDefault
(
effective
)
}
effective
.
UsePKCE
=
true
effective
.
ValidateIDToken
=
true
if
v
,
ok
:=
settings
[
SettingKeyOIDCConnectAllowedSigningAlgs
];
ok
&&
strings
.
TrimSpace
(
v
)
!=
""
{
effective
.
AllowedSigningAlgs
=
strings
.
TrimSpace
(
v
)
}
...
...
backend/internal/service/setting_service_oidc_config_test.go
View file @
ddf80f5e
...
...
@@ -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/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
)
}
backend/internal/service/setting_service_public_test.go
View file @
ddf80f5e
...
...
@@ -112,3 +112,42 @@ func TestSettingService_GetPublicSettings_ExposesWeChatOAuthModeCapabilities(t *
require
.
True
(
t
,
settings
.
WeChatOAuthOpenEnabled
)
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
)
}
backend/internal/service/setting_service_wechat_config_test.go
View file @
ddf80f5e
...
...
@@ -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
,
"/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
)
}
backend/internal/service/user.go
View file @
ddf80f5e
...
...
@@ -23,6 +23,9 @@ type User struct {
Status
string
AllowedGroups
[]
int64
TokenVersion
int64
// Incremented on password change to invalidate existing tokens
// TokenVersionResolved indicates TokenVersion already contains the fingerprint-derived
// value expected in JWT claims and refresh-token state.
TokenVersionResolved
bool
SignupSource
string
LastLoginAt
*
time
.
Time
LastActiveAt
*
time
.
Time
...
...
backend/internal/service/user_service.go
View file @
ddf80f5e
...
...
@@ -127,6 +127,7 @@ type UserIdentitySummary struct {
Bound
bool
`json:"bound"`
BoundCount
int
`json:"bound_count"`
DisplayName
string
`json:"display_name,omitempty"`
AvatarURL
string
`json:"-"`
SubjectHint
string
`json:"subject_hint,omitempty"`
ProviderKey
string
`json:"provider_key,omitempty"`
VerifiedAt
*
time
.
Time
`json:"verified_at,omitempty"`
...
...
@@ -228,6 +229,7 @@ func (s *UserService) GetProfile(ctx context.Context, userID int64) (*User, erro
if
err
!=
nil
{
return
nil
,
fmt
.
Errorf
(
"get user: %w"
,
err
)
}
normalizeLoadedUserTokenVersion
(
user
)
if
err
:=
s
.
hydrateUserAvatar
(
ctx
,
user
);
err
!=
nil
{
return
nil
,
fmt
.
Errorf
(
"get user avatar: %w"
,
err
)
}
...
...
@@ -248,12 +250,59 @@ func (s *UserService) GetProfileIdentitySummaries(ctx context.Context, userID in
return
UserIdentitySummarySet
{},
err
}
return
UserIdentitySummarySet
{
summaries
:=
UserIdentitySummarySet
{
Email
:
s
.
buildEmailIdentitySummary
(
user
,
records
),
LinuxDo
:
s
.
buildProviderIdentitySummary
(
"linuxdo"
,
user
,
records
),
OIDC
:
s
.
buildProviderIdentitySummary
(
"oidc"
,
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
)
{
...
...
@@ -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
)
{
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
)
if
provider
==
""
||
provider
==
"email"
{
return
nil
,
ErrIdentityProviderInvalid
return
nil
,
false
,
ErrIdentityProviderInvalid
}
user
,
err
:=
s
.
userRepo
.
GetByID
(
ctx
,
userID
)
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
)
if
err
!=
nil
{
return
nil
,
err
return
nil
,
false
,
err
}
if
len
(
filterUserAuthIdentities
(
records
,
provider
))
==
0
{
return
user
,
nil
return
user
,
false
,
nil
}
if
!
s
.
canUnbindProvider
(
provider
,
user
,
records
)
{
return
nil
,
ErrIdentityUnbindLastMethod
return
nil
,
false
,
ErrIdentityUnbindLastMethod
}
if
err
:=
s
.
userRepo
.
UnbindUserAuthProvider
(
ctx
,
userID
,
provider
);
err
!=
nil
{
return
nil
,
err
return
nil
,
false
,
err
}
if
s
.
authCacheInvalidator
!=
nil
{
s
.
authCacheInvalidator
.
InvalidateAuthCacheByUserID
(
ctx
,
userID
)
...
...
@@ -306,9 +360,9 @@ func (s *UserService) UnbindUserAuthProvider(ctx context.Context, userID int64,
updatedUser
,
err
:=
s
.
GetProfile
(
ctx
,
userID
)
if
err
!=
nil
{
return
nil
,
err
return
nil
,
false
,
err
}
return
updatedUser
,
nil
return
updatedUser
,
true
,
nil
}
// UpdateProfile 更新用户资料
...
...
@@ -608,6 +662,7 @@ func (s *UserService) buildProviderIdentitySummary(provider string, user *User,
summary
.
Bound
=
true
summary
.
BoundCount
=
len
(
filtered
)
summary
.
DisplayName
=
userAuthIdentityDisplayName
(
primary
)
summary
.
AvatarURL
=
strings
.
TrimSpace
(
firstStringIdentityValue
(
primary
.
Metadata
,
"avatar_url"
,
"suggested_avatar_url"
,
"headimgurl"
))
summary
.
SubjectHint
=
maskOpaqueIdentity
(
primary
.
ProviderSubject
)
summary
.
ProviderKey
=
strings
.
TrimSpace
(
primary
.
ProviderKey
)
summary
.
VerifiedAt
=
primary
.
VerifiedAt
...
...
@@ -625,7 +680,7 @@ func (s *UserService) canUnbindProvider(provider string, user *User, records []U
return
false
}
if
s
.
buildEmailIdentitySummary
(
user
,
records
)
.
Bound
{
if
s
.
canUseEmailAsSignInMethod
(
user
,
records
)
{
return
true
}
...
...
@@ -641,6 +696,44 @@ func (s *UserService) canUnbindProvider(provider string, user *User, records []U
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
)
{
if
userID
<=
0
||
s
==
nil
||
s
.
userRepo
==
nil
{
return
nil
,
nil
...
...
@@ -662,11 +755,11 @@ func buildUserIdentityBindAuthorizeURL(provider, redirectTo string) (string, err
path
:=
""
switch
provider
{
case
"linuxdo"
:
path
=
"/api/v1/auth/oauth/linuxdo/start"
path
=
"/api/v1/auth/oauth/linuxdo/
bind/
start"
case
"oidc"
:
path
=
"/api/v1/auth/oauth/oidc/start"
path
=
"/api/v1/auth/oauth/oidc/
bind/
start"
case
"wechat"
:
path
=
"/api/v1/auth/oauth/wechat/start"
path
=
"/api/v1/auth/oauth/wechat/
bind/
start"
default
:
return
""
,
ErrIdentityProviderInvalid
}
...
...
@@ -842,12 +935,21 @@ func (s *UserService) GetByID(ctx context.Context, id int64) (*User, error) {
if
err
!=
nil
{
return
nil
,
fmt
.
Errorf
(
"get user: %w"
,
err
)
}
normalizeLoadedUserTokenVersion
(
user
)
if
err
:=
s
.
hydrateUserAvatar
(
ctx
,
user
);
err
!=
nil
{
return
nil
,
fmt
.
Errorf
(
"get user avatar: %w"
,
err
)
}
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,减少鉴权热路径写放大。
// 该操作为尽力而为,不应中断正常请求。
func
(
s
*
UserService
)
TouchLastActive
(
ctx
context
.
Context
,
userID
int64
)
{
...
...
backend/internal/service/user_service_test.go
View file @
ddf80f5e
...
...
@@ -51,6 +51,44 @@ type mockUserRepoTxState struct {
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
)
GetByID
(
ctx
context
.
Context
,
_
int64
)
(
*
User
,
error
)
{
if
m
.
getByIDErr
!=
nil
{
...
...
@@ -349,6 +387,70 @@ func TestUnbindUserAuthProviderRejectsLastRemainingLoginMethod(t *testing.T) {
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
)
{
repo
:=
&
mockUserRepo
{
getByIDUser
:
&
User
{
...
...
@@ -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"
)
require
.
NoError
(
t
,
err
)
require
.
Equal
(
t
,
[]
string
{
"linuxdo"
},
repo
.
unboundProviders
)
require
.
Equal
(
t
,
int64
(
12
),
user
.
ID
)
require
.
Equal
(
t
,
[]
int64
{
12
},
invalidator
.
invalidatedUserIDs
)
summaries
,
err
:=
svc
.
GetProfileIdentitySummaries
(
context
.
Background
(),
12
,
user
)
require
.
NoError
(
t
,
err
)
...
...
@@ -382,6 +486,71 @@ func TestUnbindUserAuthProviderRemovesProviderAndReturnsUpdatedProfile(t *testin
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
)
{
repo
:=
&
mockUserRepo
{}
svc
:=
NewUserService
(
repo
,
nil
,
nil
,
nil
)
// billingCache = nil
...
...
backend/migrations/108a_widen_auth_identity_migration_report_type.sql
0 → 100644
View file @
ddf80f5e
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
$$
;
backend/migrations/109_auth_identity_compat_backfill.sql
View file @
ddf80f5e
ALTER
TABLE
auth_identity_migration_reports
ALTER
COLUMN
report_type
TYPE
VARCHAR
(
80
);
INSERT
INTO
auth_identities
(
user_id
,
provider_type
,
...
...
backend/migrations/110_pending_auth_and_provider_default_grants.sql
View file @
ddf80f5e
...
...
@@ -38,23 +38,22 @@ VALUES
(
'auth_source_default_email_balance'
,
'0'
),
(
'auth_source_default_email_concurrency'
,
'5'
),
(
'auth_source_default_email_subscriptions'
,
'[]'
),
(
'auth_source_default_email_grant_on_signup'
,
'
tru
e'
),
(
'auth_source_default_email_grant_on_signup'
,
'
fals
e'
),
(
'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'
,
'
tru
e'
),
(
'auth_source_default_linuxdo_grant_on_signup'
,
'
fals
e'
),
(
'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'
,
'
tru
e'
),
(
'auth_source_default_oidc_grant_on_signup'
,
'
fals
e'
),
(
'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'
,
'
tru
e'
),
(
'auth_source_default_wechat_grant_on_signup'
,
'
fals
e'
),
(
'auth_source_default_wechat_grant_on_first_bind'
,
'false'
),
(
'force_email_on_third_party_signup'
,
'false'
)
ON
CONFLICT
(
key
)
DO
NOTHING
;
backend/migrations/112_add_payment_order_provider_key_snapshot.sql
View file @
ddf80f5e
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
SET
provider_key
=
(
...
...
backend/migrations/115_auth_identity_legacy_external_backfill.sql
View file @
ddf80f5e
...
...
@@ -31,6 +31,41 @@ BEGIN
END
IF
;
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
(
user_id
,
provider_type
,
...
...
@@ -52,11 +87,18 @@ SELECT
'display_name'
,
legacy
.
display_name
,
'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
uei
.
id
,
uei
.
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
.
display_name
)
AS
display_name
,
public
.
__migration_115_safe_legacy_metadata_jsonb
(
uei
.
metadata
)
AS
metadata_json
,
...
...
@@ -65,13 +107,28 @@ FROM (
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
,
''
))
<>
''
)
AS
legacy
ON
CONFLICT
(
provider_type
,
provider_key
,
provider_subject
)
DO
NOTHING
;
$
sql
$
;
EXECUTE
$
sql
$
AND
LOWER
(
BTRIM
(
COALESCE
(
uei
.
provider
,
''
)))
=
'wechat'
AND
BTRIM
(
COALESCE
(
uei
.
provider_union_id
,
''
))
<>
''
),
legacy_subjects
AS
(
SELECT
provider_union_id
AS
provider_subject
,
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
(
user_id
,
provider_type
,
...
...
@@ -96,27 +153,36 @@ SELECT
'display_name'
,
legacy
.
display_name
,
'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
uei
.
id
,
uei
.
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
.
display_name
)
AS
display_name
,
public
.
__migration_115_safe_legacy_metadata_jsonb
(
uei
.
metadata
)
AS
metadata_json
,
uei
.
created_at
,
uei
.
updated_at
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
ON
CONFLICT
(
provider_type
,
provider_key
,
provider_subject
)
DO
NOTHING
;
$
sql
$
;
EXECUTE
$
sql
$
),
legacy_subjects
AS
(
SELECT
provider_union_id
AS
provider_subject
,
COUNT
(
DISTINCT
user_id
)
AS
distinct_user_count
FROM
legacy
GROUP
BY
provider_union_id
)
INSERT
INTO
auth_identity_channels
(
identity_id
,
provider_type
,
...
...
@@ -138,23 +204,10 @@ SELECT
'unionid'
,
legacy
.
provider_union_id
,
'migration'
,
'115_auth_identity_legacy_external_backfill'
)
FROM
(
SELECT
uei
.
user_id
,
BTRIM
(
uei
.
provider_user_id
)
AS
provider_user_id
,
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
FROM
legacy
JOIN
legacy_subjects
AS
subjects
ON
subjects
.
provider_subject
=
legacy
.
provider_union_id
AND
subjects
.
distinct_user_count
=
1
JOIN
auth_identities
AS
ai
ON
ai
.
user_id
=
legacy
.
user_id
AND
ai
.
provider_type
=
'wechat'
...
...
backend/migrations/116_auth_identity_legacy_external_safety_reports.sql
View file @
ddf80f5e
...
...
@@ -74,6 +74,82 @@ $sql$;
EXECUTE
$
sql
$
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
'legacy_external_identity_conflict'
,
'legacy_external_identity:'
||
legacy
.
id
::
text
,
...
...
@@ -116,6 +192,39 @@ FROM (
(
LOWER
(
BTRIM
(
COALESCE
(
uei
.
provider
,
''
)))
=
'wechat'
AND
BTRIM
(
COALESCE
(
uei
.
provider_union_id
,
''
))
<>
''
)
)
)
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
ON
ai
.
provider_type
=
legacy
.
provider_type
AND
ai
.
provider_key
=
legacy
.
provider_key
...
...
@@ -125,29 +234,7 @@ ON CONFLICT (report_type, report_key) DO NOTHING;
$
sql
$
;
EXECUTE
$
sql
$
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
(
WITH
legacy
AS
(
SELECT
uei
.
id
,
uei
.
user_id
,
...
...
@@ -175,12 +262,58 @@ FROM (
OR
(
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
ON
ai
.
provider_type
=
legacy
.
provider_type
AND
ai
.
provider_key
=
legacy
.
provider_key
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
;
$
sql
$
;
...
...
@@ -225,6 +358,19 @@ FROM (
AND
BTRIM
(
COALESCE
(
uei
.
provider_union_id
,
''
))
<>
''
AND
BTRIM
(
COALESCE
(
uei
.
provider_user_id
,
''
))
<>
''
)
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
ON
legacy_ai
.
user_id
=
legacy
.
user_id
AND
legacy_ai
.
provider_type
=
'wechat'
...
...
@@ -245,6 +391,33 @@ ON CONFLICT (report_type, report_key) DO NOTHING;
$
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
(
identity_id
,
provider_type
,
...
...
@@ -266,26 +439,9 @@ SELECT
'unionid'
,
legacy
.
provider_union_id
,
'migration'
,
'116_auth_identity_legacy_external_safety_reports'
)
FROM
(
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
,
''
))
<>
''
)
AS
legacy
FROM
legacy
JOIN
clear_subjects
ON
clear_subjects
.
provider_subject
=
legacy
.
provider_union_id
JOIN
auth_identities
AS
legacy_ai
ON
legacy_ai
.
user_id
=
legacy
.
user_id
AND
legacy_ai
.
provider_type
=
'wechat'
...
...
backend/migrations/118_wechat_dual_mode_and_auth_source_defaults.sql
View file @
ddf80f5e
...
...
@@ -3,6 +3,7 @@ VALUES
(
'wechat_connect_open_enabled'
,
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
LOWER
(
TRIM
(
COALESCE
((
SELECT
value
FROM
settings
WHERE
key
=
'wechat_connect_mode'
),
'open'
)))
=
'mp'
THEN
'false'
ELSE
'true'
...
...
@@ -11,6 +12,7 @@ VALUES
(
'wechat_connect_mp_enabled'
,
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
LOWER
(
TRIM
(
COALESCE
((
SELECT
value
FROM
settings
WHERE
key
=
'wechat_connect_mode'
),
'open'
)))
=
'mp'
THEN
'true'
ELSE
'false'
...
...
@@ -21,12 +23,3 @@ VALUES
(
'auth_source_default_oidc_grant_on_signup'
,
'false'
),
(
'auth_source_default_wechat_grant_on_signup'
,
'false'
)
ON
CONFLICT
(
key
)
DO
NOTHING
;
UPDATE
settings
SET
value
=
'false'
WHERE
key
IN
(
'auth_source_default_email_grant_on_signup'
,
'auth_source_default_linuxdo_grant_on_signup'
,
'auth_source_default_oidc_grant_on_signup'
,
'auth_source_default_wechat_grant_on_signup'
);
backend/migrations/119_enforce_payment_orders_out_trade_no_unique.sql
0 → 100644
View file @
ddf80f5e
-- 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
$$
;
backend/migrations/120_enforce_payment_orders_out_trade_no_unique_notx.sql
0 → 100644
View file @
ddf80f5e
-- 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
;
backend/migrations/120a_align_payment_orders_out_trade_no_index_name.sql
0 → 100644
View file @
ddf80f5e
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
$$
;
backend/migrations/121_auth_identity_migration_report_type_widen.sql
0 → 100644
View file @
ddf80f5e
ALTER
TABLE
auth_identity_migration_reports
ALTER
COLUMN
report_type
TYPE
VARCHAR
(
80
);
backend/migrations/122_pending_auth_completion_token_cleanup.sql
0 → 100644
View file @
ddf80f5e
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'
);
backend/migrations/123_fix_legacy_auth_source_grant_on_signup_defaults.sql
0 → 100644
View file @
ddf80f5e
-- 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
;
Prev
1
2
3
4
5
6
7
Next
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment