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
067eb23d
Commit
067eb23d
authored
Apr 21, 2026
by
IanShaw027
Browse files
Tighten WeChat OAuth capability mode selection
parent
12f4af74
Changes
15
Hide whitespace changes
Inline
Side-by-side
backend/internal/handler/dto/settings.go
View file @
067eb23d
...
...
@@ -200,6 +200,8 @@ type PublicSettings struct {
CustomEndpoints
[]
CustomEndpoint
`json:"custom_endpoints"`
LinuxDoOAuthEnabled
bool
`json:"linuxdo_oauth_enabled"`
WeChatOAuthEnabled
bool
`json:"wechat_oauth_enabled"`
WeChatOAuthOpenEnabled
bool
`json:"wechat_oauth_open_enabled"`
WeChatOAuthMPEnabled
bool
`json:"wechat_oauth_mp_enabled"`
OIDCOAuthEnabled
bool
`json:"oidc_oauth_enabled"`
OIDCOAuthProviderName
string
`json:"oidc_oauth_provider_name"`
SoraClientEnabled
bool
`json:"sora_client_enabled"`
...
...
backend/internal/handler/setting_handler.go
View file @
067eb23d
...
...
@@ -58,6 +58,8 @@ func (h *SettingHandler) GetPublicSettings(c *gin.Context) {
CustomEndpoints
:
dto
.
ParseCustomEndpoints
(
settings
.
CustomEndpoints
),
LinuxDoOAuthEnabled
:
settings
.
LinuxDoOAuthEnabled
,
WeChatOAuthEnabled
:
settings
.
WeChatOAuthEnabled
,
WeChatOAuthOpenEnabled
:
settings
.
WeChatOAuthOpenEnabled
,
WeChatOAuthMPEnabled
:
settings
.
WeChatOAuthMPEnabled
,
OIDCOAuthEnabled
:
settings
.
OIDCOAuthEnabled
,
OIDCOAuthProviderName
:
settings
.
OIDCOAuthProviderName
,
BackendModeEnabled
:
settings
.
BackendModeEnabled
,
...
...
backend/internal/handler/setting_handler_public_test.go
View file @
067eb23d
...
...
@@ -81,3 +81,35 @@ func TestSettingHandler_GetPublicSettings_ExposesForceEmailOnThirdPartySignup(t
require
.
Equal
(
t
,
0
,
resp
.
Code
)
require
.
True
(
t
,
resp
.
Data
.
ForceEmailOnThirdPartySignup
)
}
func
TestSettingHandler_GetPublicSettings_ExposesWeChatOAuthModeCapabilities
(
t
*
testing
.
T
)
{
gin
.
SetMode
(
gin
.
TestMode
)
t
.
Setenv
(
"WECHAT_OAUTH_OPEN_APP_ID"
,
"wx-open-app"
)
t
.
Setenv
(
"WECHAT_OAUTH_OPEN_APP_SECRET"
,
"wx-open-secret"
)
t
.
Setenv
(
"WECHAT_OAUTH_MP_APP_ID"
,
""
)
t
.
Setenv
(
"WECHAT_OAUTH_MP_APP_SECRET"
,
""
)
h
:=
NewSettingHandler
(
service
.
NewSettingService
(
&
settingHandlerPublicRepoStub
{},
&
config
.
Config
{}),
"test-version"
)
recorder
:=
httptest
.
NewRecorder
()
c
,
_
:=
gin
.
CreateTestContext
(
recorder
)
c
.
Request
=
httptest
.
NewRequest
(
http
.
MethodGet
,
"/api/v1/settings/public"
,
nil
)
h
.
GetPublicSettings
(
c
)
require
.
Equal
(
t
,
http
.
StatusOK
,
recorder
.
Code
)
var
resp
struct
{
Code
int
`json:"code"`
Data
struct
{
WeChatOAuthEnabled
bool
`json:"wechat_oauth_enabled"`
WeChatOAuthOpenEnabled
bool
`json:"wechat_oauth_open_enabled"`
WeChatOAuthMPEnabled
bool
`json:"wechat_oauth_mp_enabled"`
}
`json:"data"`
}
require
.
NoError
(
t
,
json
.
Unmarshal
(
recorder
.
Body
.
Bytes
(),
&
resp
))
require
.
Equal
(
t
,
0
,
resp
.
Code
)
require
.
True
(
t
,
resp
.
Data
.
WeChatOAuthEnabled
)
require
.
True
(
t
,
resp
.
Data
.
WeChatOAuthOpenEnabled
)
require
.
False
(
t
,
resp
.
Data
.
WeChatOAuthMPEnabled
)
}
backend/internal/service/setting_service.go
View file @
067eb23d
...
...
@@ -274,7 +274,9 @@ func (s *SettingService) GetPublicSettings(ctx context.Context) (*PublicSettings
if
oidcProviderName
==
""
{
oidcProviderName
=
"OIDC"
}
weChatEnabled
:=
isWeChatOAuthConfigured
()
weChatOpenEnabled
:=
isWeChatOAuthOpenConfigured
()
weChatMPEnabled
:=
isWeChatOAuthMPConfigured
()
weChatEnabled
:=
weChatOpenEnabled
||
weChatMPEnabled
// Password reset requires email verification to be enabled
emailVerifyEnabled
:=
settings
[
SettingKeyEmailVerifyEnabled
]
==
"true"
...
...
@@ -319,6 +321,8 @@ func (s *SettingService) GetPublicSettings(ctx context.Context) (*PublicSettings
CustomEndpoints
:
settings
[
SettingKeyCustomEndpoints
],
LinuxDoOAuthEnabled
:
linuxDoEnabled
,
WeChatOAuthEnabled
:
weChatEnabled
,
WeChatOAuthOpenEnabled
:
weChatOpenEnabled
,
WeChatOAuthMPEnabled
:
weChatMPEnabled
,
BackendModeEnabled
:
settings
[
SettingKeyBackendModeEnabled
]
==
"true"
,
PaymentEnabled
:
settings
[
SettingPaymentEnabled
]
==
"true"
,
OIDCOAuthEnabled
:
oidcEnabled
,
...
...
@@ -376,6 +380,8 @@ func (s *SettingService) GetPublicSettingsForInjection(ctx context.Context) (any
CustomEndpoints
json
.
RawMessage
`json:"custom_endpoints"`
LinuxDoOAuthEnabled
bool
`json:"linuxdo_oauth_enabled"`
WeChatOAuthEnabled
bool
`json:"wechat_oauth_enabled"`
WeChatOAuthOpenEnabled
bool
`json:"wechat_oauth_open_enabled"`
WeChatOAuthMPEnabled
bool
`json:"wechat_oauth_mp_enabled"`
BackendModeEnabled
bool
`json:"backend_mode_enabled"`
PaymentEnabled
bool
`json:"payment_enabled"`
OIDCOAuthEnabled
bool
`json:"oidc_oauth_enabled"`
...
...
@@ -411,6 +417,8 @@ func (s *SettingService) GetPublicSettingsForInjection(ctx context.Context) (any
CustomEndpoints
:
safeRawJSONArray
(
settings
.
CustomEndpoints
),
LinuxDoOAuthEnabled
:
settings
.
LinuxDoOAuthEnabled
,
WeChatOAuthEnabled
:
settings
.
WeChatOAuthEnabled
,
WeChatOAuthOpenEnabled
:
settings
.
WeChatOAuthOpenEnabled
,
WeChatOAuthMPEnabled
:
settings
.
WeChatOAuthMPEnabled
,
BackendModeEnabled
:
settings
.
BackendModeEnabled
,
PaymentEnabled
:
settings
.
PaymentEnabled
,
OIDCOAuthEnabled
:
settings
.
OIDCOAuthEnabled
,
...
...
@@ -460,11 +468,17 @@ func filterUserVisibleMenuItems(raw string) json.RawMessage {
}
func
isWeChatOAuthConfigured
()
bool
{
openConfigured
:=
strings
.
TrimSpace
(
os
.
Getenv
(
"WECHAT_OAUTH_OPEN_APP_ID"
))
!=
""
&&
return
isWeChatOAuthOpenConfigured
()
||
isWeChatOAuthMPConfigured
()
}
func
isWeChatOAuthOpenConfigured
()
bool
{
return
strings
.
TrimSpace
(
os
.
Getenv
(
"WECHAT_OAUTH_OPEN_APP_ID"
))
!=
""
&&
strings
.
TrimSpace
(
os
.
Getenv
(
"WECHAT_OAUTH_OPEN_APP_SECRET"
))
!=
""
mpConfigured
:=
strings
.
TrimSpace
(
os
.
Getenv
(
"WECHAT_OAUTH_MP_APP_ID"
))
!=
""
&&
}
func
isWeChatOAuthMPConfigured
()
bool
{
return
strings
.
TrimSpace
(
os
.
Getenv
(
"WECHAT_OAUTH_MP_APP_ID"
))
!=
""
&&
strings
.
TrimSpace
(
os
.
Getenv
(
"WECHAT_OAUTH_MP_APP_SECRET"
))
!=
""
return
openConfigured
||
mpConfigured
}
// safeRawJSONArray returns raw as json.RawMessage if it's valid JSON, otherwise "[]".
...
...
backend/internal/service/setting_service_public_test.go
View file @
067eb23d
...
...
@@ -90,3 +90,18 @@ func TestSettingService_GetPublicSettings_ExposesForceEmailOnThirdPartySignup(t
require
.
NoError
(
t
,
err
)
require
.
True
(
t
,
settings
.
ForceEmailOnThirdPartySignup
)
}
func
TestSettingService_GetPublicSettings_ExposesWeChatOAuthModeCapabilities
(
t
*
testing
.
T
)
{
t
.
Setenv
(
"WECHAT_OAUTH_OPEN_APP_ID"
,
"wx-open-app"
)
t
.
Setenv
(
"WECHAT_OAUTH_OPEN_APP_SECRET"
,
"wx-open-secret"
)
t
.
Setenv
(
"WECHAT_OAUTH_MP_APP_ID"
,
""
)
t
.
Setenv
(
"WECHAT_OAUTH_MP_APP_SECRET"
,
""
)
svc
:=
NewSettingService
(
&
settingPublicRepoStub
{},
&
config
.
Config
{})
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
)
}
backend/internal/service/settings_view.go
View file @
067eb23d
...
...
@@ -161,13 +161,15 @@ type PublicSettings struct {
CustomMenuItems
string
// JSON array of custom menu items
CustomEndpoints
string
// JSON array of custom endpoints
LinuxDoOAuthEnabled
bool
WeChatOAuthEnabled
bool
BackendModeEnabled
bool
PaymentEnabled
bool
OIDCOAuthEnabled
bool
OIDCOAuthProviderName
string
Version
string
LinuxDoOAuthEnabled
bool
WeChatOAuthEnabled
bool
WeChatOAuthOpenEnabled
bool
WeChatOAuthMPEnabled
bool
BackendModeEnabled
bool
PaymentEnabled
bool
OIDCOAuthEnabled
bool
OIDCOAuthProviderName
string
Version
string
BalanceLowNotifyEnabled
bool
AccountQuotaNotifyEnabled
bool
...
...
frontend/src/api/auth.ts
View file @
067eb23d
...
...
@@ -363,7 +363,7 @@ export interface ResolvedWeChatOAuthStart {
unavailableReason
:
WeChatOAuthUnavailableReason
|
null
}
type
WeChatOAuthPublicSettings
=
{
export
type
WeChatOAuthPublicSettings
=
{
wechat_oauth_enabled
?:
boolean
wechat_oauth_open_enabled
?:
boolean
wechat_oauth_mp_enabled
?:
boolean
...
...
frontend/src/api/user.ts
View file @
067eb23d
...
...
@@ -4,7 +4,11 @@
*/
import
{
apiClient
}
from
'
./client
'
import
{
prepareOAuthBindAccessTokenCookie
}
from
'
./auth
'
import
{
prepareOAuthBindAccessTokenCookie
,
resolveWeChatOAuthStart
,
type
WeChatOAuthPublicSettings
,
}
from
'
./auth
'
import
type
{
User
,
ChangePasswordRequest
,
NotifyEmailEntry
,
UserAuthProvider
}
from
'
@/types
'
/**
...
...
@@ -89,6 +93,7 @@ export type BindableOAuthProvider = Exclude<UserAuthProvider, 'email'>
interface
BuildOAuthBindingStartURLOptions
{
redirectTo
?:
string
wechatOAuthSettings
?:
WeChatOAuthPublicSettings
|
null
}
export
function
resolveWeChatOAuthMode
():
'
open
'
|
'
mp
'
{
...
...
@@ -98,10 +103,19 @@ export function resolveWeChatOAuthMode(): 'open' | 'mp' {
return
/MicroMessenger/i
.
test
(
navigator
.
userAgent
)
?
'
mp
'
:
'
open
'
}
function
resolveWeChatOAuthBindingMode
(
settings
?:
WeChatOAuthPublicSettings
|
null
):
'
open
'
|
'
mp
'
|
null
{
if
(
settings
)
{
return
resolveWeChatOAuthStart
(
settings
).
mode
}
return
resolveWeChatOAuthMode
()
}
export
function
buildOAuthBindingStartURL
(
provider
:
BindableOAuthProvider
,
options
:
BuildOAuthBindingStartURLOptions
=
{}
):
string
{
):
string
|
null
{
const
redirectTo
=
options
.
redirectTo
?.
trim
()
||
'
/profile
'
const
apiBase
=
(
import
.
meta
.
env
.
VITE_API_BASE_URL
as
string
|
undefined
)
||
'
/api/v1
'
const
normalized
=
apiBase
.
replace
(
/
\/
$/
,
''
)
...
...
@@ -111,7 +125,11 @@ export function buildOAuthBindingStartURL(
})
if
(
provider
===
'
wechat
'
)
{
params
.
set
(
'
mode
'
,
resolveWeChatOAuthMode
())
const
mode
=
resolveWeChatOAuthBindingMode
(
options
.
wechatOAuthSettings
)
if
(
!
mode
)
{
return
null
}
params
.
set
(
'
mode
'
,
mode
)
}
return
`
${
normalized
}
/auth/oauth/
${
provider
}
/start?
${
params
.
toString
()}
`
...
...
@@ -124,8 +142,12 @@ export function startOAuthBinding(
if
(
typeof
window
===
'
undefined
'
)
{
return
}
const
startURL
=
buildOAuthBindingStartURL
(
provider
,
options
)
if
(
!
startURL
)
{
return
}
prepareOAuthBindAccessTokenCookie
()
window
.
location
.
href
=
buildOAuthBindingStartURL
(
provider
,
options
)
window
.
location
.
href
=
startURL
}
export
const
userAPI
=
{
...
...
frontend/src/components/user/profile/ProfileIdentityBindingsSection.vue
View file @
067eb23d
...
...
@@ -52,7 +52,9 @@
import
{
computed
}
from
'
vue
'
import
{
useI18n
}
from
'
vue-i18n
'
import
{
useRoute
}
from
'
vue-router
'
import
{
resolveWeChatOAuthStart
,
type
WeChatOAuthPublicSettings
}
from
'
@/api/auth
'
import
{
startOAuthBinding
}
from
'
@/api/user
'
import
{
useAppStore
}
from
'
@/stores
'
import
type
{
User
,
UserAuthBindingStatus
,
UserAuthProvider
}
from
'
@/types
'
const
props
=
withDefaults
(
...
...
@@ -62,17 +64,44 @@ const props = withDefaults(
oidcEnabled
?:
boolean
oidcProviderName
?:
string
wechatEnabled
?:
boolean
wechatOpenEnabled
?:
boolean
wechatMpEnabled
?:
boolean
}
>
(),
{
linuxdoEnabled
:
false
,
oidcEnabled
:
false
,
oidcProviderName
:
'
OIDC
'
,
wechatEnabled
:
false
,
wechatOpenEnabled
:
undefined
,
wechatMpEnabled
:
undefined
,
}
)
const
{
t
}
=
useI18n
()
const
route
=
useRoute
()
const
appStore
=
useAppStore
()
const
wechatOAuthSettings
=
computed
<
WeChatOAuthPublicSettings
|
null
>
(()
=>
{
if
(
appStore
.
cachedPublicSettings
)
{
return
appStore
.
cachedPublicSettings
}
if
(
typeof
props
.
wechatEnabled
===
'
boolean
'
||
typeof
props
.
wechatOpenEnabled
===
'
boolean
'
||
typeof
props
.
wechatMpEnabled
===
'
boolean
'
)
{
return
{
wechat_oauth_enabled
:
props
.
wechatEnabled
,
wechat_oauth_open_enabled
:
props
.
wechatOpenEnabled
,
wechat_oauth_mp_enabled
:
props
.
wechatMpEnabled
,
}
}
return
null
}
)
const
resolvedWeChatBinding
=
computed
(()
=>
resolveWeChatOAuthStart
(
wechatOAuthSettings
.
value
))
function
normalizeBindingStatus
(
binding
:
boolean
|
UserAuthBindingStatus
|
undefined
):
boolean
|
null
{
if
(
typeof
binding
===
'
boolean
'
)
{
...
...
@@ -129,7 +158,7 @@ const providerItems = computed(() => [
provider
:
'
wechat
'
as
const
,
label
:
t
(
'
profile.authBindings.providers.wechat
'
),
bound
:
getBindingStatus
(
'
wechat
'
),
canBind
:
props
.
wechatEnabled
&&
!
getBindingStatus
(
'
wechat
'
),
canBind
:
resolvedWeChatBinding
.
value
.
mode
!==
null
&&
!
getBindingStatus
(
'
wechat
'
),
}
,
])
...
...
@@ -139,6 +168,7 @@ function startBinding(provider: UserAuthProvider): void {
}
startOAuthBinding
(
provider
,
{
redirectTo
:
route
.
fullPath
||
'
/profile
'
,
wechatOAuthSettings
:
provider
===
'
wechat
'
?
wechatOAuthSettings
.
value
:
null
,
}
)
}
<
/script
>
frontend/src/components/user/profile/__tests__/ProfileIdentityBindingsSection.spec.ts
View file @
067eb23d
import
{
mount
}
from
'
@vue/test-utils
'
import
{
createPinia
,
setActivePinia
}
from
'
pinia
'
import
{
afterEach
,
beforeEach
,
describe
,
expect
,
it
,
vi
}
from
'
vitest
'
import
ProfileIdentityBindingsSection
from
'
@/components/user/profile/ProfileIdentityBindingsSection.vue
'
import
{
useAppStore
}
from
'
@/stores
'
import
type
{
User
}
from
'
@/types
'
const
routeState
=
vi
.
hoisted
(()
=>
({
...
...
@@ -11,6 +13,8 @@ const locationState = vi.hoisted(() => ({
current
:
{
href
:
'
http://localhost/profile
'
}
as
{
href
:
string
},
}))
let
pinia
:
ReturnType
<
typeof
createPinia
>
vi
.
mock
(
'
vue-router
'
,
()
=>
({
useRoute
:
()
=>
routeState
,
}))
...
...
@@ -57,6 +61,8 @@ function createUser(overrides: Partial<User> = {}): User {
describe
(
'
ProfileIdentityBindingsSection
'
,
()
=>
{
beforeEach
(()
=>
{
pinia
=
createPinia
()
setActivePinia
(
pinia
)
routeState
.
fullPath
=
'
/profile
'
locationState
.
current
=
{
href
:
'
http://localhost/profile
'
}
Object
.
defineProperty
(
window
,
'
location
'
,
{
...
...
@@ -67,6 +73,9 @@ describe('ProfileIdentityBindingsSection', () => {
configurable
:
true
,
value
:
'
Mozilla/5.0
'
,
})
const
appStore
=
useAppStore
()
appStore
.
cachedPublicSettings
=
null
appStore
.
publicSettingsLoaded
=
false
})
afterEach
(()
=>
{
...
...
@@ -75,6 +84,9 @@ describe('ProfileIdentityBindingsSection', () => {
it
(
'
renders provider binding states and provider-specific bind actions
'
,
()
=>
{
const
wrapper
=
mount
(
ProfileIdentityBindingsSection
,
{
global
:
{
plugins
:
[
pinia
],
},
props
:
{
user
:
createUser
({
auth_bindings
:
{
...
...
@@ -102,11 +114,16 @@ describe('ProfileIdentityBindingsSection', () => {
it
(
'
starts the WeChat bind flow for the current profile page
'
,
async
()
=>
{
const
wrapper
=
mount
(
ProfileIdentityBindingsSection
,
{
global
:
{
plugins
:
[
pinia
],
},
props
:
{
user
:
createUser
(),
linuxdoEnabled
:
false
,
oidcEnabled
:
false
,
wechatEnabled
:
true
,
wechatOpenEnabled
:
true
,
wechatMpEnabled
:
false
,
},
})
...
...
@@ -117,4 +134,22 @@ describe('ProfileIdentityBindingsSection', () => {
expect
(
locationState
.
current
.
href
).
toContain
(
'
intent=bind_current_user
'
)
expect
(
locationState
.
current
.
href
).
toContain
(
'
redirect=%2Fprofile
'
)
})
it
(
'
hides the WeChat bind action outside the WeChat browser when only mp mode is configured
'
,
()
=>
{
const
wrapper
=
mount
(
ProfileIdentityBindingsSection
,
{
global
:
{
plugins
:
[
pinia
],
},
props
:
{
user
:
createUser
(),
linuxdoEnabled
:
false
,
oidcEnabled
:
false
,
wechatEnabled
:
true
,
wechatOpenEnabled
:
false
,
wechatMpEnabled
:
true
,
},
})
expect
(
wrapper
.
find
(
'
[data-testid="profile-binding-wechat-action"]
'
).
exists
()).
toBe
(
false
)
})
})
frontend/src/stores/app.ts
View file @
067eb23d
...
...
@@ -338,6 +338,8 @@ export const useAppStore = defineStore('app', () => {
custom_endpoints
:
[],
linuxdo_oauth_enabled
:
false
,
wechat_oauth_enabled
:
false
,
wechat_oauth_open_enabled
:
false
,
wechat_oauth_mp_enabled
:
false
,
oidc_oauth_enabled
:
false
,
oidc_oauth_provider_name
:
'
OIDC
'
,
backend_mode_enabled
:
false
,
...
...
frontend/src/types/index.ts
View file @
067eb23d
...
...
@@ -165,6 +165,8 @@ export interface PublicSettings {
custom_endpoints
:
CustomEndpoint
[]
linuxdo_oauth_enabled
:
boolean
wechat_oauth_enabled
:
boolean
wechat_oauth_open_enabled
?:
boolean
wechat_oauth_mp_enabled
?:
boolean
oidc_oauth_enabled
:
boolean
oidc_oauth_provider_name
:
string
backend_mode_enabled
:
boolean
...
...
frontend/src/views/auth/WechatCallbackView.vue
View file @
067eb23d
...
...
@@ -297,6 +297,7 @@ import {
login2FA
,
prepareOAuthBindAccessTokenCookie
,
persistOAuthTokenContext
,
resolveWeChatOAuthStart
,
type
OAuthAdoptionDecision
,
type
PendingOAuthExchangeResponse
}
from
'
@/api/auth
'
...
...
@@ -378,7 +379,47 @@ function normalizeWeChatOAuthMode(value: unknown): 'open' | 'mp' | null {
return
value
===
'
open
'
||
value
===
'
mp
'
?
value
:
null
}
function
resolveRequestedWeChatOAuthMode
():
'
open
'
|
'
mp
'
{
async
function
ensurePublicSettingsLoaded
():
Promise
<
void
>
{
if
(
appStore
.
cachedPublicSettings
||
appStore
.
publicSettingsLoaded
)
{
return
}
try
{
await
appStore
.
fetchPublicSettings
()
}
catch
{
// Fall back to legacy mode selection when public settings are unavailable.
}
}
function
resolveConfiguredWeChatOAuthMode
():
'
open
'
|
'
mp
'
|
null
{
if
(
!
appStore
.
cachedPublicSettings
&&
!
appStore
.
publicSettingsLoaded
)
{
return
null
}
return
resolveWeChatOAuthStart
(
appStore
.
cachedPublicSettings
).
mode
}
function
resolveWeChatOAuthUnavailableMessage
():
string
{
const
resolved
=
resolveWeChatOAuthStart
(
appStore
.
cachedPublicSettings
)
switch
(
resolved
.
unavailableReason
)
{
case
'
external_browser_required
'
:
return
'
This WeChat sign-in flow is only available in your system browser.
'
case
'
wechat_browser_required
'
:
return
'
This WeChat sign-in flow is only available inside the WeChat browser.
'
case
'
not_configured
'
:
return
'
WeChat sign-in is not configured yet.
'
default
:
return
t
(
'
auth.loginFailed
'
)
}
}
function
resolveRequestedWeChatOAuthMode
():
'
open
'
|
'
mp
'
|
null
{
const
configuredMode
=
resolveConfiguredWeChatOAuthMode
()
if
(
configuredMode
)
{
return
configuredMode
}
const
queryMode
=
normalizeWeChatOAuthMode
(
route
.
query
.
mode
)
return
queryMode
||
resolveWeChatOAuthMode
()
}
...
...
@@ -389,11 +430,15 @@ function resolveRedirectTarget(): string {
)
}
function
resolveWeChatStartURL
(
intent
:
'
bind_current_user
'
|
'
adopt_existing_user_by_email
'
):
string
{
function
resolveWeChatStartURL
(
intent
:
'
bind_current_user
'
|
'
adopt_existing_user_by_email
'
):
string
|
null
{
const
apiBase
=
(
import
.
meta
.
env
.
VITE_API_BASE_URL
as
string
|
undefined
)
||
'
/api/v1
'
const
normalized
=
apiBase
.
replace
(
/
\/
$/
,
''
)
const
mode
=
resolveRequestedWeChatOAuthMode
()
if
(
!
mode
)
{
return
null
}
const
params
=
new
URLSearchParams
({
mode
:
resolveRequestedWeChatOAuthMode
()
,
mode
,
redirect
:
resolveRedirectTarget
(),
intent
,
}
)
...
...
@@ -406,11 +451,15 @@ function resolveWeChatStartURL(intent: 'bind_current_user' | 'adopt_existing_use
return
`${normalized
}
/auth/oauth/wechat/start?${params.toString()
}
`
}
function
buildExistingAccountResumePath
():
string
{
function
buildExistingAccountResumePath
():
string
|
null
{
const
mode
=
resolveRequestedWeChatOAuthMode
()
if
(
!
mode
)
{
return
null
}
const
params
=
new
URLSearchParams
({
wechat_bind_existing
:
'
1
'
,
redirect
:
resolveRedirectTarget
(),
mode
:
resolveRequestedWeChatOAuthMode
()
,
mode
,
}
)
const
email
=
existingAccountEmail
.
value
.
trim
()
...
...
@@ -444,14 +493,31 @@ function serializeAdoptionDecision(decision: OAuthAdoptionDecision): Record<stri
}
async
function
handleExistingAccountBinding
()
{
const
unavailableMessage
=
resolveConfiguredWeChatOAuthMode
()
===
null
?
resolveWeChatOAuthUnavailableMessage
()
:
''
if
(
getAuthToken
())
{
const
startURL
=
resolveWeChatStartURL
(
'
bind_current_user
'
)
if
(
!
startURL
)
{
errorMessage
.
value
=
unavailableMessage
||
resolveWeChatOAuthUnavailableMessage
()
appStore
.
showError
(
errorMessage
.
value
)
return
}
prepareOAuthBindAccessTokenCookie
()
window
.
location
.
href
=
resolveWeChatStartURL
(
'
bind_current_user
'
)
window
.
location
.
href
=
startURL
return
}
const
resumePath
=
buildExistingAccountResumePath
()
if
(
!
resumePath
)
{
errorMessage
.
value
=
unavailableMessage
||
resolveWeChatOAuthUnavailableMessage
()
appStore
.
showError
(
errorMessage
.
value
)
return
}
const
params
=
new
URLSearchParams
({
redirect
:
buildExistingAccountR
esumePath
()
,
redirect
:
r
esumePath
,
}
)
const
email
=
existingAccountEmail
.
value
.
trim
()
if
(
email
)
{
...
...
@@ -720,19 +786,36 @@ async function handleSubmitTotpChallenge() {
}
onMounted
(
async
()
=>
{
await
ensurePublicSettingsLoaded
()
if
(
typeof
route
.
query
.
email
===
'
string
'
)
{
existingAccountEmail
.
value
=
route
.
query
.
email
}
if
(
route
.
query
.
wechat_bind_existing
===
'
1
'
)
{
if
(
getAuthToken
())
{
const
startURL
=
resolveWeChatStartURL
(
'
bind_current_user
'
)
if
(
!
startURL
)
{
errorMessage
.
value
=
resolveWeChatOAuthUnavailableMessage
()
appStore
.
showError
(
errorMessage
.
value
)
isProcessing
.
value
=
false
return
}
prepareOAuthBindAccessTokenCookie
()
window
.
location
.
href
=
resolveWeChatStartURL
(
'
bind_current_user
'
)
window
.
location
.
href
=
startURL
return
}
const
resumePath
=
buildExistingAccountResumePath
()
if
(
!
resumePath
)
{
errorMessage
.
value
=
resolveWeChatOAuthUnavailableMessage
()
appStore
.
showError
(
errorMessage
.
value
)
isProcessing
.
value
=
false
return
}
const
params
=
new
URLSearchParams
({
redirect
:
buildExistingAccountR
esumePath
()
,
redirect
:
r
esumePath
,
}
)
const
email
=
existingAccountEmail
.
value
.
trim
()
if
(
email
)
{
...
...
frontend/src/views/auth/__tests__/WechatCallbackView.spec.ts
View file @
067eb23d
...
...
@@ -14,8 +14,10 @@ const {
setTokenMock
,
showSuccessMock
,
showErrorMock
,
fetchPublicSettingsMock
,
routeState
,
locationState
,
appStoreState
,
}
=
vi
.
hoisted
(()
=>
({
exchangePendingOAuthCompletionMock
:
vi
.
fn
(),
completeWeChatOAuthRegistrationMock
:
vi
.
fn
(),
...
...
@@ -28,6 +30,7 @@ const {
setTokenMock
:
vi
.
fn
(),
showSuccessMock
:
vi
.
fn
(),
showErrorMock
:
vi
.
fn
(),
fetchPublicSettingsMock
:
vi
.
fn
(),
routeState
:
{
query
:
{}
as
Record
<
string
,
unknown
>
,
},
...
...
@@ -39,6 +42,10 @@ const {
pathname
:
'
/auth/wechat/callback
'
}
as
{
href
:
string
;
hash
:
string
;
search
:
string
;
pathname
:
string
},
},
appStoreState
:
{
cachedPublicSettings
:
null
as
null
|
Record
<
string
,
unknown
>
,
publicSettingsLoaded
:
false
,
},
}))
vi
.
mock
(
'
vue-router
'
,
()
=>
({
...
...
@@ -102,8 +109,10 @@ vi.mock('@/stores', () => ({
setToken
:
setTokenMock
,
}),
useAppStore
:
()
=>
({
...
appStoreState
,
showSuccess
:
showSuccessMock
,
showError
:
showErrorMock
,
fetchPublicSettings
:
fetchPublicSettingsMock
,
}),
}))
...
...
@@ -139,7 +148,10 @@ describe('WechatCallbackView', () => {
showErrorMock
.
mockReset
()
prepareOAuthBindAccessTokenCookieMock
.
mockReset
()
getAuthTokenMock
.
mockReset
()
fetchPublicSettingsMock
.
mockReset
()
routeState
.
query
=
{}
appStoreState
.
cachedPublicSettings
=
null
appStoreState
.
publicSettingsLoaded
=
false
localStorage
.
clear
()
locationState
.
current
=
{
href
:
'
http://localhost/auth/wechat/callback
'
,
...
...
@@ -157,6 +169,38 @@ describe('WechatCallbackView', () => {
})
})
it
(
'
overrides an incompatible query mode with the configured open capability during bind recovery
'
,
async
()
=>
{
routeState
.
query
=
{
wechat_bind_existing
:
'
1
'
,
mode
:
'
mp
'
,
redirect
:
'
/profile
'
,
}
appStoreState
.
cachedPublicSettings
=
{
wechat_oauth_enabled
:
true
,
wechat_oauth_open_enabled
:
true
,
wechat_oauth_mp_enabled
:
false
,
}
appStoreState
.
publicSettingsLoaded
=
true
getAuthTokenMock
.
mockReturnValue
(
'
current-auth-token
'
)
mount
(
WechatCallbackView
,
{
global
:
{
stubs
:
{
AuthLayout
:
{
template
:
'
<div><slot /></div>
'
},
Icon
:
true
,
RouterLink
:
{
template
:
'
<a><slot /></a>
'
},
transition
:
false
,
},
},
})
await
flushPromises
()
expect
(
prepareOAuthBindAccessTokenCookieMock
).
toHaveBeenCalledTimes
(
1
)
expect
(
locationState
.
current
.
href
).
toContain
(
'
mode=open
'
)
expect
(
locationState
.
current
.
href
).
not
.
toContain
(
'
mode=mp
'
)
})
it
(
'
does not send adoption decisions during the initial exchange
'
,
async
()
=>
{
exchangePendingOAuthCompletionMock
.
mockResolvedValue
({
access_token
:
'
access-token
'
,
...
...
frontend/src/views/user/ProfileView.vue
View file @
067eb23d
...
...
@@ -67,7 +67,6 @@
<
script
setup
lang=
"ts"
>
import
{
computed
,
h
,
onMounted
,
ref
}
from
'
vue
'
import
{
useI18n
}
from
'
vue-i18n
'
import
{
authAPI
}
from
'
@/api
'
import
{
Icon
}
from
'
@/components/icons
'
import
StatCard
from
'
@/components/common/StatCard.vue
'
import
AppLayout
from
'
@/components/layout/AppLayout.vue
'
...
...
@@ -76,10 +75,12 @@ import ProfileEditForm from '@/components/user/profile/ProfileEditForm.vue'
import
ProfileInfoCard
from
'
@/components/user/profile/ProfileInfoCard.vue
'
import
ProfilePasswordForm
from
'
@/components/user/profile/ProfilePasswordForm.vue
'
import
ProfileTotpCard
from
'
@/components/user/profile/ProfileTotpCard.vue
'
import
{
useAppStore
}
from
'
@/stores/app
'
import
{
useAuthStore
}
from
'
@/stores/auth
'
import
{
formatDate
}
from
'
@/utils/format
'
const
{
t
}
=
useI18n
()
const
appStore
=
useAppStore
()
const
authStore
=
useAuthStore
()
const
user
=
computed
(()
=>
authStore
.
user
)
...
...
@@ -121,8 +122,11 @@ onMounted(async () => {
console
.
error
(
'
Failed to refresh profile:
'
,
error
)
})
const
settingsLoad
=
a
uthAPI
.
get
PublicSettings
()
const
settingsLoad
=
a
ppStore
.
fetch
PublicSettings
()
.
then
((
settings
)
=>
{
if
(
!
settings
)
{
return
}
contactInfo
.
value
=
settings
.
contact_info
||
''
balanceLowNotifyEnabled
.
value
=
settings
.
balance_low_notify_enabled
??
false
systemDefaultThreshold
.
value
=
settings
.
balance_low_notify_threshold
??
0
...
...
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